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

import (
	"fmt"
	"go/ast"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/analysistest"
	"golang.org/x/tools/go/analysis/internal/checker"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/ast/inspector"
	"golang.org/x/tools/internal/testenv"
	"golang.org/x/tools/internal/testfiles"
	"golang.org/x/tools/txtar"
)

func TestApplyFixes(t *testing.T) {
	testenv.NeedsGoPackages(t)

	files := map[string]string{
		"rename/test.go": `package rename

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

// the end
`}
	want := `package rename

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

// the end
`

	testdata, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatal(err)
	}
	path := filepath.Join(testdata, "src/rename/test.go")
	checker.Fix = true
	checker.Run([]string{"file=" + path}, []*analysis.Analyzer{renameAnalyzer})

	contents, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}

	got := string(contents)
	if got != want {
		t.Errorf("contents of rewritten file\ngot: %s\nwant: %s", got, want)
	}

	defer cleanup()
}

var renameAnalyzer = &analysis.Analyzer{
	Name:     "rename",
	Requires: []*analysis.Analyzer{inspect.Analyzer},
	Run:      run,
	Doc:      "renames symbols named bar to baz",
}

var otherAnalyzer = &analysis.Analyzer{ // like analyzer but with a different Name.
	Name:     "other",
	Requires: []*analysis.Analyzer{inspect.Analyzer},
	Run:      run,
	Doc:      "renames symbols named bar to baz only in package 'other'",
}

func run(pass *analysis.Pass) (interface{}, error) {
	const (
		from      = "bar"
		to        = "baz"
		conflict  = "conflict"  // add conflicting edits to package conflict.
		duplicate = "duplicate" // add duplicate edits to package conflict.
		other     = "other"     // add conflicting edits to package other from different analyzers.
	)

	if pass.Analyzer.Name == other {
		if pass.Pkg.Name() != other {
			return nil, nil // only apply Analyzer other to packages named other
		}
	}

	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
	nodeFilter := []ast.Node{(*ast.Ident)(nil)}
	inspect.Preorder(nodeFilter, func(n ast.Node) {
		ident := n.(*ast.Ident)
		if ident.Name == from {
			msg := fmt.Sprintf("renaming %q to %q", from, to)
			edits := []analysis.TextEdit{
				{Pos: ident.Pos(), End: ident.End(), NewText: []byte(to)},
			}
			switch pass.Pkg.Name() {
			case conflict:
				edits = append(edits, []analysis.TextEdit{
					{Pos: ident.Pos() - 1, End: ident.End(), NewText: []byte(to)},
					{Pos: ident.Pos(), End: ident.End() - 1, NewText: []byte(to)},
					{Pos: ident.Pos(), End: ident.End(), NewText: []byte("lorem ipsum")},
				}...)
			case duplicate:
				edits = append(edits, edits...)
			case other:
				if pass.Analyzer.Name == other {
					edits[0].Pos = edits[0].Pos + 1 // shift by one to mismatch analyzer and other
				}
			}
			pass.Report(analysis.Diagnostic{
				Pos:            ident.Pos(),
				End:            ident.End(),
				Message:        msg,
				SuggestedFixes: []analysis.SuggestedFix{{Message: msg, TextEdits: edits}}})
		}
	})

	return nil, nil
}

func TestRunDespiteErrors(t *testing.T) {
	testenv.NeedsGoPackages(t)

	files := map[string]string{
		"rderr/test.go": `package rderr

// Foo deliberately has a type error
func Foo(s string) int {
	return s + 1
}
`}

	testdata, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatal(err)
	}
	path := filepath.Join(testdata, "src/rderr/test.go")

	// A no-op analyzer that should finish regardless of
	// parse or type errors in the code.
	noop := &analysis.Analyzer{
		Name:     "noop",
		Requires: []*analysis.Analyzer{inspect.Analyzer},
		Run: func(pass *analysis.Pass) (interface{}, error) {
			return nil, nil
		},
		RunDespiteErrors: true,
	}

	// A no-op analyzer that should finish regardless of
	// parse or type errors in the code.
	noopWithFact := &analysis.Analyzer{
		Name:     "noopfact",
		Requires: []*analysis.Analyzer{inspect.Analyzer},
		Run: func(pass *analysis.Pass) (interface{}, error) {
			return nil, nil
		},
		RunDespiteErrors: true,
		FactTypes:        []analysis.Fact{&EmptyFact{}},
	}

	for _, test := range []struct {
		name      string
		pattern   []string
		analyzers []*analysis.Analyzer
		code      int
	}{
		// parse/type errors
		{name: "skip-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
		// RunDespiteErrors allows a driver to run an Analyzer even after parse/type errors.
		//
		// The noop analyzer doesn't use facts, so the driver loads only the root
		// package from source. For the rest, it asks 'go list' for export data,
		// which fails because the compiler encounters the type error.  Since the
		// errors come from 'go list', the driver doesn't run the analyzer.
		{name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 1},
		// The noopfact analyzer does use facts, so the driver loads source for
		// all dependencies, does type checking itself, recognizes the error as a
		// type error, and runs the analyzer.
		{name: "despite-error-fact", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 0},
		// combination of parse/type errors and no errors
		{name: "despite-error-and-no-error", pattern: []string{"file=" + path, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
		// non-existing package error
		{name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
		{name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
		{name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
		// combination of type/parsing and different errors
		{name: "different-errors", pattern: []string{"file=" + path, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
		// non existing dir error
		{name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
		// no errors
		{name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 0},
	} {
		if test.name == "despite-error" && testenv.Go1Point() < 20 {
			// The behavior in the comment on the despite-error test only occurs for Go 1.20+.
			continue
		}
		if got := checker.Run(test.pattern, test.analyzers); got != test.code {
			t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code)
		}
	}

	defer cleanup()
}

type EmptyFact struct{}

func (f *EmptyFact) AFact() {}

func TestURL(t *testing.T) {
	// TestURL test that URLs get forwarded to diagnostics by internal/checker.
	testenv.NeedsGoPackages(t)

	files := map[string]string{
		"p/test.go": `package p // want "package name is p"`,
	}
	pkgname := &analysis.Analyzer{
		Name: "pkgname",
		Doc:  "trivial analyzer that reports package names",
		URL:  "https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker",
		Run: func(p *analysis.Pass) (interface{}, error) {
			for _, f := range p.Files {
				p.ReportRangef(f.Name, "package name is %s", f.Name.Name)
			}
			return nil, nil
		},
	}

	testdata, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()
	path := filepath.Join(testdata, "src/p/test.go")
	results := analysistest.Run(t, testdata, pkgname, "file="+path)

	var urls []string
	for _, r := range results {
		for _, d := range r.Diagnostics {
			urls = append(urls, d.URL)
		}
	}
	want := []string{"https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker"}
	if !reflect.DeepEqual(urls, want) {
		t.Errorf("Expected Diagnostics.URLs %v. got %v", want, urls)
	}
}

// TestPassReadFile exercises the Pass.ReadFile function.
func TestPassReadFile(t *testing.T) {
	cwd, _ := os.Getwd()

	const src = `
-- go.mod --
module example.com

-- p/file.go --
package p

-- p/ignored.go --
//go:build darwin && mips64

package p

hello from ignored

-- p/other.s --
hello from other
`

	// Expand archive into tmp tree.
	tmpdir := t.TempDir()
	if err := testfiles.ExtractTxtar(tmpdir, txtar.Parse([]byte(src))); err != nil {
		t.Fatal(err)
	}

	ran := false
	a := &analysis.Analyzer{
		Name:     "a",
		Requires: []*analysis.Analyzer{inspect.Analyzer},
		Doc:      "doc",
		Run: func(pass *analysis.Pass) (any, error) {
			if len(pass.OtherFiles)+len(pass.IgnoredFiles) == 0 {
				t.Errorf("OtherFiles and IgnoredFiles are empty")
				return nil, nil
			}

			for _, test := range []struct {
				filename string
				want     string // substring of file content or error message
			}{
				{
					pass.OtherFiles[0], // [other.s]
					"hello from other",
				},
				{
					pass.IgnoredFiles[0], // [ignored.go]
					"hello from ignored",
				},
				{
					"nonesuch",
					"nonesuch is not among OtherFiles, ", // etc
				},
				{
					filepath.Join(cwd, "checker_test.go"),
					"checker_test.go is not among OtherFiles, ", // etc
				},
			} {
				content, err := pass.ReadFile(test.filename)
				var got string
				if err != nil {
					got = err.Error()
				} else {
					got = string(content)
					if len(got) > 100 {
						got = got[:100] + "..."
					}
				}
				if !strings.Contains(got, test.want) {
					t.Errorf("Pass.ReadFile(%q) did not contain %q; got:\n%s",
						test.filename, test.want, got)
				}
			}
			ran = true
			return nil, nil
		},
	}

	analysistest.Run(t, tmpdir, a, "example.com/p")

	if !ran {
		t.Error("analyzer did not run")
	}
}
