// Copyright 2021 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.

//go:build go1.18

package typeparams_test

import (
	"bytes"
	"go/ast"
	"go/build"
	"go/importer"
	"go/parser"
	"go/token"
	"go/types"
	"strings"
	"testing"
)

// TestAPIConsistency verifies that exported APIs match at Go 1.17 and Go
// 1.18+.
//
// It relies on the convention that the names of type aliases in the typeparams
// package match the names of the types they are aliasing.
//
// This test could be made more precise.
func TestAPIConsistency(t *testing.T) {
	api118 := getAPI(buildPackage(t, true))
	api117 := getAPI(buildPackage(t, false))

	for name, api := range api117 {
		if api != api118[name] {
			t.Errorf("%q: got %s at 1.17, but %s at 1.18+", name, api, api118[name])
		}
		delete(api118, name)
	}
	for name, api := range api118 {
		// Go 1.23 has iterator methods that return Seq.
		// These methods can't be supported at 1.17.
		if strings.Contains(api, "Seq") && api117[name] == "" {
			continue
		}

		if api != api117[name] {
			t.Errorf("%q: got %s at 1.18+, but %s at 1.17", name, api, api117[name])
		}
	}
}

func getAPI(pkg *types.Package) map[string]string {
	api := make(map[string]string)
	for _, name := range pkg.Scope().Names() {
		if !token.IsExported(name) {
			continue
		}
		api[name] = name
		obj := pkg.Scope().Lookup(name)
		if f, ok := obj.(*types.Func); ok {
			api[name] = formatSignature(f.Type().(*types.Signature))
		}
		typ := pkg.Scope().Lookup(name).Type()
		// Consider method sets of pointer and non-pointer receivers.
		msets := map[string]*types.MethodSet{
			name:       types.NewMethodSet(typ),
			"*" + name: types.NewMethodSet(types.NewPointer(typ)),
		}
		for name, mset := range msets {
			for i := 0; i < mset.Len(); i++ {
				f := mset.At(i).Obj().(*types.Func)
				mname := f.Name()
				if token.IsExported(mname) {
					api[name+"."+mname] = formatSignature(f.Type().(*types.Signature))
				}
			}
		}
	}
	return api
}

func formatSignature(sig *types.Signature) string {
	var b bytes.Buffer
	b.WriteString("func")
	writeTuple(&b, sig.Params())
	writeTuple(&b, sig.Results())
	return b.String()
}

func writeTuple(buf *bytes.Buffer, t *types.Tuple) {
	buf.WriteRune('(')

	// The API at Go 1.18 uses aliases for types in go/types. These types are
	// _actually_ in the go/types package, and therefore would be formatted as
	// e.g. *types.TypeParam, which would not match *typeparams.TypeParam --
	// go/types does not track aliases. As we use the same name for all aliases,
	// we can make the formatted signatures match by dropping the package
	// qualifier.
	qf := func(*types.Package) string { return "" }

	for i := 0; i < t.Len(); i++ {
		if i > 0 {
			buf.WriteString(", ")
		}
		buf.WriteString(types.TypeString(t.At(i).Type(), qf))
	}
	buf.WriteRune(')')
}

func buildPackage(t *testing.T, go118 bool) *types.Package {
	ctxt := build.Default
	if !go118 {
		for i, tag := range ctxt.ReleaseTags {
			if tag == "go1.18" {
				ctxt.ReleaseTags = ctxt.ReleaseTags[:i]
				break
			}
		}
	}
	bpkg, err := ctxt.ImportDir(".", 0)
	if err != nil {
		t.Fatal(err)
	}
	return typeCheck(t, bpkg.GoFiles)
}

func typeCheck(t *testing.T, filenames []string) *types.Package {
	fset := token.NewFileSet()
	var files []*ast.File
	for _, name := range filenames {
		f, err := parser.ParseFile(fset, name, nil, 0)
		if err != nil {
			t.Fatal(err)
		}
		files = append(files, f)
	}
	conf := types.Config{
		Importer: importer.Default(),
	}
	pkg, err := conf.Check("", fset, files, nil)
	if err != nil {
		t.Fatal(err)
	}
	return pkg
}
