// Copyright 2022 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

import (
	"go/ast"
	"go/token"
	"testing"

	. "go/types"
)

func BenchmarkNamed(b *testing.B) {
	const src = `
package p

type T struct {
	P int
}

func (T) M(int) {}
func (T) N() (i int) { return }

type G[P any] struct {
	F P
}

func (G[P]) M(P) {}
func (G[P]) N() (p P) { return }

type Inst = G[int]
	`
	pkg := mustTypecheck(src, nil, nil)

	var (
		T        = pkg.Scope().Lookup("T").Type()
		G        = pkg.Scope().Lookup("G").Type()
		SrcInst  = pkg.Scope().Lookup("Inst").Type()
		UserInst = mustInstantiate(b, G, Typ[Int])
	)

	tests := []struct {
		name string
		typ  Type
	}{
		{"nongeneric", T},
		{"generic", G},
		{"src instance", SrcInst},
		{"user instance", UserInst},
	}

	b.Run("Underlying", func(b *testing.B) {
		for _, test := range tests {
			b.Run(test.name, func(b *testing.B) {
				// Access underlying once, to trigger any lazy calculation.
				_ = test.typ.Underlying()
				b.ResetTimer()
				for i := 0; i < b.N; i++ {
					_ = test.typ.Underlying()
				}
			})
		}
	})

	b.Run("NewMethodSet", func(b *testing.B) {
		for _, test := range tests {
			b.Run(test.name, func(b *testing.B) {
				// Access underlying once, to trigger any lazy calculation.
				_ = NewMethodSet(test.typ)
				b.ResetTimer()
				for i := 0; i < b.N; i++ {
					_ = NewMethodSet(test.typ)
				}
			})
		}
	})
}

func mustInstantiate(tb testing.TB, orig Type, targs ...Type) Type {
	inst, err := Instantiate(nil, orig, targs, true)
	if err != nil {
		tb.Fatal(err)
	}
	return inst
}

// Test that types do not expand infinitely, as in go.dev/issue/52715.
func TestFiniteTypeExpansion(t *testing.T) {
	const src = `
package p

type Tree[T any] struct {
	*Node[T]
}

func (*Tree[R]) N(r R) R { return r }

type Node[T any] struct {
	*Tree[T]
}

func (Node[Q]) M(Q) {}

type Inst = *Tree[int]
`

	fset := token.NewFileSet()
	f := mustParse(fset, src)
	pkg := NewPackage("p", f.Name.Name)
	if err := NewChecker(nil, fset, pkg, nil).Files([]*ast.File{f}); err != nil {
		t.Fatal(err)
	}

	firstFieldType := func(n *Named) *Named {
		return n.Underlying().(*Struct).Field(0).Type().(*Pointer).Elem().(*Named)
	}

	Inst := Unalias(pkg.Scope().Lookup("Inst").Type()).(*Pointer).Elem().(*Named)
	Node := firstFieldType(Inst)
	Tree := firstFieldType(Node)
	if !Identical(Inst, Tree) {
		t.Fatalf("Not a cycle: got %v, want %v", Tree, Inst)
	}
	if Inst != Tree {
		t.Errorf("Duplicate instances in cycle: %s (%p) -> %s (%p) -> %s (%p)", Inst, Inst, Node, Node, Tree, Tree)
	}
}

// TestMethodOrdering is a simple test verifying that the indices of methods of
// a named type remain the same as long as the same source and AddMethod calls
// are presented to the type checker in the same order (go.dev/issue/61298).
func TestMethodOrdering(t *testing.T) {
	const src = `
package p

type T struct{}

func (T) a() {}
func (T) c() {}
func (T) b() {}
`
	// should get the same method order each time
	var methods []string
	for i := 0; i < 5; i++ {
		// collect T methods as provided in src
		pkg := mustTypecheck(src, nil, nil)
		T := pkg.Scope().Lookup("T").Type().(*Named)

		// add a few more methods manually
		for _, name := range []string{"foo", "bar", "bal"} {
			m := NewFunc(nopos, pkg, name, nil /* don't care about signature */)
			T.AddMethod(m)
		}

		// check method order
		if i == 0 {
			// first round: collect methods in given order
			methods = make([]string, T.NumMethods())
			for j := range methods {
				methods[j] = T.Method(j).Name()
			}
		} else {
			// successive rounds: methods must appear in the same order
			if got := T.NumMethods(); got != len(methods) {
				t.Errorf("got %d methods, want %d", got, len(methods))
				continue
			}
			for j, m := range methods {
				if got := T.Method(j).Name(); got != m {
					t.Errorf("got method %s, want %s", got, m)
				}
			}
		}
	}
}
