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

import (
	"fmt"
	"slices"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/settings"
	. "golang.org/x/tools/gopls/internal/test/integration"
)

// This test exercises the filtering of code actions in generated files.
// Most code actions, being potential edits, are discarded, but
// some (GoTest, GoDoc) are pure queries, and so are allowed.
func TestCodeActionsInGeneratedFiles(t *testing.T) {
	const src = `
-- go.mod --
module example.com
go 1.19

-- src/a.go --
package a

func f() { g() }
func g() {}
-- gen/a.go --
// Code generated by hand; DO NOT EDIT.
package a

func f() { g() }
func g() {}

-- issue72742/a.go --
package main

func main(){
	fmt.Println("helloworld")
}
`

	Run(t, src, func(t *testing.T, env *Env) {
		check := func(filename string, re string, want []protocol.CodeActionKind) {
			env.OpenFile(filename)
			loc := env.RegexpSearch(filename, re)
			actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger)
			if err != nil {
				t.Fatal(err)
			}

			type kinds = []protocol.CodeActionKind
			got := make(kinds, 0)
			for _, act := range actions {
				got = append(got, act.Kind)
			}

			if diff := cmp.Diff(want, got); diff != "" {
				t.Errorf("%s: unexpected CodeActionKinds: (-want +got):\n%s",
					filename, diff)
				t.Log(actions)
			}
		}

		check("src/a.go", `g\(\)`, []protocol.CodeActionKind{
			settings.AddTest,
			settings.GoAssembly,
			settings.GoDoc,
			settings.GoFreeSymbols,
			settings.GoSplitPackage,
			settings.GoToggleCompilerOptDetails,
			settings.RefactorInlineCall,
			settings.GoplsDocFeatures,
		})

		check("gen/a.go", `g\(\)`, []protocol.CodeActionKind{
			settings.GoAssembly,
			settings.GoDoc,
			settings.GoFreeSymbols,
			settings.GoSplitPackage,
			settings.GoToggleCompilerOptDetails,
			settings.GoplsDocFeatures,
		})

		check("issue72742/a.go", `fmt`, []protocol.CodeActionKind{
			settings.OrganizeImports,
			settings.AddTest,
			settings.GoAssembly,
			settings.GoDoc,
			settings.GoFreeSymbols,
			settings.GoSplitPackage,
			settings.GoToggleCompilerOptDetails,
			settings.GoplsDocFeatures,
		})
	})
}

// Test refactor.inline.call is not included in automatically triggered code action
// unless users want refactoring.
//
// (The mechanism behind this behavior has changed. It was added when
// we used to interpret CodeAction(Only=[]) as "all kinds", which was
// a distracting nuisance (too many lightbulbs); this was fixed by
// adding special logic to refactor.inline.call to respect the trigger
// kind; but now we do this for all actions (for similar reasons) and
// interpret Only=[] as Only=[quickfix] unless triggerKind=invoked;
// except that the test client always requests CodeAction(Only=[""]).
// So, we should remove the special logic from refactorInlineCall
// and vary the Only parameter used by the test client.)
func TestVSCodeIssue65167(t *testing.T) {
	const vim1 = `package main

func main() {
	Func()  // range to be selected
}

func Func() int { return 0 }
`

	Run(t, "", func(t *testing.T, env *Env) {
		env.CreateBuffer("main.go", vim1)
		for _, trigger := range []protocol.CodeActionTriggerKind{
			protocol.CodeActionUnknownTrigger,
			protocol.CodeActionInvoked,
			protocol.CodeActionAutomatic,
		} {
			t.Run(fmt.Sprintf("trigger=%v", trigger), func(t *testing.T) {
				for _, selectedRange := range []bool{false, true} {
					t.Run(fmt.Sprintf("range=%t", selectedRange), func(t *testing.T) {
						loc := env.RegexpSearch("main.go", "Func")
						if !selectedRange {
							// assume the cursor is placed at the beginning of `Func`, so end==start.
							loc.Range.End = loc.Range.Start
						}
						actions := env.CodeAction(loc, nil, trigger)
						want := trigger != protocol.CodeActionAutomatic || selectedRange
						if got := slices.ContainsFunc(actions, func(act protocol.CodeAction) bool {
							return act.Kind == settings.RefactorInlineCall
						}); got != want {
							t.Errorf("got refactor.inline.call = %t, want %t", got, want)
						}
					})
				}
			})
		}
	})
}

// TestDescendingRange isn't really a test of CodeAction at all: it
// merely tests the response of the server to any (start, end) range
// that is descending. See #74394.
func TestDescendingRange(t *testing.T) {
	const src = `
-- go.mod --
module example.com
go 1.19

-- a/a.go --
package a
`
	Run(t, src, func(t *testing.T, env *Env) {
		env.OpenFile("a/a.go")
		loc := env.RegexpSearch("a/a.go", "package")
		rng := &loc.Range
		rng.Start, rng.End = rng.End, rng.Start
		_, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger)
		got, wantSubstr := fmt.Sprint(err), "start (offset 7) > end (offset 0)"
		if !strings.Contains(got, wantSubstr) {
			t.Fatalf("CodeAction error: got %q, want substring %q", got, wantSubstr)
		}
	})
}

// TestIssue76235 is a regression test for a crash in CodeAction
// (refactor.extract.variable-all). It can't be a marker test since
// the fixed behavior is a no-op: no code actions, no crash.
func TestIssue76235(t *testing.T) {
	const src = `
-- a/a.go --
package a; func f([3]int)
`
	Run(t, src, func(t *testing.T, env *Env) {
		env.OpenFile("a/a.go")
		loc := env.RegexpSearch("a/a.go", "3")
		env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) // ignore error
	})
}
