// Copyright 2023 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 typerefs_test

import (
	"context"
	"fmt"
	"go/token"
	"sort"
	"testing"

	"github.com/google/go-cmp/cmp"
	"golang.org/x/tools/gopls/internal/lsp/cache"
	"golang.org/x/tools/gopls/internal/lsp/source"
	"golang.org/x/tools/gopls/internal/lsp/source/typerefs"
	"golang.org/x/tools/gopls/internal/span"
	"golang.org/x/tools/internal/testenv"
)

// TestRefs checks that the analysis reports, for each exported member
// of the test package ("p"), its correct dependencies on exported
// members of its direct imports (e.g. "ext").
func TestRefs(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		label     string
		srcs      []string            // source for the local package; package name must be p
		imports   map[string]string   // for simplicity: importPath -> pkgID/pkgName (we set pkgName == pkgID); 'ext' is always available.
		want      map[string][]string // decl name -> id.<decl name>
		go118     bool                // test uses generics
		allowErrs bool                // whether we expect parsing errors
	}{
		{
			label: "empty package",
			want:  map[string][]string{},
		},
		{
			label: "fields",
			srcs: []string{`
package p

import "ext"

type A struct{ b B }
type B func(c C) (d D)
type C ext.C
type D ext.D

// Should not be referenced by field names.
type b ext.B_
type c int.C_
type d ext.D_
`},
			want: map[string][]string{
				"A": {"ext.C", "ext.D"},
				"B": {"ext.C", "ext.D"},
				"C": {"ext.C"},
				"D": {"ext.D"},
			},
		},
		{
			label: "embedding",
			srcs: []string{`
package p

import "ext"

type A struct{
	B
	_ struct {
		C
	}
	D
}
type B ext.B
type C ext.C
type D interface{
	B
}
`},
			want: map[string][]string{
				"A": {"ext.B", "ext.C"},
				"B": {"ext.B"},
				"C": {"ext.C"},
				"D": {"ext.B"},
			},
		},
		{
			label: "constraint embedding",
			srcs: []string{`
package p

import "ext"

type A interface{
	int | B | ~C
	struct{D}
}

type B ext.B
type C ext.C
type D ext.D
`},
			want: map[string][]string{
				"A": {"ext.B", "ext.C", "ext.D"},
				"B": {"ext.B"},
				"C": {"ext.C"},
				"D": {"ext.D"},
			},
			go118: true,
		},
		{
			label: "funcs",
			srcs: []string{`
package p

import "ext"

type A ext.A
type B ext.B
const C B = 2
func F(A) B {
	return C
}
var V = F(W)
var W A
`},
			want: map[string][]string{
				"A": {"ext.A"},
				"B": {"ext.B"},
				"C": {"ext.B"},
				"F": {"ext.A", "ext.B"},
				"V": {
					"ext.A", // via F
					"ext.B", // via W: can't be eliminated: F could be builtin or generic
				},
				"W": {"ext.A"},
			},
		},
		{
			label: "methods",
			srcs: []string{`package p

import "ext"

type A ext.A
type B ext.B
`, `package p

func (A) M(B)
func (*B) M(A)
`},
			want: map[string][]string{
				"A": {"ext.A", "ext.B"},
				"B": {"ext.A", "ext.B"},
			},
		},
		{
			label: "initializers",
			srcs: []string{`
package p

import "ext"

var A b = C // type does not depend on C
type b ext.B
var C = d // type does depend on D
var d b

var e = d + a

var F = func() B { return E }

var G = struct{
	A b
	_ [unsafe.Sizeof(ext.V)]int // array size + Sizeof creates edge to a var
	_ [unsafe.Sizeof(G)]int // creates a self edge; doesn't affect output though
}{}

var H = (D + A + C*C)

var I = (A+C).F
`},
			want: map[string][]string{
				"A": {"ext.B"},
				"C": {"ext.B"},          // via d
				"G": {"ext.B", "ext.V"}, // via b,C
				"H": {"ext.B"},          // via d,A,C
				"I": {"ext.B"},
			},
		},
		{
			label: "builtins",
			srcs: []string{`package p

import "ext"

var A = new(b)
type b struct{ ext.B }

type C chan d
type d ext.D

type S []ext.S
type t ext.T
var U = append(([]*S)(nil), new(t))

type X map[k]v
type k ext.K
type v ext.V

var Z = make(map[k]A)

// close, delete, and panic cannot occur outside of statements
`},
			want: map[string][]string{
				"A": {"ext.B"},
				"C": {"ext.D"},
				"S": {"ext.S"},
				"U": {"ext.S", "ext.T"}, // ext.T edge could be eliminated
				"X": {"ext.K", "ext.V"},
				"Z": {"ext.B", "ext.K"},
			},
		},
		{
			label: "builtin shadowing",
			srcs: []string{`package p

import "ext"

var A = new(ext.B)
func new() c
type c ext.C
`},
			want: map[string][]string{
				"A": {"ext.B", "ext.C"},
			},
		},
		{
			label: "named forwarding",
			srcs: []string{`package p

import "ext"

type A B
type B c
type c ext.C
`},
			want: map[string][]string{
				"A": {"ext.C"},
				"B": {"ext.C"},
			},
		},
		{
			label: "aliases",
			srcs: []string{`package p

import "ext"

type A = B
type B = C
type C = ext.C
`},
			want: map[string][]string{
				"A": {"ext.C"},
				"B": {"ext.C"},
				"C": {"ext.C"},
			},
		},
		{
			label: "array length",
			srcs: []string{`package p

import "ext"
import "unsafe"

type A [unsafe.Sizeof(ext.B{ext.C})]int
type A2 [unsafe.Sizeof(ext.B{f:ext.C})]int // use a KeyValueExpr

type D [unsafe.Sizeof(struct{ f E })]int
type E ext.E

type F [3]G
type G [ext.C]int
`},
			want: map[string][]string{
				"A":  {"ext.B"}, // no ext.C: doesn't enter CompLit
				"A2": {"ext.B"}, // ditto
				"D":  {"ext.E"},
				"E":  {"ext.E"},
				"F":  {"ext.C"},
				"G":  {"ext.C"},
			},
		},
		{
			label: "imports",
			srcs: []string{`package p

import "ext"

import (
	"q"
	r2 "r"
	"s" // note: package name is t
	"z"
)

type A struct {
	q.Q
	r2.R
	s.S // invalid ref
	z.Z // references both external z.Z as well as package-level type z
}

type B struct {
	r.R // invalid ref
	t.T
}

var X int = q.V // X={}: no descent into RHS of 'var v T = rhs'
var Y = q.V.W

type z ext.Z
`},
			imports: map[string]string{"q": "q", "r": "r", "s": "t", "z": "z"},
			want: map[string][]string{
				"A": {"ext.Z", "q.Q", "r.R", "z.Z"},
				"B": {"t.T"},
				"Y": {"q.V"},
			},
		},
		{
			label: "import blank",
			srcs: []string{`package p

import _ "q"

type A q.Q
`},
			imports: map[string]string{"q": "q"},
			want:    map[string][]string{},
		},
		{
			label: "import dot",
			srcs: []string{`package p

import . "q"

type A q.Q // not actually an edge, since q is imported .
type B struct {
	C // assumed to be an edge to q
	D // resolved to package decl
}


type E error // unexported, therefore must be universe.error
type F Field
var G = Field.X
`, `package p

import "ext"
import "q"

type D ext.D
`},
			imports: map[string]string{"q": "q"},
			want: map[string][]string{
				"B": {"ext.D", "q.C"},
				"D": {"ext.D"},
				"F": {"q.Field"},
				"G": {"q.Field"},
			},
		},
		{
			label: "typeparams",
			srcs: []string{`package p

import "ext"

type A[T any] struct {
	t T
	b B
}

type B ext.B

func F1[T any](T, B)
func F2[T C]()(T, B)

type T ext.T

type C ext.C

func F3[T1 ~[]T2, T2 ~[]T3](t1 T1, t2 T2)
type T3 ext.T3
`, `package p

func (A[B]) M(C) {}
`},
			want: map[string][]string{
				"A":  {"ext.B", "ext.C"},
				"B":  {"ext.B"},
				"C":  {"ext.C"},
				"F1": {"ext.B"},
				"F2": {"ext.B", "ext.C"},
				"F3": {"ext.T3"},
				"T":  {"ext.T"},
				"T3": {"ext.T3"},
			},
			go118: true,
		},
		{
			label: "instances",
			srcs: []string{`package p

import "ext"

type A[T any] ext.A
type B[T1, T2 any] ext.B

type C A[int]
type D B[int, A[E]]
type E ext.E
`},
			want: map[string][]string{
				"A": {"ext.A"},
				"B": {"ext.B"},
				"C": {"ext.A"},
				"D": {"ext.A", "ext.B", "ext.E"},
				"E": {"ext.E"},
			},
			go118: true,
		},
		{
			label: "duplicate decls",
			srcs: []string{`package p

import "a"
import "ext"

type a a.A
type A a
type b ext.B
type C a.A
func (C) Foo(x) {} // invalid parameter, but that does not matter
type C b
func (C) Bar(y) {} // invalid parameter, but that does not matter

var x ext.X
var y ext.Y
`},
			imports: map[string]string{"a": "a", "b": "b"}, // "b" import should not matter, since it isn't in this file
			want: map[string][]string{
				"A": {"a.A"},
				"C": {"a.A", "ext.B", "ext.X", "ext.Y"},
			},
		},
		{
			label: "invalid decls",
			srcs: []string{`package p

import "ext"

type A B

func () Foo(B){}

var B struct{ ext.B
`},
			want: map[string][]string{
				"A":   {"ext.B"},
				"B":   {"ext.B"},
				"Foo": {"ext.B"},
			},
			allowErrs: true,
		},
		{
			label: "unmapped receiver",
			srcs: []string{`package p

type P struct{}

func (a) x(P)
`},
			want:      map[string][]string{},
			allowErrs: true,
		},
		{
			label: "SCC special case",
			srcs: []string{`package p

import "ext"

type X Y
type Y struct { Z; *X }
type Z map[ext.A]ext.B
`},
			want: map[string][]string{
				"X": {"ext.A", "ext.B"},
				"Y": {"ext.A", "ext.B"},
				"Z": {"ext.A", "ext.B"},
			},
			allowErrs: true,
		},
	}

	for _, test := range tests {
		t.Run(test.label, func(t *testing.T) {
			if test.go118 {
				testenv.NeedsGo1Point(t, 18)
			}

			var pgfs []*source.ParsedGoFile
			for i, src := range test.srcs {
				uri := span.URI(fmt.Sprintf("file:///%d.go", i))
				pgf, _ := cache.ParseGoSrc(ctx, token.NewFileSet(), uri, []byte(src), source.ParseFull)
				if !test.allowErrs && pgf.ParseErr != nil {
					t.Fatalf("ParseGoSrc(...) returned parse errors: %v", pgf.ParseErr)
				}
				pgfs = append(pgfs, pgf)
			}

			imports := map[source.ImportPath]*source.Metadata{
				"ext": {ID: "ext", Name: "ext"}, // this one comes for free
			}
			for path, m := range test.imports {
				imports[source.ImportPath(path)] = &source.Metadata{
					ID:   source.PackageID(m),
					Name: source.PackageName(m),
				}
			}

			data := typerefs.Encode(pgfs, "p", imports)

			got := make(map[string][]string)
			index := typerefs.NewPackageIndex()
			for _, class := range typerefs.Decode(index, "p", data) {
				// We redundantly expand out the name x refs cross product
				// here since that's what the existing tests expect.
				for _, name := range class.Decls {
					var syms []string
					for _, sym := range class.Refs {
						syms = append(syms, fmt.Sprintf("%s.%s", index.DeclaringPackage(sym), sym.Name))
					}
					sort.Strings(syms)
					got[name] = syms
				}
			}

			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("Refs(...) returned unexpected refs (-want +got):\n%s", diff)
			}
		})
	}
}
