// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"errors"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/rogpeppe/go-internal/goproxytest"
	"github.com/rogpeppe/go-internal/gotooltest"
	"github.com/rogpeppe/go-internal/testscript"
	"github.com/rogpeppe/go-internal/txtar"
)

const (
	// goModProxyDir is the special subdirectory in a txtar script's supporting files
	// within which we expect to find github.com/rogpeppe/go-internal/goproxytest
	// directories.
	goModProxyDir = ".gomodproxy"
)

type envVarsFlag struct {
	vals []string
}

func (e *envVarsFlag) String() string {
	return fmt.Sprintf("%v", e.vals)
}

func (e *envVarsFlag) Set(v string) error {
	e.vals = append(e.vals, v)
	return nil
}

func main() {
	os.Exit(main1())
}

func main1() int {
	switch err := mainerr(); err {
	case nil:
		return 0
	case flag.ErrHelp:
		return 2
	default:
		fmt.Fprintln(os.Stderr, err)
		return 1
	}
}

func mainerr() (retErr error) {
	fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
	fs.Usage = func() {
		mainUsage(os.Stderr)
	}
	var envVars envVarsFlag
	fWork := fs.Bool("work", false, "print temporary work directory and do not remove when done")
	fVerbose := fs.Bool("v", false, "run tests verbosely")
	fs.Var(&envVars, "e", "pass through environment variable to script (can appear multiple times)")
	if err := fs.Parse(os.Args[1:]); err != nil {
		return err
	}

	td, err := ioutil.TempDir("", "testscript")
	if err != nil {
		return fmt.Errorf("unable to create temp dir: %v", err)
	}
	if *fWork {
		fmt.Fprintf(os.Stderr, "temporary work directory: %v\n", td)
	} else {
		defer os.RemoveAll(td)
	}

	files := fs.Args()
	if len(files) == 0 {
		files = []string{"-"}
	}

	for i, fileName := range files {
		// TODO make running files concurrent by default? If we do, note we'll need to do
		// something smarter with the runner stdout and stderr below
		runDir := filepath.Join(td, strconv.Itoa(i))
		if err := os.Mkdir(runDir, 0777); err != nil {
			return fmt.Errorf("failed to create a run directory within %v for %v: %v", td, fileName, err)
		}
		if err := run(runDir, fileName, *fVerbose, envVars.vals); err != nil {
			return err
		}
	}

	return nil
}

var (
	failedRun = errors.New("failed run")
	skipRun   = errors.New("skip")
)

type runner struct {
	verbose bool
}

func (r runner) Skip(is ...interface{}) {
	panic(skipRun)
}

func (r runner) Fatal(is ...interface{}) {
	r.Log(is...)
	r.FailNow()
}

func (r runner) Parallel() {
	// No-op for now; we are currently only running a single script in a
	// testscript instance.
}

func (r runner) Log(is ...interface{}) {
	fmt.Print(is...)
}

func (r runner) FailNow() {
	panic(failedRun)
}

func (r runner) Run(n string, f func(t testscript.T)) {
	// For now we we don't top/tail the run of a subtest. We are currently only
	// running a single script in a testscript instance, which means that we
	// will only have a single subtest.
	f(r)
}

func (r runner) Verbose() bool {
	return r.verbose
}

func run(runDir, fileName string, verbose bool, envVars []string) error {
	var ar *txtar.Archive
	var err error

	mods := filepath.Join(runDir, goModProxyDir)

	if err := os.MkdirAll(mods, 0777); err != nil {
		return fmt.Errorf("failed to create goModProxy dir: %v", err)
	}

	if fileName == "-" {
		fileName = "<stdin>"
		byts, err := ioutil.ReadAll(os.Stdin)
		if err != nil {
			return fmt.Errorf("failed to read from stdin: %v", err)
		}
		ar = txtar.Parse(byts)
	} else {
		ar, err = txtar.ParseFile(fileName)
	}

	if err != nil {
		return fmt.Errorf("failed to txtar parse %v: %v", fileName, err)
	}

	var script, gomodProxy txtar.Archive
	script.Comment = ar.Comment

	for _, f := range ar.Files {
		fp := filepath.Clean(filepath.FromSlash(f.Name))
		parts := strings.Split(fp, string(os.PathSeparator))

		if len(parts) > 1 && parts[0] == goModProxyDir {
			gomodProxy.Files = append(gomodProxy.Files, f)
		} else {
			script.Files = append(script.Files, f)
		}
	}

	if txtar.Write(&gomodProxy, runDir); err != nil {
		return fmt.Errorf("failed to write .gomodproxy files: %v", err)
	}

	if err := ioutil.WriteFile(filepath.Join(runDir, "script.txt"), txtar.Format(&script), 0666); err != nil {
		return fmt.Errorf("failed to write script for %v: %v", fileName, err)
	}

	p := testscript.Params{
		Dir: runDir,
	}

	if _, err := exec.LookPath("go"); err == nil {
		if err := gotooltest.Setup(&p); err != nil {
			return fmt.Errorf("failed to setup go tool for %v run: %v", fileName, err)
		}
	}

	addSetup := func(f func(env *testscript.Env) error) {
		origSetup := p.Setup
		p.Setup = func(env *testscript.Env) error {
			if origSetup != nil {
				if err := origSetup(env); err != nil {
					return err
				}
			}
			return f(env)
		}
	}

	if len(gomodProxy.Files) > 0 {
		srv, err := goproxytest.NewServer(mods, "")
		if err != nil {
			return fmt.Errorf("cannot start proxy for %v: %v", fileName, err)
		}
		defer srv.Close()

		addSetup(func(env *testscript.Env) error {
			// Add GOPROXY after calling the original setup
			// so that it overrides any GOPROXY set there.
			env.Vars = append(env.Vars,
				"GOPROXY="+srv.URL,
				"GONOSUMDB=*",
			)
			return nil
		})
	}

	if len(envVars) > 0 {
		addSetup(func(env *testscript.Env) error {
			for _, v := range envVars {
				if v == "WORK" {
					// cannot override WORK
					continue
				}
				env.Vars = append(env.Vars, v+"="+os.Getenv(v))
			}
			return nil
		})
	}

	r := runner{
		verbose: verbose,
	}

	func() {
		defer func() {
			switch recover() {
			case nil, skipRun:
			case failedRun:
				err = failedRun
			default:
				panic(fmt.Errorf("unexpected panic: %v [%T]", err, err))
			}
		}()
		testscript.RunT(r, p)
	}()

	if err != nil {
		return fmt.Errorf("error running %v in %v\n", fileName, runDir)
	}

	return nil
}
