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

import (
	"fmt"
	"go/ast"
	"go/importer"
	"go/parser"
	"go/token"
	"go/types"
	"reflect"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
)

// TestFreeRefs is a unit test of the free-references algorithm.
func TestFreeRefs(t *testing.T) {
	if runtime.GOOS == "js" || runtime.GOARCH == "wasm" {
		t.Skip("some test imports are unsupported on js or wasm")
	}

	for i, test := range []struct {
		src  string
		want []string // expected list of "scope kind dotted-path" triples
	}{
		{
			// basic example (has a "cannot infer" type error)
			`package p; func f[T ~int](x any) { var y T; « f(x.(T) + y) » }`,
			[]string{"pkg func f", "local var x", "local typename T", "local var y"},
		},
		{
			// selection need not be tree-aligned
			`package p; type T int; type U « T; func _(x U) »`,
			[]string{"pkg typename T", "pkg typename U"},
		},
		{
			// imported symbols
			`package p; import "fmt"; func f() { « var x fmt.Stringer » }`,
			[]string{"file pkgname fmt.Stringer"},
		},
		{
			// unsafe and error, our old nemeses
			`package p; import "unsafe"; var ( « _  unsafe.Pointer; _ = error(nil).Error »; )`,
			[]string{"file pkgname unsafe.Pointer"},
		},
		{
			// two attributes of a var, but not the var itself
			`package p; import "bytes"; func _(buf bytes.Buffer) { « buf.WriteByte(0); buf.WriteString(""); » }`,
			[]string{"local var buf.WriteByte", "local var buf.WriteString"},
		},
		{
			// dot imports (an edge case)
			`package p; import . "errors"; var _ = « New»`,
			[]string{"file pkgname errors.New"},
		},
		{
			// struct field (regression test for overzealous dot import logic)
			`package p; import "net/url"; var _ = «url.URL{Host: ""}»`,
			[]string{"file pkgname url.URL"},
		},
		{
			// dot imports (another regression test of same)
			`package p; import . "net/url"; var _ = «URL{Host: ""}»`,
			[]string{"file pkgname url.URL"},
		},
		{
			// dot import of unsafe (a corner case)
			`package p; import . "unsafe"; var _ « Pointer»`,
			[]string{"file pkgname unsafe.Pointer"},
		},
		{
			// dotted path
			`package p; import "go/build"; var _ = « build.Default.GOOS »`,
			[]string{"file pkgname build.Default.GOOS"},
		},
		{
			// type error
			`package p; import "nope"; var _ = « nope.nope.nope »`,
			[]string{"file pkgname nope"},
		},
	} {
		name := fmt.Sprintf("file%d.go", i)
		t.Run(name, func(t *testing.T) {
			fset := token.NewFileSet()
			startOffset := strings.Index(test.src, "«")
			endOffset := strings.Index(test.src, "»")
			if startOffset < 0 || endOffset < startOffset {
				t.Fatalf("invalid «...» selection (%d:%d)", startOffset, endOffset)
			}
			src := test.src[:startOffset] +
				" " +
				test.src[startOffset+len("«"):endOffset] +
				" " +
				test.src[endOffset+len("»"):]
			f, err := parser.ParseFile(fset, name, src, parser.SkipObjectResolution)
			if err != nil {
				t.Fatal(err)
			}
			conf := &types.Config{
				Importer: importer.Default(),
				Error:    func(err error) { t.Log(err) }, // not fatal
			}
			info := &types.Info{
				Uses:   make(map[*ast.Ident]types.Object),
				Scopes: make(map[ast.Node]*types.Scope),
				Types:  make(map[ast.Expr]types.TypeAndValue),
			}
			pkg, _ := conf.Check(f.Name.Name, fset, []*ast.File{f}, info) // ignore errors
			tf := fset.File(f.Package)
			refs := freeRefs(pkg, info, f, tf.Pos(startOffset), tf.Pos(endOffset))

			kind := func(obj types.Object) string { // e.g. "var", "const"
				return strings.ToLower(reflect.TypeOf(obj).Elem().Name())
			}

			var got []string
			for _, ref := range refs {
				msg := ref.scope + " " + kind(ref.objects[0]) + " " + ref.dotted
				got = append(got, msg)
			}
			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("(-want +got)\n%s", diff)
			}
		})
	}
}
