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

import (
	"fmt"
	"go/parser"
	"go/token"
	"reflect"
	"sort"
	"testing"

	"golang.org/x/tools/go/expect"
	"golang.org/x/tools/go/loader"
	"golang.org/x/tools/go/ssa"
	"golang.org/x/tools/internal/typeparams"
)

// TestGenericBodies tests that bodies of generic functions and methods containing
// different constructs can be built in BuilderMode(0).
//
// Each test specifies the contents of package containing a single go file.
// Each call print(arg0, arg1, ...) to the builtin print function
// in ssa is correlated a comment at the end of the line of the form:
//
//	//@ types(a, b, c)
//
// where a, b and c are the types of the arguments to the print call
// serialized using go/types.Type.String().
// See x/tools/go/expect for details on the syntax.
func TestGenericBodies(t *testing.T) {
	if !typeparams.Enabled {
		t.Skip("TestGenericBodies requires type parameters")
	}
	for _, test := range []struct {
		pkg      string // name of the package.
		contents string // contents of the Go package.
	}{
		{
			pkg: "p",
			contents: `
			package p

			func f(x int) {
				var i interface{}
				print(i, 0) //@ types("interface{}", int)
				print()     //@ types()
				print(x)    //@ types(int)
			}
			`,
		},
		{
			pkg: "q",
			contents: `
			package q

			func f[T any](x T) {
				print(x) //@ types(T)
			}
			`,
		},
		{
			pkg: "r",
			contents: `
			package r

			func f[T ~int]() {
				var x T
				print(x) //@ types(T)
			}
			`,
		},
		{
			pkg: "s",
			contents: `
			package s

			func a[T ~[4]byte](x T) {
				for k, v := range x {
					print(x, k, v) //@ types(T, int, byte)
				}
			}
			func b[T ~*[4]byte](x T) {
				for k, v := range x {
					print(x, k, v) //@ types(T, int, byte)
				}
			}
			func c[T ~[]byte](x T) {
				for k, v := range x {
					print(x, k, v) //@ types(T, int, byte)
				}
			}
			func d[T ~string](x T) {
				for k, v := range x {
					print(x, k, v) //@ types(T, int, rune)
				}
			}
			func e[T ~map[int]string](x T) {
				for k, v := range x {
					print(x, k, v) //@ types(T, int, string)
				}
			}
			func f[T ~chan string](x T) {
				for v := range x {
					print(x, v) //@ types(T, string)
				}
			}

			func From() {
				type A [4]byte
				print(a[A]) //@ types("func(x s.A)")

				type B *[4]byte
				print(b[B]) //@ types("func(x s.B)")

				type C []byte
				print(c[C]) //@ types("func(x s.C)")

				type D string
				print(d[D]) //@ types("func(x s.D)")

				type E map[int]string
				print(e[E]) //@ types("func(x s.E)")

				type F chan string
				print(f[F]) //@ types("func(x s.F)")
			}
			`,
		},
		{
			pkg: "t",
			contents: `
			package t

			func f[S any, T ~chan S](x T) {
				for v := range x {
					print(x, v) //@ types(T, S)
				}
			}

			func From() {
				type F chan string
				print(f[string, F]) //@ types("func(x t.F)")
			}
			`,
		},
		{
			pkg: "u",
			contents: `
			package u

			func fibonacci[T ~chan int](c, quit T) {
				x, y := 0, 1
				for {
					select {
					case c <- x:
						x, y = y, x+y
					case <-quit:
						print(c, quit, x, y) //@ types(T, T, int, int)
						return
					}
				}
			}
			func start[T ~chan int](c, quit T) {
				go func() {
					for i := 0; i < 10; i++ {
						print(<-c) //@ types(int)
					}
					quit <- 0
				}()
			}
			func From() {
				type F chan int
				c := make(F)
				quit := make(F)
				print(start[F], c, quit)     //@ types("func(c u.F, quit u.F)", "u.F", "u.F")
				print(fibonacci[F], c, quit) //@ types("func(c u.F, quit u.F)", "u.F", "u.F")
			}
			`,
		},
		{
			pkg: "v",
			contents: `
			package v

			func f[T ~struct{ x int; y string }](i int) T {
				u := []T{ T{0, "lorem"},  T{1, "ipsum"}}
				return u[i]
			}
			func From() {
				type S struct{ x int; y string }
				print(f[S])     //@ types("func(i int) v.S")
			}
			`,
		},
		{
			pkg: "w",
			contents: `
			package w

			func f[T ~[4]int8](x T, l, h int) []int8 {
				return x[l:h]
			}
			func g[T ~*[4]int16](x T, l, h int) []int16 {
				return x[l:h]
			}
			func h[T ~[]int32](x T, l, h int) T {
				return x[l:h]
			}
			func From() {
				type F [4]int8
				type G *[4]int16
				type H []int32
				print(f[F](F{}, 0, 0))  //@ types("[]int8")
				print(g[G](nil, 0, 0)) //@ types("[]int16")
				print(h[H](nil, 0, 0)) //@ types("w.H")
			}
			`,
		},
		{
			pkg: "x",
			contents: `
			package x

			func h[E any, T ~[]E](x T, l, h int) []E {
				s := x[l:h]
				print(s) //@ types("T")
				return s
			}
			func From() {
				type H []int32
				print(h[int32, H](nil, 0, 0)) //@ types("[]int32")
			}
			`,
		},
		{
			pkg: "y",
			contents: `
			package y

			// Test "make" builtin with different forms on core types and
			// when capacities are constants or variable.
			func h[E any, T ~[]E](m, n int) {
				print(make(T, 3))    //@ types(T)
				print(make(T, 3, 5)) //@ types(T)
				print(make(T, m))    //@ types(T)
				print(make(T, m, n)) //@ types(T)
			}
			func i[K comparable, E any, T ~map[K]E](m int) {
				print(make(T))    //@ types(T)
				print(make(T, 5)) //@ types(T)
				print(make(T, m)) //@ types(T)
			}
			func j[E any, T ~chan E](m int) {
				print(make(T))    //@ types(T)
				print(make(T, 6)) //@ types(T)
				print(make(T, m)) //@ types(T)
			}
			func From() {
				type H []int32
				h[int32, H](3, 4)
				type I map[int8]H
				i[int8, H, I](5)
				type J chan I
				j[I, J](6)
			}
			`,
		},
		{
			pkg: "z",
			contents: `
			package z

			func h[T ~[4]int](x T) {
				print(len(x), cap(x)) //@ types(int, int)
			}
			func i[T ~[4]byte | []int | ~chan uint8](x T) {
				print(len(x), cap(x)) //@ types(int, int)
			}
			func j[T ~[4]int | any | map[string]int]() {
				print(new(T)) //@ types("*T")
			}
			func k[T ~[4]int | any | map[string]int](x T) {
				print(x) //@ types(T)
				panic(x)
			}
			`,
		},
		{
			pkg: "a",
			contents: `
			package a

			func f[E any, F ~func() E](x F) {
				print(x, x()) //@ types(F, E)
			}
			func From() {
				type T func() int
				f[int, T](func() int { return 0 })
				f[int, func() int](func() int { return 1 })
			}
			`,
		},
		{
			pkg: "b",
			contents: `
			package b

			func f[E any, M ~map[string]E](m M) {
				y, ok := m["lorem"]
				print(m, y, ok) //@ types(M, E, bool)
			}
			func From() {
				type O map[string][]int
				f(O{"lorem": []int{0, 1, 2, 3}})
			}
			`,
		},
		{
			pkg: "c",
			contents: `
			package c

			func a[T interface{ []int64 | [5]int64 }](x T) int64 {
				print(x, x[2], x[3]) //@ types(T, int64, int64)
				x[2] = 5
				return x[3]
			}
			func b[T interface{ []byte | string }](x T) byte {
				print(x, x[3]) //@ types(T, byte)
		        return x[3]
			}
			func c[T interface{ []byte }](x T) byte {
				print(x, x[2], x[3]) //@ types(T, byte, byte)
				x[2] = 'b'
				return x[3]
			}
			func d[T interface{ map[int]int64 }](x T) int64 {
				print(x, x[2], x[3]) //@ types(T, int64, int64)
				x[2] = 43
        		return x[3]
			}
			func e[T ~string](t T) {
				print(t, t[0]) //@ types(T, uint8)
			}
			func f[T ~string|[]byte](t T) {
				print(t, t[0]) //@ types(T, uint8)
			}
			func g[T []byte](t T) {
				print(t, t[0]) //@ types(T, byte)
			}
			func h[T ~[4]int|[]int](t T) {
				print(t, t[0]) //@ types(T, int)
			}
			func i[T ~[4]int|*[4]int|[]int](t T) {
				print(t, t[0]) //@ types(T, int)
			}
			func j[T ~[4]int|*[4]int|[]int](t T) {
				print(t, &t[0]) //@ types(T, "*int")
			}
			`,
		},
		{
			pkg: "d",
			contents: `
			package d

			type MyInt int
			type Other int
			type MyInterface interface{ foo() }

			// ChangeType tests
			func ct0(x int) { v := MyInt(x);  print(x, v) /*@ types(int, "d.MyInt")*/ }
			func ct1[T MyInt | Other, S int ](x S) { v := T(x);  print(x, v) /*@ types(S, T)*/ }
			func ct2[T int, S MyInt | int ](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ }
			func ct3[T MyInt | Other, S MyInt | int ](x S) { v := T(x) ; print(x, v) /*@ types(S, T)*/ }

			// Convert tests
			func co0[T int | int8](x MyInt) { v := T(x); print(x, v) /*@ types("d.MyInt", T)*/}
			func co1[T int | int8](x T) { v := MyInt(x); print(x, v) /*@ types(T, "d.MyInt")*/ }
			func co2[S, T int | int8](x T) { v := S(x); print(x, v) /*@ types(T, S)*/ }

			// MakeInterface tests
			func mi0[T MyInterface](x T) { v := MyInterface(x); print(x, v) /*@ types(T, "d.MyInterface")*/ }

			// NewConst tests
			func nc0[T any]() { v := (*T)(nil); print(v) /*@ types("*T")*/}

			// SliceToArrayPointer
			func sl0[T *[4]int | *[2]int](x []int) { v := T(x); print(x, v) /*@ types("[]int", T)*/ }
			func sl1[T *[4]int | *[2]int, S []int](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ }
			`,
		},
		{
			pkg: "e",
			contents: `
			package e

			func c[T interface{ foo() string }](x T) {
				print(x, x.foo, x.foo())  /*@ types(T, "func() string", string)*/
			}
			`,
		},
		{
			pkg: "f",
			contents: `package f

			func eq[T comparable](t T, i interface{}) bool {
				return t == i
			}
			`,
		},
		{
			pkg: "g",
			contents: `package g
			type S struct{ f int }
			func c[P *S]() []P { return []P{{f: 1}} }
			`,
		},
		{
			pkg: "h",
			contents: `package h
			func sign[bytes []byte | string](s bytes) (bool, bool) {
				neg := false
				if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
					neg = s[0] == '-'
					s = s[1:]
				}
				return !neg, len(s) > 0
			}`,
		},
		{
			pkg: "i",
			contents: `package i
			func digits[bytes []byte | string](s bytes) bool {
				for _, c := range []byte(s) {
					if c < '0' || '9' < c {
						return false
					}
				}
				return true
			}`,
		},
		{
			pkg: "j",
			contents: `
			package j

			type E interface{}

			func Foo[T E, PT interface{ *T }]() T {
				pt := PT(new(T))
				x := *pt
				print(x)  /*@ types(T)*/
				return x
			}
			`,
		},
	} {
		test := test
		t.Run(test.pkg, func(t *testing.T) {
			// Parse
			conf := loader.Config{ParserMode: parser.ParseComments}
			fname := test.pkg + ".go"
			f, err := conf.ParseFile(fname, test.contents)
			if err != nil {
				t.Fatalf("parse: %v", err)
			}
			conf.CreateFromFiles(test.pkg, f)

			// Load
			lprog, err := conf.Load()
			if err != nil {
				t.Fatalf("Load: %v", err)
			}

			// Create and build SSA
			prog := ssa.NewProgram(lprog.Fset, ssa.SanityCheckFunctions)
			for _, info := range lprog.AllPackages {
				if info.TransitivelyErrorFree {
					prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable)
				}
			}
			p := prog.Package(lprog.Package(test.pkg).Pkg)
			p.Build()

			// Collect calls to the builtin print function.
			probes := make(map[*ssa.CallCommon]bool)
			for _, mem := range p.Members {
				if fn, ok := mem.(*ssa.Function); ok {
					for _, bb := range fn.Blocks {
						for _, i := range bb.Instrs {
							if i, ok := i.(ssa.CallInstruction); ok {
								call := i.Common()
								if b, ok := call.Value.(*ssa.Builtin); ok && b.Name() == "print" {
									probes[i.Common()] = true
								}
							}
						}
					}
				}
			}

			// Collect all notes in f, i.e. comments starting with "//@ types".
			notes, err := expect.ExtractGo(prog.Fset, f)
			if err != nil {
				t.Errorf("expect.ExtractGo: %v", err)
			}

			// Matches each probe with a note that has the same line.
			sameLine := func(x, y token.Pos) bool {
				xp := prog.Fset.Position(x)
				yp := prog.Fset.Position(y)
				return xp.Filename == yp.Filename && xp.Line == yp.Line
			}
			expectations := make(map[*ssa.CallCommon]*expect.Note)
			for call := range probes {
				var match *expect.Note
				for _, note := range notes {
					if note.Name == "types" && sameLine(call.Pos(), note.Pos) {
						match = note // first match is good enough.
						break
					}
				}
				if match != nil {
					expectations[call] = match
				} else {
					t.Errorf("Unmatched probe: %v", call)
				}
			}

			// Check each expectation.
			for call, note := range expectations {
				var args []string
				for _, a := range call.Args {
					args = append(args, a.Type().String())
				}
				if got, want := fmt.Sprint(args), fmt.Sprint(note.Args); got != want {
					t.Errorf("Arguments to print() were expected to be %q. got %q", want, got)
				}
			}
		})
	}
}

// TestInstructionString tests serializing instructions via Instruction.String().
func TestInstructionString(t *testing.T) {
	if !typeparams.Enabled {
		t.Skip("TestInstructionString requires type parameters")
	}
	// Tests (ssa.Instruction).String(). Instructions are from a single go file.
	// The Instructions tested are those that match a comment of the form:
	//
	//	//@ instrs(f, kind, strs...)
	//
	// where f is the name of the function, kind is the type of the instructions matched
	// within the function, and tests that the String() value for all of the instructions
	// matched of String() is strs (in some order).
	// See x/tools/go/expect for details on the syntax.

	const contents = `
	package p

	//@ instrs("f", "*ssa.TypeAssert")
	//@ instrs("f", "*ssa.Call", "print(nil:interface{}, 0:int)")
	func f(x int) { // non-generic smoke test.
		var i interface{}
		print(i, 0)
	}

	//@ instrs("h", "*ssa.Alloc", "local T (u)")
	//@ instrs("h", "*ssa.FieldAddr", "&t0.x [#0]")
	func h[T ~struct{ x string }]() T {
		u := T{"lorem"}
		return u
	}

	//@ instrs("c", "*ssa.TypeAssert", "typeassert t0.(interface{})")
	//@ instrs("c", "*ssa.Call", "invoke x.foo()")
	func c[T interface{ foo() string }](x T) {
		_ = x.foo
		_ = x.foo()
	}

	//@ instrs("d", "*ssa.TypeAssert", "typeassert t0.(interface{})")
	//@ instrs("d", "*ssa.Call", "invoke x.foo()")
	func d[T interface{ foo() string; comparable }](x T) {
		_ = x.foo
		_ = x.foo()
	}
	`

	// Parse
	conf := loader.Config{ParserMode: parser.ParseComments}
	const fname = "p.go"
	f, err := conf.ParseFile(fname, contents)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	conf.CreateFromFiles("p", f)

	// Load
	lprog, err := conf.Load()
	if err != nil {
		t.Fatalf("Load: %v", err)
	}

	// Create and build SSA
	prog := ssa.NewProgram(lprog.Fset, ssa.SanityCheckFunctions)
	for _, info := range lprog.AllPackages {
		if info.TransitivelyErrorFree {
			prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable)
		}
	}
	p := prog.Package(lprog.Package("p").Pkg)
	p.Build()

	// Collect all notes in f, i.e. comments starting with "//@ instr".
	notes, err := expect.ExtractGo(prog.Fset, f)
	if err != nil {
		t.Errorf("expect.ExtractGo: %v", err)
	}

	// Expectation is a {function, type string} -> {want, matches}
	// where matches is all Instructions.String() that match the key.
	// Each expecation is that some permutation of matches is wants.
	type expKey struct {
		function string
		kind     string
	}
	type expValue struct {
		wants   []string
		matches []string
	}
	expectations := make(map[expKey]*expValue)
	for _, note := range notes {
		if note.Name == "instrs" {
			if len(note.Args) < 2 {
				t.Error("Had @instrs annotation without at least 2 arguments")
				continue
			}
			fn, kind := fmt.Sprint(note.Args[0]), fmt.Sprint(note.Args[1])
			var wants []string
			for _, arg := range note.Args[2:] {
				wants = append(wants, fmt.Sprint(arg))
			}
			expectations[expKey{fn, kind}] = &expValue{wants, nil}
		}
	}

	// Collect all Instructions that match the expectations.
	for _, mem := range p.Members {
		if fn, ok := mem.(*ssa.Function); ok {
			for _, bb := range fn.Blocks {
				for _, i := range bb.Instrs {
					kind := fmt.Sprintf("%T", i)
					if e := expectations[expKey{fn.Name(), kind}]; e != nil {
						e.matches = append(e.matches, i.String())
					}
				}
			}
		}
	}

	// Check each expectation.
	for key, value := range expectations {
		if _, ok := p.Members[key.function]; !ok {
			t.Errorf("Expectation on %s does not match a member in %s", key.function, p.Pkg.Name())
		}
		got, want := value.matches, value.wants
		sort.Strings(got)
		sort.Strings(want)
		if !reflect.DeepEqual(want, got) {
			t.Errorf("Within %s wanted instructions of kind %s: %q. got %q", key.function, key.kind, want, got)
		}
	}
}
