// Copyright 2022 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 checker_test

import (
	"flag"
	"fmt"
	"go/token"
	"log"
	"os"
	"os/exec"
	"path"
	"regexp"
	"runtime"
	"strings"
	"testing"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/analysistest"
	"golang.org/x/tools/go/analysis/multichecker"
	"golang.org/x/tools/internal/testenv"
)

// These are the analyzers available to the multichecker.
// (Tests may add more in init functions as needed.)
var candidates = map[string]*analysis.Analyzer{
	renameAnalyzer.Name: renameAnalyzer,
	otherAnalyzer.Name:  otherAnalyzer,
}

func TestMain(m *testing.M) {
	// If the ANALYZERS=a,..,z environment is set, then this
	// process should behave like a multichecker with the
	// named analyzers.
	if s, ok := os.LookupEnv("ANALYZERS"); ok {
		var analyzers []*analysis.Analyzer
		for _, name := range strings.Split(s, ",") {
			a := candidates[name]
			if a == nil {
				log.Fatalf("no such analyzer: %q", name)
			}
			analyzers = append(analyzers, a)
		}
		multichecker.Main(analyzers...)
		panic("unreachable")
	}

	// ordinary test
	flag.Parse()
	os.Exit(m.Run())
}

const (
	exitCodeSuccess     = 0 // success (no diagnostics)
	exitCodeFailed      = 1 // analysis failed to run
	exitCodeDiagnostics = 3 // diagnostics were reported
)

// fix runs a multichecker subprocess with -fix in the specified
// directory, applying the comma-separated list of named analyzers to
// the packages matching the patterns. It returns the CombinedOutput.
func fix(t *testing.T, dir, analyzers string, wantExit int, patterns ...string) string {
	testenv.NeedsGoPackages(t)
	testenv.NeedsExec(t)
	testenv.NeedsTool(t, "go")

	cmd := exec.Command(os.Args[0], "-fix")
	cmd.Args = append(cmd.Args, patterns...)
	cmd.Env = append(os.Environ(),
		"ANALYZERS="+analyzers,
		"GOPATH="+dir,
		"GO111MODULE=off",
		"GOPROXY=off")

	clean := func(s string) string {
		return strings.ReplaceAll(s, os.TempDir(), "os.TempDir/")
	}
	outBytes, err := cmd.CombinedOutput()
	out := clean(string(outBytes))
	t.Logf("$ %s\n%s", clean(fmt.Sprint(cmd)), out)
	if err, ok := err.(*exec.ExitError); !ok {
		t.Fatalf("failed to execute multichecker: %v", err)
	} else if err.ExitCode() != wantExit {
		// plan9 ExitCode() currently only returns 0 for success or 1 for failure
		if !(runtime.GOOS == "plan9" && wantExit != exitCodeSuccess && err.ExitCode() != exitCodeSuccess) {
			t.Errorf("exit code was %d, want %d", err.ExitCode(), wantExit)
		}
	}
	return out
}

// TestFixes ensures that checker.Run applies fixes correctly.
// This test fork/execs the main function above.
func TestFixes(t *testing.T) {
	files := map[string]string{
		"rename/foo.go": `package rename

func Foo() {
	bar := 12
	_ = bar
}

// the end
`,
		"rename/intestfile_test.go": `package rename

func InTestFile() {
	bar := 13
	_ = bar
}

// the end
`,
		"rename/foo_test.go": `package rename_test

func Foo() {
	bar := 14
	_ = bar
}

// the end
`,
		"duplicate/dup.go": `package duplicate

func Foo() {
	bar := 14
	_ = bar
}

// the end
`,
	}
	fixed := map[string]string{
		"rename/foo.go": `package rename

func Foo() {
	baz := 12
	_ = baz
}

// the end
`,
		"rename/intestfile_test.go": `package rename

func InTestFile() {
	baz := 13
	_ = baz
}

// the end
`,
		"rename/foo_test.go": `package rename_test

func Foo() {
	baz := 14
	_ = baz
}

// the end
`,
		"duplicate/dup.go": `package duplicate

func Foo() {
	baz := 14
	_ = baz
}

// the end
`,
	}
	dir, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatalf("Creating test files failed with %s", err)
	}
	defer cleanup()

	fix(t, dir, "rename,other", exitCodeDiagnostics, "rename", "duplicate")

	for name, want := range fixed {
		path := path.Join(dir, "src", name)
		contents, err := os.ReadFile(path)
		if err != nil {
			t.Errorf("error reading %s: %v", path, err)
		}
		if got := string(contents); got != want {
			t.Errorf("contents of %s file did not match expectations. got=%s, want=%s", path, got, want)
		}
	}
}

// TestConflict ensures that checker.Run detects conflicts correctly.
// This test fork/execs the main function above.
func TestConflict(t *testing.T) {
	files := map[string]string{
		"conflict/foo.go": `package conflict

func Foo() {
	bar := 12
	_ = bar
}

// the end
`,
	}
	dir, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatalf("Creating test files failed with %s", err)
	}
	defer cleanup()

	out := fix(t, dir, "rename,other", exitCodeFailed, "conflict")

	pattern := `conflicting edits from rename and rename on .*foo.go`
	matched, err := regexp.MatchString(pattern, out)
	if err != nil {
		t.Errorf("error matching pattern %s: %v", pattern, err)
	} else if !matched {
		t.Errorf("output did not match pattern: %s", pattern)
	}

	// No files updated
	for name, want := range files {
		path := path.Join(dir, "src", name)
		contents, err := os.ReadFile(path)
		if err != nil {
			t.Errorf("error reading %s: %v", path, err)
		}
		if got := string(contents); got != want {
			t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want)
		}
	}
}

// TestOther ensures that checker.Run reports conflicts from
// distinct actions correctly.
// This test fork/execs the main function above.
func TestOther(t *testing.T) {
	files := map[string]string{
		"other/foo.go": `package other

func Foo() {
	bar := 12
	_ = bar
}

// the end
`,
	}
	dir, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatalf("Creating test files failed with %s", err)
	}
	defer cleanup()

	out := fix(t, dir, "rename,other", exitCodeFailed, "other")

	pattern := `.*conflicting edits from other and rename on .*foo.go`
	matched, err := regexp.MatchString(pattern, out)
	if err != nil {
		t.Errorf("error matching pattern %s: %v", pattern, err)
	} else if !matched {
		t.Errorf("output did not match pattern: %s", pattern)
	}

	// No files updated
	for name, want := range files {
		path := path.Join(dir, "src", name)
		contents, err := os.ReadFile(path)
		if err != nil {
			t.Errorf("error reading %s: %v", path, err)
		}
		if got := string(contents); got != want {
			t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want)
		}
	}
}

// TestNoEnd tests that a missing SuggestedFix.End position is
// correctly interpreted as if equal to SuggestedFix.Pos (see issue #64199).
func TestNoEnd(t *testing.T) {
	files := map[string]string{
		"a/a.go": "package a\n\nfunc F() {}",
	}
	dir, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatalf("Creating test files failed with %s", err)
	}
	defer cleanup()

	fix(t, dir, "noend", exitCodeDiagnostics, "a")

	got, err := os.ReadFile(path.Join(dir, "src/a/a.go"))
	if err != nil {
		t.Fatal(err)
	}
	const want = "package a\n\n/*hello*/\nfunc F() {}\n"
	if string(got) != want {
		t.Errorf("new file contents were <<%s>>, want <<%s>>", got, want)
	}
}

func init() {
	candidates["noend"] = &analysis.Analyzer{
		Name: "noend",
		Doc:  "inserts /*hello*/ before first decl",
		Run: func(pass *analysis.Pass) (any, error) {
			decl := pass.Files[0].Decls[0]
			pass.Report(analysis.Diagnostic{
				Pos:     decl.Pos(),
				End:     token.NoPos,
				Message: "say hello",
				SuggestedFixes: []analysis.SuggestedFix{{
					Message: "say hello",
					TextEdits: []analysis.TextEdit{
						{
							Pos:     decl.Pos(),
							End:     token.NoPos,
							NewText: []byte("/*hello*/"),
						},
					},
				}},
			})
			return nil, nil
		},
	}
}
