// Copyright 2024 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 typesinternal_test

import (
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"maps"
	"slices"
	"strings"
	"testing"

	"golang.org/x/tools/go/types/typeutil"
	"golang.org/x/tools/internal/testenv"
	"golang.org/x/tools/internal/typesinternal"
)

const elementSrc = `
package p

type A = int

type B = *map[chan int][]func() [2]bool

type C = T

type T struct{ x int }
func (T) method() uint
func (*T) ptrmethod() complex128

type D = A

type E = struct{ x int }

type F = func(int8, int16) (int32, int64)

type G = struct { U }

type U struct{}
func (U) method() uint32

`

func TestForEachElement(t *testing.T) {
	// Add generic method, if go1.27.
	// It doesn't change the outcome:
	// complex64 is not expected in the result.
	elementSrc := elementSrc
	if testenv.Go1Point() >= 27 {
		elementSrc += `
// generic method: T.generic[U] is not explored.
func (T) generic[U any](complex64) {}

// method of generic type
type H[T any] uintptr
func (H[T]) m(T) uint16
type Hf64 = H[float64]
`
		// (still available: uint8 uint64 float32)
	}

	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "a.go", elementSrc, 0)
	if err != nil {
		t.Fatal(err) // parse error
	}
	var config types.Config
	pkg, err := config.Check(f.Name.Name, fset, []*ast.File{f}, nil)
	if err != nil {
		t.Fatal(err) // type error
	}

	type testcase struct {
		name string   // name of a type alias whose RHS type's elements to compute
		want []string // strings of types that are/are not elements (! => not)
	}
	tests := []testcase{
		// simple type
		{"A", []string{"int"}},

		// compound type
		{"B", []string{
			"*map[chan int][]func() [2]bool",
			"map[chan int][]func() [2]bool",
			"chan int",
			"int",
			"[]func() [2]bool",
			"func() [2]bool",
			"[2]bool",
			"bool",
		}},

		// defined struct type with methods, incl. pointer methods.
		// Observe that it descends into the field type, but
		// the result does not include the struct type itself.
		// (This follows the Go toolchain behavior, and finesses the need
		// to create wrapper methods for that struct type.)
		{"C", []string{"T", "*T", "int", "uint", "complex128", "!complex64", "!struct{x int}"}},

		// alias type
		{"D", []string{"int"}},

		// struct type not beneath a defined type
		{"E", []string{"struct{x int}", "int"}},

		// signature types: the params/results tuples
		// are traversed but not included.
		{"F", []string{"func(int8, int16) (int32, int64)",
			"int8", "int16", "int32", "int64"}},

		// struct with embedded field that has methods
		{"G", []string{"*U", "struct{U}", "uint32", "U"}},
	}
	if testenv.Go1Point() >= 27 {
		tests = append(tests, []testcase{
			// H[float64].m is a ground type, so it is visited, giving us uint16.
			{"Hf64", []string{"*H[float64]", "H[float64]", "float64", "uint16"}},
		}...)
	}
	var msets typeutil.MethodSetCache
	for _, test := range tests {
		tname, ok := pkg.Scope().Lookup(test.name).(*types.TypeName)
		if !ok {
			t.Errorf("no such type %q", test.name)
			continue
		}
		T := types.Unalias(tname.Type())

		toStr := func(T types.Type) string {
			return types.TypeString(T, func(*types.Package) string { return "" })
		}

		got := make(map[string]bool)
		set := new(typeutil.Map)  // for de-duping
		set2 := new(typeutil.Map) // for consistency check
		typesinternal.ForEachElement(set, &msets, T, func(elem types.Type) {
			got[toStr(elem)] = true
			set2.Set(elem, true)
		})

		// Assert that set==set2, meaning f(x) was
		// called for each x in the de-duping map.
		if set.Len() != set2.Len() {
			t.Errorf("ForEachElement called f %d times yet de-dup set has %d elements",
				set2.Len(), set.Len())
		} else {
			set.Iterate(func(key types.Type, _ any) {
				if set2.At(key) == nil {
					t.Errorf("ForEachElement did not call f(%v)", key)
				}
			})
		}

		// Assert than all expected (and no unexpected) elements were found.
		fail := false
		for _, typstr := range test.want {
			found := got[typstr]
			typstr, unwanted := strings.CutPrefix(typstr, "!")
			if found && unwanted {
				fail = true
				t.Errorf("ForEachElement(%s): unwanted element %q", T, typstr)
			} else if !found && !unwanted {
				fail = true
				t.Errorf("ForEachElement(%s): element %q not found", T, typstr)
			}
		}
		if fail {
			t.Logf("got elements:\n%s", strings.Join(slices.Sorted(maps.Keys(got)), "\n"))
		}
	}
}
