// Copyright 2025 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 golang

import (
	"go/types"
	"maps"
	"testing"

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

func TestUnify(t *testing.T) {
	// Most cases from TestMatches in gopls/internal/util/fingerprint/fingerprint_test.go.
	const src = `
-- go.mod --
module example.com
go 1.24

-- a/a.go --
package a

type Int = int
type String = string

// Eq.Equal matches casefold.Equal.
type Eq[T any] interface { Equal(T, T) bool }
type casefold struct{}
func (casefold) Equal(x, y string) bool

// A matches AString.
type A[T any] = struct { x T }
type AString = struct { x string }

// B matches anything!
type B[T any] = T

func C1[T any](int, T, ...string) T { panic(0) }
func C2[U any](int, int, ...U) bool { panic(0) }
func C3(int, bool, ...string) rune
func C4(int, bool, ...string)
func C5(int, float64, bool, string) bool
func C6(int, bool, ...string) bool

func DAny[T any](Named[T]) { panic(0) }
func DString(Named[string])
func DInt(Named[int])

type Named[T any] struct { x T }

func E1(byte) rune
func E2(uint8) int32
func E3(int8) uint32

// generic vs. generic
func F1[T any](T) { panic(0) }
func F2[T any](*T) { panic(0) }
func F3[T any](T, T) { panic(0) }
func F4[U any](U, *U) {panic(0) }
func F4a[U any](U, Named[U]) {panic(0) }
func F5[T, U any](T, U, U) { panic(0) }
func F6[T any](T, int, T) { panic(0) }
func F7[T any](bool, T, T) { panic(0) }
func F8[V any](*V, int, int) { panic(0) }
func F9[V any](V, *V, V) { panic(0) }
`
	type tmap = map[*types.TypeParam]types.Type

	var (
		boolType   = types.Typ[types.Bool]
		intType    = types.Typ[types.Int]
		stringType = types.Typ[types.String]
	)

	pkg := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./a")[0]
	scope := pkg.Types.Scope()

	tparam := func(name string, index int) *types.TypeParam {
		obj := scope.Lookup(name)
		var tps *types.TypeParamList
		switch obj := obj.(type) {
		case *types.Func:
			tps = obj.Signature().TypeParams()
		case *types.TypeName:
			if n, ok := obj.Type().(*types.Named); ok {
				tps = n.TypeParams()
			} else {
				tps = obj.Type().(*types.Alias).TypeParams()
			}
		default:
			t.Fatalf("unsupported object of type %T", obj)
		}
		return tps.At(index)
	}

	for _, test := range []struct {
		x, y       string // the symbols in the above source code whose types to unify
		method     string // optional field or method
		params     tmap   // initial values of type params
		want       bool   // success or failure
		wantParams tmap   // expected output
	}{
		{
			// In Eq[T], T is bound to string.
			x:          "Eq",
			y:          "casefold",
			method:     "Equal",
			want:       true,
			wantParams: tmap{tparam("Eq", 0): stringType},
		},
		{
			// If we unify A[T] and A[string], T should be bound to string.
			x:          "A",
			y:          "AString",
			want:       true,
			wantParams: tmap{tparam("A", 0): stringType},
		},
		{x: "A", y: "Eq", want: false}, // completely unrelated
		{
			x:          "B",
			y:          "String",
			want:       true,
			wantParams: tmap{tparam("B", 0): stringType},
		},
		{
			x:          "B",
			y:          "Int",
			want:       true,
			wantParams: tmap{tparam("B", 0): intType},
		},
		{
			x:    "B",
			y:    "A",
			want: true,
			// B's T is bound to A's struct { x T }
			wantParams: tmap{tparam("B", 0): scope.Lookup("A").Type().Underlying()},
		},
		{
			// C1's U unifies with C6's bool.
			x:          "C1",
			y:          "C6",
			wantParams: tmap{tparam("C1", 0): boolType},
			want:       true,
		},
		// C1 fails to unify with C2 because C1's T must be bound to both int and bool.
		{x: "C1", y: "C2", want: false},
		// The remaining "C" cases fail for less interesting reasons, usually different numbers
		// or types of parameters or results.
		{x: "C1", y: "C3", want: false},
		{x: "C1", y: "C4", want: false},
		{x: "C1", y: "C5", want: false},
		{x: "C2", y: "C3", want: false},
		{x: "C2", y: "C4", want: false},
		{x: "C3", y: "C4", want: false},
		{
			x:          "DAny",
			y:          "DString",
			want:       true,
			wantParams: tmap{tparam("DAny", 0): stringType},
		},
		{x: "DString", y: "DInt", want: false}, // different instantiations of Named
		{x: "E1", y: "E2", want: true},         // byte and rune are just aliases
		{x: "E2", y: "E3", want: false},

		// The following tests cover all of the type param cases of unify.
		{
			// F1[*int] = F2[int], for example
			// F1's T is bound to a pointer to F2's T.
			x: "F1",
			// F2's T is unbound: any instantiation works.
			y:          "F2",
			want:       true,
			wantParams: tmap{tparam("F1", 0): types.NewPointer(tparam("F2", 0))},
		},
		{x: "F3", y: "F4", want: false},  // would require U identical to *U, prevented by occur check
		{x: "F3", y: "F4a", want: false}, // occur check through Named[T]
		{
			x:    "F5",
			y:    "F6",
			want: true,
			wantParams: tmap{
				tparam("F5", 0): intType,
				tparam("F5", 1): intType,
				tparam("F6", 0): intType,
			},
		},
		{x: "F6", y: "F7", want: false}, // both are bound
		{
			x:      "F5",
			y:      "F6",
			params: tmap{tparam("F6", 0): intType}, // consistent with the result
			want:   true,
			wantParams: tmap{
				tparam("F5", 0): intType,
				tparam("F5", 1): intType,
				tparam("F6", 0): intType,
			},
		},
		{
			x:      "F5",
			y:      "F6",
			params: tmap{tparam("F6", 0): boolType}, // not consistent
			want:   false,
		},
		{x: "F6", y: "F7", want: false}, // both are bound
		{
			// T=*V, U=int, V=int
			x:    "F5",
			y:    "F8",
			want: true,
			wantParams: tmap{
				tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
				tparam("F5", 1): intType,
			},
		},
		{
			// T=*V, U=int, V=int
			// Partial initial information is fine, as long as it's consistent.
			x:      "F5",
			y:      "F8",
			want:   true,
			params: tmap{tparam("F5", 1): intType},
			wantParams: tmap{
				tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
				tparam("F5", 1): intType,
			},
		},
		{
			// T=*V, U=int, V=int
			// Partial initial information is fine, as long as it's consistent.
			x:      "F5",
			y:      "F8",
			want:   true,
			params: tmap{tparam("F5", 0): types.NewPointer(tparam("F8", 0))},
			wantParams: tmap{
				tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
				tparam("F5", 1): intType,
			},
		},
		{x: "F5", y: "F9", want: false}, // T is unbound, V is bound, and T occurs in V
		{
			// T bound to Named[T']
			x:    "F1",
			y:    "DAny",
			want: true,
			wantParams: tmap{
				tparam("F1", 0): scope.Lookup("DAny").(*types.Func).Signature().Params().At(0).Type()},
		},
	} {

		lookup := func(name string) types.Type {
			obj := scope.Lookup(name)
			if obj == nil {
				t.Fatalf("Lookup %s failed", name)
			}
			if test.method != "" {
				obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, pkg.Types, test.method)
				if obj == nil {
					t.Fatalf("Lookup %s.%s failed", name, test.method)
				}
			}
			return obj.Type()
		}

		check := func(a, b string, want, compareParams bool) {
			t.Helper()

			ta := lookup(a)
			tb := lookup(b)

			var gotParams tmap
			if test.params == nil {
				// Get the unifier even if there are no input params.
				gotParams = tmap{}
			} else {
				gotParams = maps.Clone(test.params)
			}
			got := unify(ta, tb, gotParams)
			if got != want {
				t.Errorf("a=%s b=%s method=%s: unify returned %t for these inputs:\n- %s\n- %s",
					a, b, test.method, got, ta, tb)
				return
			}
			if !compareParams {
				return
			}
			if !maps.EqualFunc(gotParams, test.wantParams, types.Identical) {
				t.Errorf("x=%s y=%s method=%s: params: got %v, want %v",
					a, b, test.method, gotParams, test.wantParams)
			}
		}

		check(test.x, test.y, test.want, true)
		// unify is symmetric
		check(test.y, test.x, test.want, true)
		// unify is reflexive
		check(test.x, test.x, true, false)
		check(test.y, test.y, true, false)
	}
}
