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

import (
	"encoding/gob"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"os"
	"reflect"
	"strings"
	"testing"

	"golang.org/x/tools/go/analysis/analysistest"
	"golang.org/x/tools/go/packages"

	"golang.org/x/tools/internal/facts"
	"golang.org/x/tools/internal/testenv"
	"golang.org/x/tools/internal/typesinternal"
)

type myFact struct {
	S string
}

func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) }
func (f *myFact) AFact()         {}

func init() {
	gob.Register(new(myFact))
}

func TestEncodeDecode(t *testing.T) {
	tests := []struct {
		name     string
		files    map[string]string
		plookups []pkgLookups // see testEncodeDecode for details
	}{
		{
			name: "loading-order",
			// c -> b -> a, a2
			// c does not directly depend on a, but it indirectly uses a.T.
			//
			// Package a2 is never loaded directly so it is incomplete.
			//
			// We use only types in this example because we rely on
			// types.Eval to resolve the lookup expressions, and it only
			// works for types. This is a definite gap in the typechecker API.
			files: map[string]string{
				"a/a.go":  `package a; type A int; type T int`,
				"a2/a.go": `package a2; type A2 int; type Unneeded int`,
				"b/b.go":  `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`,
				"c/c.go":  `package c; import "b"; type C []b.B`,
			},
			// In the following table, we analyze packages (a, b, c) in order,
			// look up various objects accessible within each package,
			// and see if they have a fact.  The "analysis" exports a fact
			// for every object at package level.
			//
			// Note: Loop iterations are not independent test cases;
			// order matters, as we populate factmap.
			plookups: []pkgLookups{
				{"a", []lookup{
					{"A", "myFact(a.A)"},
				}},
				{"b", []lookup{
					{"a.A", "myFact(a.A)"},
					{"a.T", "myFact(a.T)"},
					{"B", "myFact(b.B)"},
					{"F", "myFact(b.F)"},
					{"F(nil)()", "myFact(a.T)"}, // (result type of b.F)
				}},
				{"c", []lookup{
					{"b.B", "myFact(b.B)"},
					{"b.F", "myFact(b.F)"},
					{"b.F(nil)()", "myFact(a.T)"},
					{"C", "myFact(c.C)"},
					{"C{}[0]", "myFact(b.B)"},
					{"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2)
				}},
			},
		},
		{
			name: "underlying",
			// c->b->a
			// c does not import a directly or use any of its types, but it does use
			// the types within a indirectly. c.q has the type a.a so package a should
			// be included by importMap.
			files: map[string]string{
				"a/a.go": `package a; type a int; type T *a`,
				"b/b.go": `package b; import "a"; type B a.T`,
				"c/c.go": `package c; import "b"; type C b.B; var q = *C(nil)`,
			},
			plookups: []pkgLookups{
				{"a", []lookup{
					{"a", "myFact(a.a)"},
					{"T", "myFact(a.T)"},
				}},
				{"b", []lookup{
					{"B", "myFact(b.B)"},
					{"B(nil)", "myFact(b.B)"},
					{"*(B(nil))", "myFact(a.a)"},
				}},
				{"c", []lookup{
					{"C", "myFact(c.C)"},
					{"C(nil)", "myFact(c.C)"},
					{"*C(nil)", "myFact(a.a)"},
					{"q", "myFact(a.a)"},
				}},
			},
		},
		{
			name: "methods",
			// c->b->a
			// c does not import a directly or use any of its types, but it does use
			// the types within a indirectly via a method.
			files: map[string]string{
				"a/a.go": `package a; type T int`,
				"b/b.go": `package b; import "a"; type B struct{}; func (_ B) M() a.T { return 0 }`,
				"c/c.go": `package c; import "b"; var C b.B`,
			},
			plookups: []pkgLookups{
				{"a", []lookup{
					{"T", "myFact(a.T)"},
				}},
				{"b", []lookup{
					{"B{}", "myFact(b.B)"},
					{"B{}.M()", "myFact(a.T)"},
				}},
				{"c", []lookup{
					{"C", "myFact(b.B)"},
					{"C.M()", "myFact(a.T)"},
				}},
			},
		},
		{
			name: "globals",
			files: map[string]string{
				"a/a.go": `package a;
				type T1 int
				type T2 int
				type T3 int
				type T4 int
				type T5 int
				type K int; type V string
				`,
				"b/b.go": `package b
				import "a"
				var (
					G1 []a.T1
					G2 [7]a.T2
					G3 chan a.T3
					G4 *a.T4
					G5 struct{ F a.T5 }
					G6 map[a.K]a.V
				)
				`,
				"c/c.go": `package c; import "b";
				var (
					v1 = b.G1
					v2 = b.G2
					v3 = b.G3
					v4 = b.G4
					v5 = b.G5
					v6 = b.G6
				)
				`,
			},
			plookups: []pkgLookups{
				{"a", []lookup{}},
				{"b", []lookup{}},
				{"c", []lookup{
					{"v1[0]", "myFact(a.T1)"},
					{"v2[0]", "myFact(a.T2)"},
					{"<-v3", "myFact(a.T3)"},
					{"*v4", "myFact(a.T4)"},
					{"v5.F", "myFact(a.T5)"},
					{"v6[0]", "myFact(a.V)"},
				}},
			},
		},
		{
			name: "typeparams",
			files: map[string]string{
				"a/a.go": `package a
				  type T1 int
				  type T2 int
				  type T3 interface{Foo()}
				  type T4 int
				  type T5 int
				  type T6 interface{Foo()}
				`,
				"b/b.go": `package b
				  import "a"
				  type N1[T a.T1|int8] func() T
				  type N2[T any] struct{ F T }
				  type N3[T a.T3] func() T
				  type N4[T a.T4|int8] func() T
				  type N5[T interface{Bar() a.T5} ] func() T

				  type t5 struct{}; func (t5) Bar() a.T5 { return 0 }

				  var G1 N1[a.T1]
				  var G2 func() N2[a.T2]
				  var G3 N3[a.T3]
				  var G4 N4[a.T4]
				  var G5 N5[t5]

				  func F6[T a.T6]() T { var x T; return x }
				  `,
				"c/c.go": `package c; import "b";
				  var (
					  v1 = b.G1
					  v2 = b.G2
					  v3 = b.G3
					  v4 = b.G4
					  v5 = b.G5
					  v6 = b.F6[t6]
				  )

				  type t6 struct{}; func (t6) Foo() {}
				`,
			},
			plookups: []pkgLookups{
				{"a", []lookup{}},
				{"b", []lookup{}},
				{"c", []lookup{
					{"v1", "myFact(b.N1)"},
					{"v1()", "myFact(a.T1)"},
					{"v2()", "myFact(b.N2)"},
					{"v2().F", "myFact(a.T2)"},
					{"v3", "myFact(b.N3)"},
					{"v4", "myFact(b.N4)"},
					{"v4()", "myFact(a.T4)"},
					{"v5", "myFact(b.N5)"},
					{"v5()", "myFact(b.t5)"},
					{"v6()", "myFact(c.t6)"},
				}},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			testEncodeDecode(t, test.files, test.plookups)
		})
	}
}

func TestEncodeDecodeAliases(t *testing.T) {
	testenv.NeedsGo1Point(t, 24)

	files := map[string]string{
		"a/a.go": `package a
				  type A = int
				`,
		"b/b.go": `package b
				  import "a"
				  type B = a.A
				`,
		"c/c.go": `package c
				  import "b";
				  type N1[T int|~string] = struct{}

				  var V1 = N1[b.B]{}
				`,
	}
	plookups := []pkgLookups{
		{"a", []lookup{}},
		{"b", []lookup{}},
		// fake objexpr for RHS of V1's type arg (see customFind hack)
		{"c", []lookup{{"c.V1->c.N1->b.B->a.A", "myFact(a.A)"}}},
	}
	testEncodeDecode(t, files, plookups)
}

type lookup struct {
	objexpr string // expression whose type is a named type
	want    string // printed form of fact associated with that type (or "no fact")
}

type pkgLookups struct {
	path    string
	lookups []lookup
}

// testEncodeDecode tests fact encoding and decoding and simulates how package facts
// are passed during analysis. It operates on a group of Go file contents. Then
// for each <package, []lookup> in tests it does the following:
//  1. loads and type checks the package,
//  2. calls (*facts.Decoder).Decode to load the facts exported by its imports,
//  3. exports a myFact Fact for all of package level objects,
//  4. For each lookup for the current package:
//     4.a) lookup the types.Object for a Go source expression in the current package
//     (or confirms one is not expected want=="no object"),
//     4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"),
//     4.c) compares the content of the Fact to want.
//  5. encodes the Facts of the package.
//
// Note: tests are not independent test cases; order matters (as does a package being
// skipped). It changes what Facts can be imported.
//
// Failures are reported on t.
func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) {
	dir, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()

	// factmap represents the passing of encoded facts from one
	// package to another. In practice one would use the file system.
	factmap := make(map[string][]byte)
	read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil }

	// Analyze packages in order, look up various objects accessible within
	// each package, and see if they have a fact.  The "analysis" exports a
	// fact for every object at package level.
	//
	// Note: Loop iterations are not independent test cases;
	// order matters, as we populate factmap.
	for _, test := range tests {
		// load package
		pkg, err := load(t, dir, test.path)
		if err != nil {
			t.Fatal(err)
		}

		// decode
		facts, err := facts.NewDecoder(pkg).Decode(read)
		if err != nil {
			t.Fatalf("Decode failed: %v", err)
		}
		t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts

		// export
		// (one fact for each package-level object)
		for _, name := range pkg.Scope().Names() {
			obj := pkg.Scope().Lookup(name)
			fact := &myFact{obj.Pkg().Name() + "." + obj.Name()}
			facts.ExportObjectFact(obj, fact)
		}
		t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts

		// import
		// (after export, because an analyzer may import its own facts)
		for _, lookup := range test.lookups {
			fact := new(myFact)
			var got string
			if obj := find(pkg, lookup.objexpr); obj == nil {
				got = "no object"
			} else if facts.ImportObjectFact(obj, fact) {
				got = fact.String()
			} else {
				got = "no fact"
			}
			if got != lookup.want {
				t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s",
					pkg.Path(), lookup.objexpr, fact, got, lookup.want)
			}
		}

		// encode
		factmap[pkg.Path()] = facts.Encode()
	}
}

// customFind allows for overriding how an object is looked up
// by find. This is necessary for objects that are accessible through
// the API but are not the type of any expression we can pass to types.CheckExpr.
var customFind = map[string]func(p *types.Package) types.Object{
	"c.V1->c.N1->b.B->a.A": func(p *types.Package) types.Object {
		cV1 := p.Scope().Lookup("V1")
		cN1 := cV1.Type().(*types.Alias)
		aT1 := cN1.TypeArgs().At(0).(*types.Alias)
		zZ1 := aT1.Rhs().(*types.Alias)
		return zZ1.Obj()
	},
}

func find(p *types.Package, expr string) types.Object {
	// types.Eval only allows us to compute a TypeName object for an expression.
	// TODO(adonovan): support other expressions that denote an object:
	// - an identifier (or qualified ident) for a func, const, or var
	// - new(T).f for a field or method
	// I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677.
	// If that becomes available, use it.
	if f := customFind[expr]; f != nil {
		return f(p)
	}
	// Choose an arbitrary position within the (single-file) package
	// so that we are within the scope of its import declarations.
	somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos()
	tv, err := types.Eval(token.NewFileSet(), p, somepos, expr)
	if err != nil {
		return nil
	}
	if n, ok := tv.Type.(typesinternal.NamedOrAlias); ok {
		return n.Obj()
	}
	return nil
}

func load(t *testing.T, dir string, path string) (*types.Package, error) {
	cfg := &packages.Config{
		Mode: packages.LoadSyntax,
		Dir:  dir,
		Env:  append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
	}
	testenv.NeedsGoPackagesEnv(t, cfg.Env)
	pkgs, err := packages.Load(cfg, path)
	if err != nil {
		return nil, err
	}
	if packages.PrintErrors(pkgs) > 0 {
		return nil, fmt.Errorf("packages had errors")
	}
	if len(pkgs) == 0 {
		return nil, fmt.Errorf("no package matched %s", path)
	}
	return pkgs[0].Types, nil
}

type otherFact struct {
	S string
}

func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) }
func (f *otherFact) AFact()         {}

func TestFactFilter(t *testing.T) {
	files := map[string]string{
		"a/a.go": `package a; type A int`,
	}
	dir, cleanup, err := analysistest.WriteFiles(files)
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()

	pkg, err := load(t, dir, "a")
	if err != nil {
		t.Fatal(err)
	}

	obj := pkg.Scope().Lookup("A")
	s, err := facts.NewDecoder(pkg).Decode(func(pkgPath string) ([]byte, error) { return nil, nil })
	if err != nil {
		t.Fatal(err)
	}
	s.ExportObjectFact(obj, &myFact{"good object fact"})
	s.ExportPackageFact(&myFact{"good package fact"})
	s.ExportObjectFact(obj, &otherFact{"bad object fact"})
	s.ExportPackageFact(&otherFact{"bad package fact"})

	filter := map[reflect.Type]bool{
		reflect.TypeFor[*myFact](): true,
	}

	pkgFacts := s.AllPackageFacts(filter)
	wantPkgFacts := `[{package a ("a") myFact(good package fact)}]`
	if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts {
		t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts)
	}

	objFacts := s.AllObjectFacts(filter)
	wantObjFacts := "[{type a.A int myFact(good object fact)}]"
	if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts {
		t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts)
	}
}

// TestMalformed checks that facts can be encoded and decoded *despite*
// types.Config.Check returning an error. Importing facts is expected to
// happen when Analyzers have RunDespiteErrors set to true. So this
// needs to robust, e.g. no infinite loops.
func TestMalformed(t *testing.T) {
	var findPkg func(*types.Package, string) *types.Package
	findPkg = func(p *types.Package, name string) *types.Package {
		if p.Name() == name {
			return p
		}
		for _, o := range p.Imports() {
			if f := findPkg(o, name); f != nil {
				return f
			}
		}
		return nil
	}

	type pkgTest struct {
		content string
		err     string            // if non-empty, expected substring of err.Error() from conf.Check().
		wants   map[string]string // package path to expected name
	}
	tests := []struct {
		name string
		pkgs []pkgTest
	}{
		{
			name: "initialization-cycle",
			pkgs: []pkgTest{
				// Notation: myFact(a.[N]) means: package a has members {N}.
				{
					content: `package a; type N[T any] struct { F *N[N[T]] }`,
					err:     "instantiation cycle:",
					wants:   map[string]string{"a": "myFact(a.[N])", "b": "no package", "c": "no package"},
				},
				{
					content: `package b; import "a"; type B a.N[int]`,
					wants:   map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "no package"},
				},
				{
					content: `package c; import "b"; var C b.B`,
					wants:   map[string]string{"a": "no fact", "b": "myFact(b.[B])", "c": "myFact(c.[C])"},
					// package fact myFact(a.[N]) not reexported
				},
			},
		},
	}

	for i := range tests {
		test := tests[i]
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			// setup for test wide variables.
			packages := make(map[string]*types.Package)
			conf := types.Config{
				Importer: closure(packages),
				Error:    func(err error) {}, // do not stop on first type checking error
			}
			fset := token.NewFileSet()
			factmap := make(map[string][]byte)
			read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil }

			// Processes the pkgs in order. For package, export a package fact,
			// and use this fact to verify which package facts are reachable via Decode.
			// We allow for packages to have type checking errors.
			for i, pkgTest := range test.pkgs {
				// parse
				f, err := parser.ParseFile(fset, fmt.Sprintf("%d.go", i), pkgTest.content, 0)
				if err != nil {
					t.Fatal(err)
				}

				// typecheck
				pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, nil)
				var got string
				if err != nil {
					got = err.Error()
				}
				if !strings.Contains(got, pkgTest.err) {
					t.Fatalf("%s: type checking error %q did not match pattern %q", pkg.Path(), err.Error(), pkgTest.err)
				}
				packages[pkg.Path()] = pkg

				// decode facts
				facts, err := facts.NewDecoder(pkg).Decode(read)
				if err != nil {
					t.Fatalf("Decode failed: %v", err)
				}

				// export facts
				fact := &myFact{fmt.Sprintf("%s.%s", pkg.Name(), pkg.Scope().Names())}
				facts.ExportPackageFact(fact)

				// import facts
				for other, want := range pkgTest.wants {
					fact := new(myFact)
					var got string
					if found := findPkg(pkg, other); found == nil {
						got = "no package"
					} else if facts.ImportPackageFact(found, fact) {
						got = fact.String()
					} else {
						got = "no fact"
					}
					if got != want {
						t.Errorf("in %s, ImportPackageFact(%s, %T) = %s, want %s",
							pkg.Path(), other, fact, got, want)
					}
				}

				// encode facts
				factmap[pkg.Path()] = facts.Encode()
			}
		})
	}
}

type closure map[string]*types.Package

func (c closure) Import(path string) (*types.Package, error) { return c[path], nil }
