// Copyright 2026 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 types_test

// This file defines a test of using [types.Hasher].

import (
	"go/ast"
	"go/token"
	"go/types"
	"hash/maphash"
	"testing"
)

func TestHasher(t *testing.T) {
	const src = `
package p

// Basic defined types.
type T1 int
type T2 int

// Identical methods.
func (T1) M(int) {}
func (T2) M(int) {}

// A constraint interface.
type C interface {
	~int | string
}

type I interface {
}

// A generic type.
type G[P C] int

// Generic functions with identical signature.
func Fa1[P C](p P) {}
func Fa2[Q C](q Q) {}

// Fb1 and Fb2 are identical and should be mapped to the same entry, even if we
// map their arguments first.
func Fb1[P any](x *P) {
	var y *P // Map this first.
	_ = y
}
func Fb2[Q any](x *Q) {
}

// G1 and G2 are mutally recursive, and have identical methods.
type G1[P any] struct{
	Field *G2[P]
}
func (G1[P]) M(G1[P], G2[P]) {}
type G2[Q any] struct{
	Field *G1[Q]
}
func (G2[P]) M(G1[P], G2[P]) {}

// Method type expressions on different generic types are different.
var ME1 = G1[int].M
var ME2 = G2[int].M

// ME1Type should have identical type as ME1.
var ME1Type func(G1[int], G1[int], G2[int])

// Examples from issue #51314
type Constraint[T any] any
func Foo[T Constraint[T]]() {}
func Fn[T1 ~*T2, T2 ~*T1](t1 T1, t2 T2) {}

// Bar and Baz are identical to Foo.
func Bar[P Constraint[P]]() {}
func Baz[Q any]() {} // The underlying type of Constraint[P] is any.
// But Quux is not.
func Quux[Q interface{ quux() }]() {}

type Issue56048_I interface{ m() interface { Issue56048_I } }
var Issue56048 = Issue56048_I.m

type Issue56048_Ib interface{ m() chan []*interface { Issue56048_Ib } }
var Issue56048b = Issue56048_Ib.m

// Non-generic alias
type NonAlias int
type Alias1 = NonAlias
type Alias2 = NonAlias

type Tagged1 struct { F int "tag1" }
type Tagged2 struct { F int "tag2" }
`

	fset := token.NewFileSet()
	file := mustParse(fset, src)

	var conf types.Config
	pkg, err := conf.Check("", fset, []*ast.File{file}, nil)
	if err != nil {
		t.Fatal(err)
	}

	instantiate := func(origin types.Type, targs ...types.Type) types.Type {
		inst, err := types.Instantiate(nil, origin, targs, true)
		if err != nil {
			t.Fatal(err)
		}
		return inst
	}

	scope := pkg.Scope()
	var (
		tInt    = types.Typ[types.Int]
		tString = types.Typ[types.String]

		T1      = scope.Lookup("T1").Type().(*types.Named)
		T2      = scope.Lookup("T2").Type().(*types.Named)
		T1M     = T1.Method(0).Type()
		T2M     = T2.Method(0).Type()
		G       = scope.Lookup("G").Type()
		GInt1   = instantiate(G, tInt)
		GInt2   = instantiate(G, tInt)
		GStr    = instantiate(G, tString)
		C       = scope.Lookup("C").Type()
		CI      = C.Underlying().(*types.Interface)
		I       = scope.Lookup("I").Type()
		II      = I.Underlying().(*types.Interface)
		U       = CI.EmbeddedType(0).(*types.Union)
		Fa1     = scope.Lookup("Fa1").Type().(*types.Signature)
		Fa2     = scope.Lookup("Fa2").Type().(*types.Signature)
		Fa1P    = Fa1.TypeParams().At(0)
		Fa2Q    = Fa2.TypeParams().At(0)
		Fb1     = scope.Lookup("Fb1").Type().(*types.Signature)
		Fb1x    = Fb1.Params().At(0).Type()
		Fb1y    = scope.Lookup("Fb1").(*types.Func).Scope().Lookup("y").Type()
		Fb2     = scope.Lookup("Fb2").Type().(*types.Signature)
		Fb2x    = Fb2.Params().At(0).Type()
		G1      = scope.Lookup("G1").Type().(*types.Named)
		G1M     = G1.Method(0).Type()
		G1IntM1 = instantiate(G1, tInt).(*types.Named).Method(0).Type()
		G1IntM2 = instantiate(G1, tInt).(*types.Named).Method(0).Type()
		G1StrM  = instantiate(G1, tString).(*types.Named).Method(0).Type()
		G2      = scope.Lookup("G2").Type()
		G2IntM  = instantiate(G2, tInt).(*types.Named).Method(0).Type()
		ME1     = scope.Lookup("ME1").Type()
		ME1Type = scope.Lookup("ME1Type").Type()
		ME2     = scope.Lookup("ME2").Type()

		Constraint  = scope.Lookup("Constraint").Type()
		Foo         = scope.Lookup("Foo").Type()
		Fn          = scope.Lookup("Fn").Type()
		Bar         = scope.Lookup("Bar").Type()
		Baz         = scope.Lookup("Baz").Type()
		Quux        = scope.Lookup("Quux").Type()
		Issue56048  = scope.Lookup("Issue56048").Type()
		Issue56048b = scope.Lookup("Issue56048b").Type()

		NonAlias = scope.Lookup("NonAlias").Type()
		Alias1   = scope.Lookup("Alias1").Type()
		Alias2   = scope.Lookup("Alias2").Type()

		Tagged1 = scope.Lookup("Tagged1").Type().Underlying().(*types.Struct)
		Tagged2 = scope.Lookup("Tagged2").Type().Underlying().(*types.Struct)
	)

	// eqclasses groups the above types into types.Identical equivalence classes.
	eqclasses := [][]types.Type{
		{T1},
		{T2},
		{G},
		{C},
		{CI},
		{U},
		{I},
		{II}, // should not be identical to CI
		{T1M, T2M},
		{GInt1, GInt2},
		{GStr},
		{Fa1, Fa2},
		{Fa1P},
		{Fa2Q},
		{Fb1y, Fb1x},
		{Fb2x},
		{Fb1, Fb2},
		{G1},
		{G1M},
		{G2},
		{G1IntM1, G1IntM2, G2IntM},
		{G1StrM},
		{ME1, ME1Type},
		{ME2},
		{Constraint},
		{Foo, Bar},
		{Baz},
		{Fn},
		{Quux},
		{Issue56048},
		{Issue56048b},
		{NonAlias, Alias1, Alias2},
	}

	run := func(t *testing.T, hasher maphash.Hasher[types.Type], eq func(x, y types.Type) bool, classes [][]types.Type) {
		seed := maphash.MakeSeed()

		hash := func(t types.Type) uint64 {
			var h maphash.Hash
			h.SetSeed(seed)
			hasher.Hash(&h, t)
			return h.Sum64()
		}

		for xi, class := range classes {
			tx := class[0] // arbitrary representative of class

			for yi := range classes {
				if xi == yi {
					// Within a class, each element is equivalent to first
					// and has the same hash.
					for i, ty := range class {
						hx, hy := hash(tx), hash(ty)
						if !eq(tx, ty) || hx != hy {
							t.Fatalf("class[%d][%d] (%v, hash %x) is not equivalent to class[%d][%d] (%v, hash %x)",
								xi, 0, tx, hx,
								yi, i, ty, hy)
						}
					}
				} else {
					// Across classes, no element is equivalent to first.
					// (We can't say for sure that the hashes are unequal.)
					for k, typ := range classes[yi] {
						if eq(tx, typ) {
							t.Fatalf("class[%d][%d] (%v) is equivalent to class[%d][%d] (%v)",
								xi, 0, tx,
								yi, k, typ)
						}
					}
				}
			}
		}
	}

	// Hasher considers the two Tagged{1,2} types distinct.
	t.Run("Hasher", func(t *testing.T) {
		run(t, types.Hasher{}, types.Identical, append(
			eqclasses,
			[]types.Type{Tagged1},
			[]types.Type{Tagged2},
		))
	})

	// HasherIgnoreTags considers the two Tagged{1,2} types equal.
	t.Run("HasherIgnoreTags", func(t *testing.T) {
		run(t, types.HasherIgnoreTags{}, types.IdenticalIgnoreTags, append(
			eqclasses,
			[]types.Type{Tagged1, Tagged2},
		))
	})
}
