// Copyright 2017 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.

//lint:file-ignore U1000 unused fns we might want to use later.

package test

import (
	"bytes"
	"flag"
	"fmt"
	"go/format"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"testing"

	"github.com/pkg/errors"
)

var (
	// ExeSuffix is the suffix of executable files; ".exe" on Windows.
	ExeSuffix string
	mu        sync.Mutex
	// PrintLogs controls logging of test commands.
	PrintLogs = flag.Bool("logs", false, "log stdin/stdout of test commands")
	// UpdateGolden controls updating test fixtures.
	UpdateGolden = flag.Bool("update", false, "update golden files")
)

const (
	manifestName = "Gopkg.toml"
	lockName     = "Gopkg.lock"
)

func init() {
	switch runtime.GOOS {
	case "windows":
		ExeSuffix = ".exe"
	}
}

// Helper with utilities for testing.
type Helper struct {
	t              *testing.T
	temps          []string
	wd             string
	origWd         string
	env            []string
	tempdir        string
	ran            bool
	inParallel     bool
	stdout, stderr bytes.Buffer
}

// NewHelper initializes a new helper for testing.
func NewHelper(t *testing.T) *Helper {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	return &Helper{t: t, origWd: wd}
}

// Must gives a fatal error if err is not nil.
func (h *Helper) Must(err error) {
	if err != nil {
		h.t.Fatalf("%+v", err)
	}
}

// check gives a test non-fatal error if err is not nil.
func (h *Helper) check(err error) {
	if err != nil {
		h.t.Errorf("%+v", err)
	}
}

// Parallel runs the test in parallel by calling t.Parallel.
func (h *Helper) Parallel() {
	if h.ran {
		h.t.Fatalf("%+v", errors.New("internal testsuite error: call to parallel after run"))
	}
	if h.wd != "" {
		h.t.Fatalf("%+v", errors.New("internal testsuite error: call to parallel after cd"))
	}
	for _, e := range h.env {
		if strings.HasPrefix(e, "GOROOT=") || strings.HasPrefix(e, "GOPATH=") || strings.HasPrefix(e, "GOBIN=") {
			val := e[strings.Index(e, "=")+1:]
			if strings.HasPrefix(val, "testdata") || strings.HasPrefix(val, "./testdata") {
				h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: call to parallel with testdata in environment (%s)", e))
			}
		}
	}
	h.inParallel = true
	h.t.Parallel()
}

// pwd returns the current directory.
func (h *Helper) pwd() string {
	wd, err := os.Getwd()
	if err != nil {
		h.t.Fatalf("%+v", errors.Wrap(err, "could not get working directory"))
	}
	return wd
}

// Cd changes the current directory to the named directory. Note that
// using this means that the test must not be run in parallel with any
// other tests.
func (h *Helper) Cd(dir string) {
	if h.inParallel {
		h.t.Fatalf("%+v", errors.New("internal testsuite error: changing directory when running in parallel"))
	}
	if h.wd == "" {
		h.wd = h.pwd()
	}
	abs, err := filepath.Abs(dir)
	if err == nil {
		h.Setenv("PWD", abs)
	}

	err = os.Chdir(dir)
	h.Must(errors.Wrapf(err, "Unable to cd to %s", dir))
}

// Setenv sets an environment variable to use when running the test go
// command.
func (h *Helper) Setenv(name, val string) {
	if h.inParallel && (name == "GOROOT" || name == "GOPATH" || name == "GOBIN") && (strings.HasPrefix(val, "testdata") || strings.HasPrefix(val, "./testdata")) {
		h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: call to setenv with testdata (%s=%s) after parallel", name, val))
	}
	h.unsetenv(name)
	h.env = append(h.env, name+"="+val)
}

// unsetenv removes an environment variable.
func (h *Helper) unsetenv(name string) {
	if h.env == nil {
		h.env = append([]string(nil), os.Environ()...)
	}
	for i, v := range h.env {
		if strings.HasPrefix(v, name+"=") {
			h.env = append(h.env[:i], h.env[i+1:]...)
			break
		}
	}
}

// DoRun runs the test go command, recording stdout and stderr and
// returning exit status.
func (h *Helper) DoRun(args []string) error {
	if h.inParallel {
		for _, arg := range args {
			if strings.HasPrefix(arg, "testdata") || strings.HasPrefix(arg, "./testdata") {
				h.t.Fatalf("%+v", errors.New("internal testsuite error: parallel run using testdata"))
			}
		}
	}
	if *PrintLogs {
		h.t.Logf("running testdep %v", args)
	}
	var prog string
	if h.wd == "" {
		prog = "./testdep" + ExeSuffix
	} else {
		prog = filepath.Join(h.wd, "testdep"+ExeSuffix)
	}
	newargs := args
	if args[0] != "check" {
		newargs = append([]string{args[0], "-v"}, args[1:]...)
	}

	cmd := exec.Command(prog, newargs...)
	h.stdout.Reset()
	h.stderr.Reset()
	cmd.Stdout = &h.stdout
	cmd.Stderr = &h.stderr
	cmd.Env = h.env
	status := cmd.Run()
	if *PrintLogs {
		if h.stdout.Len() > 0 {
			h.t.Log("standard output:")
			h.t.Log(h.stdout.String())
		}
		if h.stderr.Len() > 0 {
			h.t.Log("standard error:")
			h.t.Log(h.stderr.String())
		}
	}
	h.ran = true
	return errors.Wrapf(status, "Error running %s\n%s", strings.Join(newargs, " "), h.stderr.String())
}

// Run runs the test go command, and expects it to succeed.
func (h *Helper) Run(args ...string) {
	if runtime.GOOS == "windows" {
		mu.Lock()
		defer mu.Unlock()
	}
	if status := h.DoRun(args); status != nil {
		h.t.Logf("go %v failed unexpectedly: %v", args, status)
		h.t.FailNow()
	}
}

// runFail runs the test go command, and expects it to fail.
func (h *Helper) runFail(args ...string) {
	if status := h.DoRun(args); status == nil {
		h.t.Fatalf("%+v", errors.New("testgo succeeded unexpectedly"))
	} else {
		h.t.Log("testgo failed as expected:", status)
	}
}

// RunGo runs a go command, and expects it to succeed.
func (h *Helper) RunGo(args ...string) {
	cmd := exec.Command("go", args...)
	h.stdout.Reset()
	h.stderr.Reset()
	cmd.Stdout = &h.stdout
	cmd.Stderr = &h.stderr
	cmd.Dir = h.wd
	cmd.Env = h.env
	status := cmd.Run()
	if h.stdout.Len() > 0 {
		h.t.Log("go standard output:")
		h.t.Log(h.stdout.String())
	}
	if h.stderr.Len() > 0 {
		h.t.Log("go standard error:")
		h.t.Log(h.stderr.String())
	}
	if status != nil {
		h.t.Logf("go %v failed unexpectedly: %v", args, status)
		h.t.FailNow()
	}
}

// NeedsExternalNetwork makes sure the tests needing external network will not
// be run when executing tests in short mode.
func NeedsExternalNetwork(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping test: no external network in -short mode")
	}
}

// NeedsGit will make sure the tests that require git will be skipped if the
// git binary is not available.
func NeedsGit(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("skipping because git binary not found")
	}
}

// RunGit runs a git command, and expects it to succeed.
func (h *Helper) RunGit(dir string, args ...string) {
	cmd := exec.Command("git", args...)
	h.stdout.Reset()
	h.stderr.Reset()
	cmd.Stdout = &h.stdout
	cmd.Stderr = &h.stderr
	cmd.Dir = dir
	cmd.Env = h.env
	status := cmd.Run()
	if *PrintLogs {
		if h.stdout.Len() > 0 {
			h.t.Logf("git %v standard output:", args)
			h.t.Log(h.stdout.String())
		}
		if h.stderr.Len() > 0 {
			h.t.Logf("git %v standard error:", args)
			h.t.Log(h.stderr.String())
		}
	}
	if status != nil {
		h.t.Logf("git %v failed unexpectedly: %v", args, status)
		h.t.FailNow()
	}
}

// getStdout returns standard output of the testgo run as a string.
func (h *Helper) getStdout() string {
	if !h.ran {
		h.t.Fatalf("%+v", errors.New("internal testsuite error: stdout called before run"))
	}
	return h.stdout.String()
}

// getStderr returns standard error of the testgo run as a string.
func (h *Helper) getStderr() string {
	if !h.ran {
		h.t.Fatalf("%+v", errors.New("internal testsuite error: stdout called before run"))
	}
	return h.stderr.String()
}

// doGrepMatch looks for a regular expression in a buffer, and returns
// whether it is found. The regular expression is matched against
// each line separately, as with the grep command.
func (h *Helper) doGrepMatch(match string, b *bytes.Buffer) bool {
	if !h.ran {
		h.t.Fatalf("%+v", errors.New("internal testsuite error: grep called before run"))
	}
	re := regexp.MustCompile(match)
	for _, ln := range bytes.Split(b.Bytes(), []byte{'\n'}) {
		if re.Match(ln) {
			return true
		}
	}
	return false
}

// doGrep looks for a regular expression in a buffer and fails if it
// is not found. The name argument is the name of the output we are
// searching, "output" or "error".  The msg argument is logged on
// failure.
func (h *Helper) doGrep(match string, b *bytes.Buffer, name, msg string) {
	if !h.doGrepMatch(match, b) {
		h.t.Log(msg)
		h.t.Logf("pattern %v not found in standard %s", match, name)
		h.t.FailNow()
	}
}

// grepStdout looks for a regular expression in the test run's
// standard output and fails, logging msg, if it is not found.
func (h *Helper) grepStdout(match, msg string) {
	h.doGrep(match, &h.stdout, "output", msg)
}

// grepStderr looks for a regular expression in the test run's
// standard error and fails, logging msg, if it is not found.
func (h *Helper) grepStderr(match, msg string) {
	h.doGrep(match, &h.stderr, "error", msg)
}

// grepBoth looks for a regular expression in the test run's standard
// output or stand error and fails, logging msg, if it is not found.
func (h *Helper) grepBoth(match, msg string) {
	if !h.doGrepMatch(match, &h.stdout) && !h.doGrepMatch(match, &h.stderr) {
		h.t.Log(msg)
		h.t.Logf("pattern %v not found in standard output or standard error", match)
		h.t.FailNow()
	}
}

// doGrepNot looks for a regular expression in a buffer and fails if
// it is found. The name and msg arguments are as for doGrep.
func (h *Helper) doGrepNot(match string, b *bytes.Buffer, name, msg string) {
	if h.doGrepMatch(match, b) {
		h.t.Log(msg)
		h.t.Logf("pattern %v found unexpectedly in standard %s", match, name)
		h.t.FailNow()
	}
}

// grepStdoutNot looks for a regular expression in the test run's
// standard output and fails, logging msg, if it is found.
func (h *Helper) grepStdoutNot(match, msg string) {
	h.doGrepNot(match, &h.stdout, "output", msg)
}

// grepStderrNot looks for a regular expression in the test run's
// standard error and fails, logging msg, if it is found.
func (h *Helper) grepStderrNot(match, msg string) {
	h.doGrepNot(match, &h.stderr, "error", msg)
}

// grepBothNot looks for a regular expression in the test run's
// standard output or stand error and fails, logging msg, if it is
// found.
func (h *Helper) grepBothNot(match, msg string) {
	if h.doGrepMatch(match, &h.stdout) || h.doGrepMatch(match, &h.stderr) {
		h.t.Log(msg)
		h.t.Fatalf("%+v", errors.Errorf("pattern %v found unexpectedly in standard output or standard error", match))
	}
}

// doGrepCount counts the number of times a regexp is seen in a buffer.
func (h *Helper) doGrepCount(match string, b *bytes.Buffer) int {
	if !h.ran {
		h.t.Fatalf("%+v", errors.New("internal testsuite error: doGrepCount called before run"))
	}
	re := regexp.MustCompile(match)
	c := 0
	for _, ln := range bytes.Split(b.Bytes(), []byte{'\n'}) {
		if re.Match(ln) {
			c++
		}
	}
	return c
}

// grepCountBoth returns the number of times a regexp is seen in both
// standard output and standard error.
func (h *Helper) grepCountBoth(match string) int {
	return h.doGrepCount(match, &h.stdout) + h.doGrepCount(match, &h.stderr)
}

// creatingTemp records that the test plans to create a temporary file
// or directory. If the file or directory exists already, it will be
// removed. When the test completes, the file or directory will be
// removed if it exists.
func (h *Helper) creatingTemp(path string) {
	if filepath.IsAbs(path) && !strings.HasPrefix(path, h.tempdir) {
		h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: creatingTemp(%q) with absolute path not in temporary directory", path))
	}
	// If we have changed the working directory, make sure we have
	// an absolute path, because we are going to change directory
	// back before we remove the temporary.
	if h.wd != "" && !filepath.IsAbs(path) {
		path = filepath.Join(h.pwd(), path)
	}
	h.Must(os.RemoveAll(path))
	h.temps = append(h.temps, path)
}

// makeTempdir makes a temporary directory for a run of testgo. If
// the temporary directory was already created, this does nothing.
func (h *Helper) makeTempdir() {
	if h.tempdir == "" {
		var err error
		h.tempdir, err = ioutil.TempDir("", "gotest")
		h.Must(err)
	}
}

// TempFile adds a temporary file for a run of testgo.
func (h *Helper) TempFile(path, contents string) {
	h.makeTempdir()
	h.Must(os.MkdirAll(filepath.Join(h.tempdir, filepath.Dir(path)), 0755))
	bytes := []byte(contents)
	if strings.HasSuffix(path, ".go") {
		formatted, err := format.Source(bytes)
		if err == nil {
			bytes = formatted
		}
	}
	h.Must(ioutil.WriteFile(filepath.Join(h.tempdir, path), bytes, 0644))
}

// WriteTestFile writes a file to the testdata directory from memory.  src is
// relative to ./testdata.
func (h *Helper) WriteTestFile(src string, content string) error {
	err := ioutil.WriteFile(filepath.Join(h.origWd, "testdata", src), []byte(content), 0666)
	return err
}

// GetFile reads a file into memory
func (h *Helper) GetFile(path string) io.ReadCloser {
	content, err := os.Open(path)
	if err != nil {
		h.t.Fatalf("%+v", errors.Wrapf(err, "Unable to open file: %s", path))
	}
	return content
}

// GetTestFile reads a file from the testdata directory into memory.  src is
// relative to ./testdata.
func (h *Helper) GetTestFile(src string) io.ReadCloser {
	fullPath := filepath.Join(h.origWd, "testdata", src)
	return h.GetFile(fullPath)
}

// GetTestFileString reads a file from the testdata directory into memory.  src is
// relative to ./testdata.
func (h *Helper) GetTestFileString(src string) string {
	srcf := h.GetTestFile(src)
	defer srcf.Close()
	content, err := ioutil.ReadAll(srcf)
	if err != nil {
		h.t.Fatalf("%+v", err)
	}
	return string(content)
}

// TempCopy copies a temporary file from testdata into the temporary directory.
// dest is relative to the temp directory location, and src is relative to
// ./testdata.
func (h *Helper) TempCopy(dest, src string) {
	in := h.GetTestFile(src)
	defer in.Close()
	h.TempDir(filepath.Dir(dest))
	out, err := os.Create(filepath.Join(h.tempdir, dest))
	if err != nil {
		panic(err)
	}
	defer out.Close()
	io.Copy(out, in)
}

// TempDir adds a temporary directory for a run of testgo.
func (h *Helper) TempDir(path string) {
	h.makeTempdir()
	fullPath := filepath.Join(h.tempdir, path)
	if err := os.MkdirAll(fullPath, 0755); err != nil && !os.IsExist(err) {
		h.t.Fatalf("%+v", errors.Errorf("Unable to create temp directory: %s", fullPath))
	}
}

// Path returns the absolute pathname to file with the temporary
// directory.
func (h *Helper) Path(name string) string {
	if h.tempdir == "" {
		h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: path(%q) with no tempdir", name))
	}

	var joined string
	if name == "." {
		joined = h.tempdir
	} else {
		joined = filepath.Join(h.tempdir, name)
	}

	// Ensure it's the absolute, symlink-less path we're returning
	abs, err := filepath.EvalSymlinks(joined)
	if err != nil {
		h.t.Fatalf("%+v", errors.Wrapf(err, "internal testsuite error: could not get absolute path for dir(%q)", joined))
	}
	return abs
}

// MustExist fails if path does not exist.
func (h *Helper) MustExist(path string) {
	if err := h.ShouldExist(path); err != nil {
		h.t.Fatalf("%+v", err)
	}
}

// ShouldExist returns an error if path does not exist.
func (h *Helper) ShouldExist(path string) error {
	if !h.Exist(path) {
		return errors.Errorf("%s does not exist but should", path)
	}

	return nil
}

// Exist returns whether or not a path exists
func (h *Helper) Exist(path string) bool {
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return false
		}
		h.t.Fatalf("%+v", errors.Wrapf(err, "Error checking if path exists: %s", path))
	}

	return true
}

// MustNotExist fails if path exists.
func (h *Helper) MustNotExist(path string) {
	if err := h.ShouldNotExist(path); err != nil {
		h.t.Fatalf("%+v", err)
	}
}

// ShouldNotExist returns an error if path exists.
func (h *Helper) ShouldNotExist(path string) error {
	if h.Exist(path) {
		return errors.Errorf("%s exists but should not", path)
	}

	return nil
}

// Cleanup cleans up a test that runs testgo.
func (h *Helper) Cleanup() {
	if h.wd != "" {
		if err := os.Chdir(h.wd); err != nil {
			// We are unlikely to be able to continue.
			fmt.Fprintln(os.Stderr, "could not restore working directory, crashing:", err)
			os.Exit(2)
		}
	}
	// NOTE(mattn): It seems that sometimes git.exe is not dead
	// when cleanup() is called. But we do not know any way to wait for it.
	if runtime.GOOS == "windows" {
		mu.Lock()
		exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run()
		mu.Unlock()
	}
	for _, path := range h.temps {
		h.check(os.RemoveAll(path))
	}
	if h.tempdir != "" {
		h.check(os.RemoveAll(h.tempdir))
	}
}

// ReadManifest returns the manifest in the current directory.
func (h *Helper) ReadManifest() string {
	m := filepath.Join(h.pwd(), manifestName)
	h.MustExist(m)

	f, err := ioutil.ReadFile(m)
	h.Must(err)
	return string(f)
}

// ReadLock returns the lock in the current directory.
func (h *Helper) ReadLock() string {
	l := filepath.Join(h.pwd(), lockName)
	h.MustExist(l)

	f, err := ioutil.ReadFile(l)
	h.Must(err)
	return string(f)
}

// GetCommit treats repo as a path to a git repository and returns the current
// revision.
func (h *Helper) GetCommit(repo string) string {
	repoPath := h.Path("pkg/dep/sources/https---" + strings.Replace(repo, "/", "-", -1))
	cmd := exec.Command("git", "rev-parse", "HEAD")
	cmd.Dir = repoPath
	out, err := cmd.CombinedOutput()
	if err != nil {
		h.t.Fatalf("%+v", errors.Wrapf(err, "git commit failed: out -> %s", string(out)))
	}
	return strings.TrimSpace(string(out))
}
