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

package misc

import (
	"fmt"
	"strings"
	"testing"

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

func TestPrepareRenameMainPackage(t *testing.T) {
	const files = `
-- go.mod --
module mod.com

go 1.18
-- main.go --
package main

import (
	"fmt"
)

func main() {
	fmt.Println(1)
}
`
	const wantErr = "can't rename package \"main\""
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("main.go")
		loc := env.RegexpSearch("main.go", `main`)
		params := &protocol.PrepareRenameParams{
			TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc),
		}
		_, err := env.Editor.Server.PrepareRename(env.Ctx, params)
		if err == nil {
			t.Errorf("missing can't rename package main error from PrepareRename")
		}

		if err.Error() != wantErr {
			t.Errorf("got %v, want %v", err.Error(), wantErr)
		}
	})
}

// Test case for golang/go#56227
func TestRenameWithUnsafeSlice(t *testing.T) {
	testenv.NeedsGo1Point(t, 17) // unsafe.Slice was added in Go 1.17
	const files = `
-- go.mod --
module mod.com

go 1.18
-- p.go --
package p

import "unsafe"

type T struct{}

func (T) M() {}

func _() {
	x := [3]int{1, 2, 3}
	ptr := unsafe.Pointer(&x)
	_ = unsafe.Slice((*int)(ptr), 3)
}
`

	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("p.go")
		env.Rename(env.RegexpSearch("p.go", "M"), "N") // must not panic
	})
}

func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) {
	const files = `
go 1.14
-- lib/a.go --
import "fmt"

const A = 1

func bar() {
	fmt.Println("Bar")
}

-- main.go --
package main

import "fmt"

func main() {
	fmt.Println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		err := env.Editor.Rename(env.Ctx, env.RegexpSearch("lib/a.go", "fmt"), "fmt1")
		if got, want := fmt.Sprint(err), "no identifier found"; got != want {
			t.Errorf("Rename: got error %v, want %v", got, want)
		}
	})
}

func TestPrepareRenameFailWithUnknownModule(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
go 1.14
-- lib/a.go --
package lib

const A = 1

-- main.go --
package main

import (
	"mod.com/lib"
)

func main() {
	println("Hello")
}
`
	const wantErr = "can't rename package: missing module information for package"
	Run(t, files, func(t *testing.T, env *Env) {
		loc := env.RegexpSearch("lib/a.go", "lib")
		params := &protocol.PrepareRenameParams{
			TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc),
		}
		_, err := env.Editor.Server.PrepareRename(env.Ctx, params)
		if err == nil || !strings.Contains(err.Error(), wantErr) {
			t.Errorf("missing cannot rename packages with unknown module from PrepareRename")
		}
	})
}

// This test ensures that each import of a renamed package
// is also renamed if it would otherwise create a conflict.
func TestRenamePackageWithConflicts(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

const A = 1

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

const B = 1

-- lib/x/a.go --
package nested1

const C = 1

-- main.go --
package main

import (
	"mod.com/lib"
	"mod.com/lib/nested"
	nested1 "mod.com/lib/x"
)

func main() {
	println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")

		// Check if the new package name exists.
		env.RegexpSearch("nested/a.go", "package nested")
		env.RegexpSearch("main.go", `nested2 "mod.com/nested"`)
		env.RegexpSearch("main.go", "mod.com/nested/nested")
		env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`)
	})
}

func TestRenamePackageWithAlias(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

const A = 1

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

const B = 1

-- main.go --
package main

import (
	"mod.com/lib"
	lib1 "mod.com/lib/nested"
)

func main() {
	println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")

		// Check if the new package name exists.
		env.RegexpSearch("nested/a.go", "package nested")
		env.RegexpSearch("main.go", "mod.com/nested")
		env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`)
	})
}

func TestRenamePackageWithDifferentDirectoryPath(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

const A = 1

-- lib/nested/a.go --
package foo

const B = 1

-- main.go --
package main

import (
	"mod.com/lib"
	foo "mod.com/lib/nested"
)

func main() {
	println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")

		// Check if the new package name exists.
		env.RegexpSearch("nested/a.go", "package nested")
		env.RegexpSearch("main.go", "mod.com/nested")
		env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`)
	})
}

func TestRenamePackage(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

const A = 1

-- lib/b.go --
package lib

const B = 1

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

const C = 1

-- main.go --
package main

import (
	"mod.com/lib"
	"mod.com/lib/nested"
)

func main() {
	println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1")

		// Check if the new package name exists.
		env.RegexpSearch("lib1/a.go", "package lib1")
		env.RegexpSearch("lib1/b.go", "package lib1")
		env.RegexpSearch("main.go", "mod.com/lib1")
		env.RegexpSearch("main.go", "mod.com/lib1/nested")
	})
}

// Test for golang/go#47564.
func TestRenameInTestVariant(t *testing.T) {
	const files = `
-- go.mod --
module mod.com

go 1.12
-- stringutil/stringutil.go --
package stringutil

func Identity(s string) string {
	return s
}
-- stringutil/stringutil_test.go --
package stringutil

func TestIdentity(t *testing.T) {
	if got := Identity("foo"); got != "foo" {
		t.Errorf("bad")
	}
}
-- main.go --
package main

import (
	"fmt"

	"mod.com/stringutil"
)

func main() {
	fmt.Println(stringutil.Identity("hello world"))
}
`

	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("main.go")
		env.Rename(env.RegexpSearch("main.go", `stringutil\.(Identity)`), "Identityx")
		env.OpenFile("stringutil/stringutil_test.go")
		text := env.BufferText("stringutil/stringutil_test.go")
		if !strings.Contains(text, "Identityx") {
			t.Errorf("stringutil/stringutil_test.go: missing expected token `Identityx` after rename:\n%s", text)
		}
	})
}

// This is a test that rename operation initiated by the editor function as expected.
func TestRenameFileFromEditor(t *testing.T) {
	const files = `
-- go.mod --
module mod.com

go 1.16
-- a/a.go --
package a

const X = 1
-- a/x.go --
package a

const X = 2
-- b/b.go --
package b
`

	Run(t, files, func(t *testing.T, env *Env) {
		// Rename files and verify that diagnostics are affected accordingly.

		// Initially, we should have diagnostics on both X's, for their duplicate declaration.
		env.OnceMet(
			InitialWorkspaceLoad,
			Diagnostics(env.AtRegexp("a/a.go", "X")),
			Diagnostics(env.AtRegexp("a/x.go", "X")),
		)

		// Moving x.go should make the diagnostic go away.
		env.RenameFile("a/x.go", "b/x.go")
		env.AfterChange(
			NoDiagnostics(ForFile("a/a.go")),               // no more duplicate declarations
			Diagnostics(env.AtRegexp("b/b.go", "package")), // as package names mismatch
		)

		// Renaming should also work on open buffers.
		env.OpenFile("b/x.go")

		// Moving x.go back to a/ should cause the diagnostics to reappear.
		env.RenameFile("b/x.go", "a/x.go")
		env.AfterChange(
			Diagnostics(env.AtRegexp("a/a.go", "X")),
			Diagnostics(env.AtRegexp("a/x.go", "X")),
		)

		// Renaming the entire directory should move both the open and closed file.
		env.RenameFile("a", "x")
		env.AfterChange(
			Diagnostics(env.AtRegexp("x/a.go", "X")),
			Diagnostics(env.AtRegexp("x/x.go", "X")),
		)

		// As a sanity check, verify that x/x.go is open.
		if text := env.BufferText("x/x.go"); text == "" {
			t.Fatal("got empty buffer for x/x.go")
		}
	})
}

func TestRenamePackage_Tests(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

const A = 1

-- lib/b.go --
package lib

const B = 1

-- lib/a_test.go --
package lib_test

import (
	"mod.com/lib"
	"fmt
)

const C = 1

-- lib/b_test.go --
package lib

import (
	"fmt
)

const D = 1

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

const D = 1

-- main.go --
package main

import (
	"mod.com/lib"
	"mod.com/lib/nested"
)

func main() {
	println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1")

		// Check if the new package name exists.
		env.RegexpSearch("lib1/a.go", "package lib1")
		env.RegexpSearch("lib1/b.go", "package lib1")
		env.RegexpSearch("main.go", "mod.com/lib1")
		env.RegexpSearch("main.go", "mod.com/lib1/nested")

		// Check if the test package is renamed
		env.RegexpSearch("lib1/a_test.go", "package lib1_test")
		env.RegexpSearch("lib1/b_test.go", "package lib1")
	})
}

func TestRenamePackage_NestedModule(t *testing.T) {
	testenv.NeedsGo1Point(t, 18)
	const files = `
-- go.work --
go 1.18
use (
	.
	./foo/bar
	./foo/baz
)

-- go.mod --
module mod.com

go 1.18

require (
    mod.com/foo/bar v0.0.0
)

replace (
	mod.com/foo/bar => ./foo/bar
	mod.com/foo/baz => ./foo/baz
)
-- foo/foo.go --
package foo

import "fmt"

func Bar() {
	fmt.Println("In foo before renamed to foox.")
}

-- foo/bar/go.mod --
module mod.com/foo/bar

-- foo/bar/bar.go --
package bar

const Msg = "Hi from package bar"

-- foo/baz/go.mod --
module mod.com/foo/baz

-- foo/baz/baz.go --
package baz

const Msg = "Hi from package baz"

-- main.go --
package main

import (
	"fmt"
	"mod.com/foo/bar"
	"mod.com/foo/baz"
	"mod.com/foo"
)

func main() {
	foo.Bar()
	fmt.Println(bar.Msg)
	fmt.Println(baz.Msg)
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("foo/foo.go")
		env.Rename(env.RegexpSearch("foo/foo.go", "foo"), "foox")

		env.RegexpSearch("foox/foo.go", "package foox")
		env.OpenFile("foox/bar/bar.go")
		env.OpenFile("foox/bar/go.mod")

		env.RegexpSearch("main.go", "mod.com/foo/bar")
		env.RegexpSearch("main.go", "mod.com/foox")
		env.RegexpSearch("main.go", "foox.Bar()")

		env.RegexpSearch("go.mod", "./foox/bar")
		env.RegexpSearch("go.mod", "./foox/baz")
	})
}

func TestRenamePackage_DuplicateImport(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

const A = 1

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

const B = 1

-- main.go --
package main

import (
	"mod.com/lib"
	lib1 "mod.com/lib"
	lib2 "mod.com/lib/nested"
)

func main() {
	println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")

		// Check if the new package name exists.
		env.RegexpSearch("nested/a.go", "package nested")
		env.RegexpSearch("main.go", "mod.com/nested")
		env.RegexpSearch("main.go", `lib1 "mod.com/nested"`)
		env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`)
	})
}

func TestRenamePackage_DuplicateBlankImport(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

const A = 1

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

const B = 1

-- main.go --
package main

import (
	"mod.com/lib"
	_ "mod.com/lib"
	lib1 "mod.com/lib/nested"
)

func main() {
	println("Hello")
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested")

		// Check if the new package name exists.
		env.RegexpSearch("nested/a.go", "package nested")
		env.RegexpSearch("main.go", "mod.com/nested")
		env.RegexpSearch("main.go", `_ "mod.com/nested"`)
		env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`)
	})
}

func TestRenamePackage_TestVariant(t *testing.T) {
	const files = `
-- go.mod --
module mod.com

go 1.12
-- foo/foo.go --
package foo

const Foo = 42
-- bar/bar.go --
package bar

import "mod.com/foo"

const Bar = foo.Foo
-- bar/bar_test.go --
package bar

import "mod.com/foo"

const Baz = foo.Foo
-- testdata/bar/bar.go --
package bar

import "mod.com/foox"

const Bar = foox.Foo
-- testdata/bar/bar_test.go --
package bar

import "mod.com/foox"

const Baz = foox.Foo
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("foo/foo.go")
		env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox")

		checkTestdata(t, env)
	})
}

func TestRenamePackage_IntermediateTestVariant(t *testing.T) {
	// In this test set up, we have the following import edges:
	//   bar_test -> baz -> foo -> bar
	//   bar_test -> foo -> bar
	//   bar_test -> bar
	//
	// As a consequence, bar_x_test.go is in the reverse closure of both
	// `foo [bar.test]` and `baz [bar.test]`. This test confirms that we don't
	// produce duplicate edits in this case.
	const files = `
-- go.mod --
module foo.mod

go 1.12
-- foo/foo.go --
package foo

import "foo.mod/bar"

const Foo = 42

const _ = bar.Bar
-- baz/baz.go --
package baz

import "foo.mod/foo"

const Baz = foo.Foo
-- bar/bar.go --
package bar

var Bar = 123
-- bar/bar_test.go --
package bar

const _ = Bar
-- bar/bar_x_test.go --
package bar_test

import (
	"foo.mod/bar"
	"foo.mod/baz"
	"foo.mod/foo"
)

const _ = bar.Bar + baz.Baz + foo.Foo
-- testdata/foox/foo.go --
package foox

import "foo.mod/bar"

const Foo = 42

const _ = bar.Bar
-- testdata/baz/baz.go --
package baz

import "foo.mod/foox"

const Baz = foox.Foo
-- testdata/bar/bar_x_test.go --
package bar_test

import (
	"foo.mod/bar"
	"foo.mod/baz"
	"foo.mod/foox"
)

const _ = bar.Bar + baz.Baz + foox.Foo
`

	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("foo/foo.go")
		env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox")

		checkTestdata(t, env)
	})
}

func TestRenamePackage_Nesting(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

import "mod.com/lib/nested"

const A = 1 + nested.B
-- lib/nested/a.go --
package nested

const B = 1
-- other/other.go --
package other

import (
	"mod.com/lib"
	"mod.com/lib/nested"
)

const C = lib.A + nested.B
-- testdata/libx/a.go --
package libx

import "mod.com/libx/nested"

const A = 1 + nested.B
-- testdata/other/other.go --
package other

import (
	"mod.com/libx"
	"mod.com/libx/nested"
)

const C = libx.A + nested.B
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "package (lib)"), "libx")

		checkTestdata(t, env)
	})
}

func TestRenamePackage_InvalidName(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

import "mod.com/lib/nested"

const A = 1 + nested.B
`

	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/a.go")
		loc := env.RegexpSearch("lib/a.go", "package (lib)")

		for _, badName := range []string{"$$$", "lib_test"} {
			if err := env.Editor.Rename(env.Ctx, loc, badName); err == nil {
				t.Errorf("Rename(lib, libx) succeeded, want non-nil error")
			}
		}
	})
}

func TestRenamePackage_InternalPackage(t *testing.T) {
	testenv.NeedsGo1Point(t, 17)
	const files = `
-- go.mod --
module mod.com

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

import (
	"fmt"
	"mod.com/lib/internal/x"
)

const A = 1

func print() {
	fmt.Println(x.B)
}

-- lib/internal/x/a.go --
package x

const B = 1

-- main.go --
package main

import "mod.com/lib"

func main() {
	lib.print()
}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("lib/internal/x/a.go")
		env.Rename(env.RegexpSearch("lib/internal/x/a.go", "x"), "utils")

		// Check if the new package name exists.
		env.RegexpSearch("lib/a.go", "mod.com/lib/internal/utils")
		env.RegexpSearch("lib/a.go", "utils.B")

		// Check if the test package is renamed
		env.RegexpSearch("lib/internal/utils/a.go", "package utils")

		env.OpenFile("lib/a.go")
		env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1")

		// Check if the new package name exists.
		env.RegexpSearch("lib1/a.go", "package lib1")
		env.RegexpSearch("lib1/a.go", "mod.com/lib1/internal/utils")
		env.RegexpSearch("main.go", `import "mod.com/lib1"`)
		env.RegexpSearch("main.go", "lib1.print()")
	})
}

// checkTestdata checks that current buffer contents match their corresponding
// expected content in the testdata directory.
func checkTestdata(t *testing.T, env *Env) {
	t.Helper()
	files := env.ListFiles("testdata")
	if len(files) == 0 {
		t.Fatal("no files in testdata directory")
	}
	for _, file := range files {
		suffix := strings.TrimPrefix(file, "testdata/")
		got := env.BufferText(suffix)
		want := env.ReadWorkspaceFile(file)
		if diff := compare.Text(want, got); diff != "" {
			t.Errorf("Rename: unexpected buffer content for %s (-want +got):\n%s", suffix, diff)
		}
	}
}
