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

import (
	"fmt"
	"go/ast"
	"go/types"
	"strings"

	"golang.org/x/tools/gopls/internal/protocol"
)

// golang/go#51089
// *testing.F deserves special treatment as member use is constrained:
// The arguments to f.Fuzz are determined by the arguments to a previous f.Add
// Inside f.Fuzz only f.Failed and f.Name are allowed.
// PJW: are there other packages where we can deduce usage constraints?

// if we find fuzz completions, then return true, as those are the only completions to offer
func (c *completer) fuzz(testingF types.Type, imp *importInfo, cb func(candidate)) bool {
	// 1. inside f.Fuzz? (only f.Failed and f.Name)
	// 2. possible completing f.Fuzz?
	//    [Ident,SelectorExpr,Callexpr,ExprStmt,BlockiStmt,FuncDecl(Fuzz...)]
	// 3. before f.Fuzz, same (for 2., offer choice when looking at an F)

	mset := types.NewMethodSet(testingF)

	// does the path contain FuncLit as arg to f.Fuzz CallExpr?
	inside := false
Loop:
	for i, n := range c.path {
		switch v := n.(type) {
		case *ast.CallExpr:
			if len(v.Args) != 1 {
				continue Loop
			}
			if _, ok := v.Args[0].(*ast.FuncLit); !ok {
				continue
			}
			if s, ok := v.Fun.(*ast.SelectorExpr); !ok || s.Sel.Name != "Fuzz" {
				continue
			}
			if i > 2 { // avoid t.Fuzz itself in tests
				inside = true
				break Loop
			}
		}
	}
	if inside {
		for method := range mset.Methods() {
			o := method.Obj()
			if o.Name() == "Failed" || o.Name() == "Name" {
				cb(candidate{
					obj:         o,
					score:       stdScore,
					imp:         imp,
					addressable: true,
				})
			}
		}
		return true
	}
	// if it could be t.Fuzz, look for the preceding t.Add
	id, ok := c.path[0].(*ast.Ident)
	if ok && strings.HasPrefix("Fuzz", id.Name) {
		var add *ast.CallExpr
		f := func(n ast.Node) bool {
			if n == nil {
				return true
			}
			call, ok := n.(*ast.CallExpr)
			if !ok {
				return true
			}
			s, ok := call.Fun.(*ast.SelectorExpr)
			if !ok {
				return true
			}
			if s.Sel.Name != "Add" {
				return true
			}
			// Sel.X should be of type *testing.F
			got := c.pkg.TypesInfo().Types[s.X]
			if got.Type.String() == "*testing.F" {
				add = call
			}
			return false // because we're done...
		}
		// look at the enclosing FuzzFoo functions
		if len(c.path) < 2 {
			return false
		}
		n := c.path[len(c.path)-2]
		if _, ok := n.(*ast.FuncDecl); !ok {
			// the path should start with ast.File, ast.FuncDecl, ...
			// but it didn't, so give up
			return false
		}
		ast.Inspect(n, f)
		if add == nil {
			// looks like f.Fuzz without a preceding f.Add.
			// let the regular completion handle it.
			return false
		}

		lbl := "Fuzz(func(t *testing.T"
		for i, a := range add.Args {
			info := c.pkg.TypesInfo().TypeOf(a)
			if info == nil {
				return false // How could this happen, but better safe than panic.
			}
			lbl += fmt.Sprintf(", %c %s", 'a'+i, info)
		}
		lbl += ")"
		xx := CompletionItem{
			Label:         lbl,
			InsertText:    lbl,
			Kind:          protocol.FunctionCompletion,
			Depth:         0,
			Score:         10, // pretty confident the user should see this
			Documentation: "argument types from f.Add",
			isSlice:       false,
		}
		c.items = append(c.items, xx)
		for method := range mset.Methods() {
			o := method.Obj()
			if o.Name() != "Fuzz" {
				cb(candidate{
					obj:         o,
					score:       stdScore,
					imp:         imp,
					addressable: true,
				})
			}
		}
		return true // done
	}
	// let the standard processing take care of it instead
	return false
}
