// Copyright 2023 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 workspace

import (
	"fmt"
	"strings"
	"testing"

	"golang.org/x/tools/gopls/internal/lsp/protocol"
	"golang.org/x/tools/gopls/internal/lsp/tests/compare"
	"golang.org/x/tools/internal/testenv"

	. "golang.org/x/tools/gopls/internal/lsp/regtest"
)

func TestQuickFix_UseModule(t *testing.T) {
	testenv.NeedsGo1Point(t, 18) // needs go.work

	const files = `
-- go.work --
go 1.20

use (
	./a
)
-- a/go.mod --
module mod.com/a

go 1.18

-- a/main.go --
package main

import "mod.com/a/lib"

func main() {
	_ = lib.C
}

-- a/lib/lib.go --
package lib

const C = "b"
-- b/go.mod --
module mod.com/b

go 1.18

-- b/main.go --
package main

import "mod.com/b/lib"

func main() {
	_ = lib.C
}

-- b/lib/lib.go --
package lib

const C = "b"
`

	for _, title := range []string{
		"Use this module",
		"Use all modules",
	} {
		t.Run(title, func(t *testing.T) {
			Run(t, files, func(t *testing.T, env *Env) {
				env.OpenFile("b/main.go")
				var d protocol.PublishDiagnosticsParams
				env.AfterChange(ReadDiagnostics("b/main.go", &d))
				fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
				var toApply []protocol.CodeAction
				for _, fix := range fixes {
					if strings.Contains(fix.Title, title) {
						toApply = append(toApply, fix)
					}
				}
				if len(toApply) != 1 {
					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
				}
				env.ApplyCodeAction(toApply[0])
				env.AfterChange(NoDiagnostics())
				want := `go 1.20

use (
	./a
	./b
)
`
				got := env.ReadWorkspaceFile("go.work")
				if diff := compare.Text(want, got); diff != "" {
					t.Errorf("unexpeced go.work content:\n%s", diff)
				}
			})
		})
	}
}

func TestQuickFix_AddGoWork(t *testing.T) {
	testenv.NeedsGo1Point(t, 18) // needs go.work

	v := goVersion(t)
	const files = `
-- a/go.mod --
module mod.com/a

go 1.18

-- a/main.go --
package main

import "mod.com/a/lib"

func main() {
	_ = lib.C
}

-- a/lib/lib.go --
package lib

const C = "b"
-- b/go.mod --
module mod.com/b

go 1.18

-- b/main.go --
package main

import "mod.com/b/lib"

func main() {
	_ = lib.C
}

-- b/lib/lib.go --
package lib

const C = "b"
`

	tests := []struct {
		name  string
		file  string
		title string
		want  string
	}{
		{
			"use b",
			"b/main.go",
			"Add a go.work file using this module",
			fmt.Sprintf(`go 1.%d

use ./b
`, v),
		},
		{
			"use a",
			"a/main.go",
			"Add a go.work file using this module",
			fmt.Sprintf(`go 1.%d

use ./a
`, v),
		},
		{
			"use all",
			"a/main.go",
			"Add a go.work file using all modules",
			fmt.Sprintf(`go 1.%d

use (
	./a
	./b
)
`, v),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			Run(t, files, func(t *testing.T, env *Env) {
				env.OpenFile(test.file)
				var d protocol.PublishDiagnosticsParams
				env.AfterChange(ReadDiagnostics(test.file, &d))
				fixes := env.GetQuickFixes(test.file, d.Diagnostics)
				var toApply []protocol.CodeAction
				for _, fix := range fixes {
					if strings.Contains(fix.Title, test.title) {
						toApply = append(toApply, fix)
					}
				}
				if len(toApply) != 1 {
					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), test.title, toApply)
				}
				env.ApplyCodeAction(toApply[0])
				env.AfterChange(
					NoDiagnostics(ForFile(test.file)),
				)

				got := env.ReadWorkspaceFile("go.work")
				if diff := compare.Text(test.want, got); diff != "" {
					t.Errorf("unexpected go.work content:\n%s", diff)
				}
			})
		})
	}
}

func TestQuickFix_UnsavedGoWork(t *testing.T) {
	testenv.NeedsGo1Point(t, 18) // needs go.work

	const files = `
-- go.work --
go 1.21

use (
	./a
)
-- a/go.mod --
module mod.com/a

go 1.18

-- a/main.go --
package main

func main() {}
-- b/go.mod --
module mod.com/b

go 1.18

-- b/main.go --
package main

func main() {}
`

	for _, title := range []string{
		"Use this module",
		"Use all modules",
	} {
		t.Run(title, func(t *testing.T) {
			Run(t, files, func(t *testing.T, env *Env) {
				env.OpenFile("go.work")
				env.OpenFile("b/main.go")
				env.RegexpReplace("go.work", "go 1.21", "go 1.21 // arbitrary comment")
				var d protocol.PublishDiagnosticsParams
				env.AfterChange(ReadDiagnostics("b/main.go", &d))
				fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
				var toApply []protocol.CodeAction
				for _, fix := range fixes {
					if strings.Contains(fix.Title, title) {
						toApply = append(toApply, fix)
					}
				}
				if len(toApply) != 1 {
					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
				}
				fix := toApply[0]
				err := env.Editor.ApplyCodeAction(env.Ctx, fix)
				if err == nil {
					t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title)
				}

				if got := err.Error(); !strings.Contains(got, "must save") {
					t.Errorf("codeAction(%q) returned error %q, want containing \"must save\"", fix.Title, err)
				}
			})
		})
	}
}

func TestQuickFix_GOWORKOff(t *testing.T) {
	testenv.NeedsGo1Point(t, 18) // needs go.work

	const files = `
-- go.work --
go 1.21

use (
	./a
)
-- a/go.mod --
module mod.com/a

go 1.18

-- a/main.go --
package main

func main() {}
-- b/go.mod --
module mod.com/b

go 1.18

-- b/main.go --
package main

func main() {}
`

	for _, title := range []string{
		"Use this module",
		"Use all modules",
	} {
		t.Run(title, func(t *testing.T) {
			WithOptions(
				EnvVars{"GOWORK": "off"},
			).Run(t, files, func(t *testing.T, env *Env) {
				env.OpenFile("go.work")
				env.OpenFile("b/main.go")
				var d protocol.PublishDiagnosticsParams
				env.AfterChange(ReadDiagnostics("b/main.go", &d))
				fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
				var toApply []protocol.CodeAction
				for _, fix := range fixes {
					if strings.Contains(fix.Title, title) {
						toApply = append(toApply, fix)
					}
				}
				if len(toApply) != 1 {
					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
				}
				fix := toApply[0]
				err := env.Editor.ApplyCodeAction(env.Ctx, fix)
				if err == nil {
					t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title)
				}

				if got := err.Error(); !strings.Contains(got, "GOWORK=off") {
					t.Errorf("codeAction(%q) returned error %q, want containing \"GOWORK=off\"", fix.Title, err)
				}
			})
		})
	}
}
