// Copyright 2019 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 (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

	"golang.org/x/tools/txtar"
)

var workDir string

var (
	testwork     = flag.Bool("testwork", false, "preserve work directory")
	updateGolden = flag.Bool("u", false, "update expected text in test files instead of failing")
)

func TestMain(m *testing.M) {
	status := 1
	defer func() {
		if !*testwork && workDir != "" {
			os.RemoveAll(workDir)
		}
		os.Exit(status)
	}()

	flag.Parse()

	proxyDir, proxyURL, err := buildProxyDir()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}
	os.Setenv("GOPROXY", proxyURL)
	if *testwork {
		fmt.Fprintf(os.Stderr, "test proxy dir: %s\ntest proxy URL: %s\n", proxyDir, proxyURL)
	} else {
		defer os.RemoveAll(proxyDir)
	}

	cacheDir, err := ioutil.TempDir("", "gorelease_test-gocache")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}
	os.Setenv("GOPATH", cacheDir)
	if *testwork {
		fmt.Fprintf(os.Stderr, "test cache dir: %s\n", cacheDir)
	} else {
		defer func() {
			if err := exec.Command("go", "clean", "-modcache").Run(); err != nil {
				fmt.Fprintln(os.Stderr, err)
			}
			if err := os.RemoveAll(cacheDir); err != nil {
				fmt.Fprintln(os.Stderr, err)
			}
		}()
	}

	os.Setenv("GO111MODULE", "on")
	os.Setenv("GOSUMDB", "off")

	status = m.Run()
}

// test describes an individual test case, written as a .test file in the
// testdata directory.
//
// Each test is a txtar archive (see golang.org/x/tools/txtar). The comment
// section (before the first file) contains a sequence of key=value pairs
// (one per line) that configure the test.
//
// Most tests include a file named "want". The output of gorelease is compared
// against this file. If the -u flag is set, this file is replaced with the
// actual output of gorelease, and the test is written back to disk. This is
// useful for updating tests after cosmetic changes.
type test struct {
	txtar.Archive

	// testPath is the name of the .test file describing the test.
	testPath string

	// modPath (set with mod=...) is the path of the module being tested. Used
	// to retrieve files from the test proxy.
	modPath string

	// version (set with version=...) is the name of a version to check out
	// from the test proxy into the working directory. Some tests use this
	// instead of specifying files they need in the txtar archive.
	version string

	// baseVersion (set with base=...) is the value of the -base flag to pass
	// to gorelease.
	baseVersion string

	// releaseVersion (set with release=...) is the value of the -version flag
	// to pass to gorelease.
	releaseVersion string

	// dir (set with dir=...) is the directory where gorelease should be invoked.
	// If unset, gorelease is invoked in the directory where the txtar archive
	// is unpacked. This is useful for invoking gorelease in a subdirectory.
	dir string

	// wantError (set with error=...) is true if the test expects a hard error
	// (returned by runRelease).
	wantError bool

	// wantSuccess (set with success=...) is true if the test expects a report
	// to be returned without errors or diagnostics. True by default.
	wantSuccess bool

	// skip (set with skip=...) is non-empty if the test should be skipped.
	skip string

	// want is set to the contents of the file named "want" in the txtar archive.
	want []byte
}

// readTest reads and parses a .test file with the given name.
func readTest(testPath string) (*test, error) {
	arc, err := txtar.ParseFile(testPath)
	if err != nil {
		return nil, err
	}
	t := &test{
		Archive:     *arc,
		testPath:    testPath,
		wantSuccess: true,
	}

	for n, line := range bytes.Split(t.Comment, []byte("\n")) {
		lineNum := n + 1
		if i := bytes.IndexByte(line, '#'); i >= 0 {
			line = line[:i]
		}
		line = bytes.TrimSpace(line)
		if len(line) == 0 {
			continue
		}

		var key, value string
		if i := bytes.IndexByte(line, '='); i < 0 {
			return nil, fmt.Errorf("%s:%d: no '=' found", testPath, lineNum)
		} else {
			key = strings.TrimSpace(string(line[:i]))
			value = strings.TrimSpace(string(line[i+1:]))
		}
		switch key {
		case "mod":
			t.modPath = value
		case "version":
			t.version = value
		case "base":
			t.baseVersion = value
		case "release":
			t.releaseVersion = value
		case "dir":
			t.dir = value
		case "skip":
			t.skip = value
		case "success":
			t.wantSuccess, err = strconv.ParseBool(value)
			if err != nil {
				return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
			}
		case "error":
			t.wantError, err = strconv.ParseBool(value)
			if err != nil {
				return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
			}
		default:
			return nil, fmt.Errorf("%s:%d: unknown key: %q", testPath, lineNum, key)
		}
	}
	if t.modPath == "" && (t.version != "" || (t.baseVersion != "" && t.baseVersion != "none")) {
		return nil, fmt.Errorf("%s: version or base was set but mod was not set", testPath)
	}

	haveFiles := false
	for _, f := range t.Files {
		if f.Name == "want" {
			t.want = bytes.TrimSpace(f.Data)
			continue
		}
		haveFiles = true
	}

	if haveFiles && t.version != "" {
		return nil, fmt.Errorf("%s: version is set but files are present", testPath)
	}

	return t, nil
}

// updateTest replaces the contents of the file named "want" within a test's
// txtar archive, then formats and writes the test file.
func updateTest(t *test, want []byte) error {
	var wantFile *txtar.File
	for i := range t.Files {
		if t.Files[i].Name == "want" {
			wantFile = &t.Files[i]
			break
		}
	}
	if wantFile == nil {
		t.Files = append(t.Files, txtar.File{Name: "want"})
		wantFile = &t.Files[len(t.Files)-1]
	}

	wantFile.Data = want
	data := txtar.Format(&t.Archive)
	return ioutil.WriteFile(t.testPath, data, 0666)
}

func TestRelease(t *testing.T) {
	testPaths, err := filepath.Glob(filepath.FromSlash("testdata/*/*.test"))
	if err != nil {
		t.Fatal(err)
	}
	if len(testPaths) == 0 {
		t.Fatal("no .test files found in testdata directory")
	}

	for _, testPath := range testPaths {
		testPath := testPath
		testName := strings.TrimSuffix(strings.TrimPrefix(filepath.ToSlash(testPath), "testdata/"), ".test")
		t.Run(testName, func(t *testing.T) {
			t.Parallel()

			test, err := readTest(testPath)
			if err != nil {
				t.Fatal(err)
			}

			if test.skip != "" {
				t.Skip(test.skip)
			}

			// Extract the files in the release version. They may be part of the
			// test archive or in testdata/mod.
			testDir, err := ioutil.TempDir("", "")
			if err != nil {
				t.Fatal(err)
			}
			if *testwork {
				fmt.Fprintf(os.Stderr, "test dir: %s\n", testDir)
			} else {
				defer os.RemoveAll(testDir)
			}

			var arc *txtar.Archive
			if test.version != "" {
				arcBase := fmt.Sprintf("%s_%s.txt", strings.ReplaceAll(test.modPath, "/", "_"), test.version)
				arcPath := filepath.Join("testdata/mod", arcBase)
				var err error
				arc, err = txtar.ParseFile(arcPath)
				if err != nil {
					t.Fatal(err)
				}
			} else {
				arc = &test.Archive
			}
			if err := extractTxtar(testDir, arc); err != nil {
				t.Fatal(err)
			}

			// Generate the report and compare it against the expected text.
			var args []string
			if test.baseVersion != "" {
				args = append(args, "-base="+test.baseVersion)
			}
			if test.releaseVersion != "" {
				args = append(args, "-version="+test.releaseVersion)
			}
			buf := &bytes.Buffer{}
			releaseDir := filepath.Join(testDir, test.dir)
			success, err := runRelease(buf, releaseDir, args)
			if err != nil {
				if !test.wantError {
					t.Fatalf("unexpected error: %v", err)
				}
				if errMsg := []byte(err.Error()); !bytes.Equal(errMsg, bytes.TrimSpace(test.want)) {
					if *updateGolden {
						if err := updateTest(test, errMsg); err != nil {
							t.Fatal(err)
						}
					} else {
						t.Fatalf("got error: %s; want error: %s", errMsg, test.want)
					}
				}
				return
			}
			if test.wantError {
				t.Fatalf("got success; want error %s", test.want)
			}

			got := bytes.TrimSpace(buf.Bytes())
			if filepath.Separator != '/' {
				got = bytes.ReplaceAll(got, []byte{filepath.Separator}, []byte{'/'})
			}
			if !bytes.Equal(got, test.want) {
				if *updateGolden {
					if err := updateTest(test, got); err != nil {
						t.Fatal(err)
					}
				} else {
					t.Fatalf("got:\n%s\n\nwant:\n%s", got, test.want)
				}
			}
			if success != test.wantSuccess {
				t.Fatalf("got success: %v; want success %v", success, test.wantSuccess)
			}
		})
	}
}
