// Copyright 2020 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 (
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"golang.org/x/tools/gopls/internal/test/compare"
	. "golang.org/x/tools/gopls/internal/test/integration"
	"golang.org/x/tools/gopls/internal/test/integration/fake"
	"golang.org/x/tools/internal/modindex"

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

// Tests golang/go#38815.
func TestIssue38815(t *testing.T) {
	const needs = `
-- go.mod --
module foo

go 1.12
-- a.go --
package main
func f() {}
`
	const ntest = `package main
func TestZ(t *testing.T) {
	f()
}
`
	const want = `package main

import "testing"

func TestZ(t *testing.T) {
	f()
}
`

	// it was returning
	// "package main\nimport \"testing\"\npackage main..."
	Run(t, needs, func(t *testing.T, env *Env) {
		env.CreateBuffer("a_test.go", ntest)
		env.SaveBuffer("a_test.go")
		got := env.BufferText("a_test.go")
		if want != got {
			t.Errorf("got\n%q, wanted\n%q", got, want)
		}
	})
}

func TestIssue59124(t *testing.T) {
	const stuff = `
-- go.mod --
module foo
go 1.19
-- a.go --
//line foo.y:102
package main

import "fmt"

//this comment is necessary for failure
func _() {
	fmt.Println("hello")
}
`
	Run(t, stuff, func(t *testing.T, env *Env) {
		env.OpenFile("a.go")
		was := env.BufferText("a.go")
		env.AfterChange(NoDiagnostics())
		env.OrganizeImports("a.go")
		is := env.BufferText("a.go")
		if diff := compare.Text(was, is); diff != "" {
			t.Errorf("unexpected diff after organizeImports:\n%s", diff)
		}
	})
}

func TestIssue66407(t *testing.T) {
	const files = `
-- go.mod --
module foo
go 1.21
-- a.go --
package foo

func f(x float64) float64 {
	return x +  rand.Float64()
}
-- b.go --
package foo

func _() {
	_ = rand.Int63()
}
`
	WithOptions(Modes(Default)).
		Run(t, files, func(t *testing.T, env *Env) {
			env.OpenFile("a.go")
			was := env.BufferText("a.go")
			env.OrganizeImports("a.go")
			is := env.BufferText("a.go")
			// expect complaint that module is before 1.22
			env.AfterChange(Diagnostics(ForFile("a.go")))
			diff := compare.Text(was, is)
			// check that it found the 'right' rand
			if !strings.Contains(diff, `import "math/rand/v2"`) {
				t.Errorf("expected rand/v2, got %q", diff)
			}
			env.OpenFile("b.go")
			was = env.BufferText("b.go")
			env.OrganizeImports("b.go")
			// a.go still has its module problem but b.go is fine
			env.AfterChange(Diagnostics(ForFile("a.go")),
				NoDiagnostics(ForFile("b.go")))
			is = env.BufferText("b.go")
			diff = compare.Text(was, is)
			if !strings.Contains(diff, `import "math/rand"`) {
				t.Errorf("expected math/rand, got %q", diff)
			}
		})
}

func TestVim1(t *testing.T) {
	const vim1 = `package main

import "fmt"

var foo = 1
var bar = 2

func main() {
	fmt.Printf("This is a test %v\n", foo)
	fmt.Printf("This is another test %v\n", foo)
	fmt.Printf("This is also a test %v\n", foo)
}
`

	// The file remains unchanged, but if there any quick fixes
	// are returned, they confuse vim (according to CL 233117).
	// Therefore check for no QuickFix CodeActions.
	Run(t, "", func(t *testing.T, env *Env) {
		env.CreateBuffer("main.go", vim1)
		env.OrganizeImports("main.go")

		// Assert no quick fixes.
		for _, act := range env.CodeActionForFile("main.go", nil) {
			if act.Kind == protocol.QuickFix {
				t.Errorf("unexpected quick fix action: %#v", act)
			}
		}
		if t.Failed() {
			got := env.BufferText("main.go")
			if got == vim1 {
				t.Errorf("no changes")
			} else {
				t.Errorf("got\n%q", got)
				t.Errorf("was\n%q", vim1)
			}
		}
	})
}

func TestVim2(t *testing.T) {
	const vim2 = `package main

import (
	"fmt"

	"example.com/blah"

	"rubbish.com/useless"
)

func main() {
	fmt.Println(blah.Name, useless.Name)
}
`

	Run(t, "", func(t *testing.T, env *Env) {
		env.CreateBuffer("main.go", vim2)
		env.OrganizeImports("main.go")

		// Assert no quick fixes.
		for _, act := range env.CodeActionForFile("main.go", nil) {
			if act.Kind == protocol.QuickFix {
				t.Errorf("unexpected quick-fix action: %#v", act)
			}
		}
	})
}

const exampleProxy = `
-- example.com@v1.2.3/go.mod --
module example.com

go 1.12
-- example.com@v1.2.3/x/x.go --
package x

const X = 1
-- example.com@v1.2.3/y/y.go --
package y

const Y = 2
`

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

go 1.12

require example.com v1.2.3
-- main.go --
package main

import "example.com/x"

var _, _ = x.X, y.Y
`
	modcache := t.TempDir()
	defer CleanModCache(t, modcache)

	opts := []RunOption{
		EnvVars{"GOMODCACHE": modcache},
		ProxyFiles(exampleProxy),
		WriteGoSum("."),
	}

	// Force go list to populate GOMODCACHE
	// so OrganizeImports can later rely on it.
	t.Run("setup", func(t *testing.T) {
		WithOptions(opts...).Run(t, files, func(t *testing.T, env *Env) {})
	})

	WithOptions(opts...).Run(t, files, func(t *testing.T, env *Env) {
		// Expect y is undefined.
		env.OpenFile("main.go")
		env.AfterChange(
			Diagnostics(
				env.AtRegexp("main.go", `y.Y`),
				WithMessage("undefined")),
		)

		// Apply suggested fix via OrganizeImports.
		env.SaveBuffer("main.go") // => OrganizeImports
		if true {
			// according to https://github.com/golang/go/issues/77894
			// this test is flaky. Print some possibly helpful diagnostic
			// information.
			ix, err := modindex.Read(modcache)
			if err != nil {
				t.Logf("could not read modcache index: %v", err)
			} else if len(ix.Entries) != 2 {
				t.Logf("%d modcache entries", len(ix.Entries))
				if len(ix.Entries) == 0 {
					fis, err := os.ReadDir(modcache)
					if err != nil {
						t.Logf("could not read modcache dir: %v", err)
					}
					t.Logf("%d modcache files", len(fis))
				}
			}
			// because this test is flaky, replace the simple check with a detailed one
			// env.AfterChange(NoDiagnostics(ForFile("main.go")))
			diags := make(map[string]*protocol.PublishDiagnosticsParams)
			env.AfterChange(ReadAllDiagnostics(&diags))
			dump := false
			for k, v := range diags {
				if len(v.Diagnostics) != 0 {
					t.Errorf("Unexpected diagnostics for %s: %#v", k, v)
					dump = true
				}
			}
			if dump {
				/* the correct result is
					0: example.com/x, example.com@v1.2.3/x, ["X C"]
				   	1: example.com/y, example.com@v1.2.3/y, ["Y C"]
				*/
				for i, e := range ix.Entries {
					t.Logf("%d: %s, %s, %q", i, e.ImportPath, e.Dir, e.Names)
				}
			}
		}

		// Verify that y.Y is defined within the module cache.
		loc := env.FirstDefinition(env.RegexpSearch("main.go", `y.(Y)`))
		path := env.Sandbox.Workdir.URIToPath(loc.URI)
		if !strings.HasPrefix(path, filepath.ToSlash(modcache)) {
			t.Errorf("found module dependency outside of GOMODCACHE: got %v, wanted subdir of %v", path, filepath.ToSlash(modcache))
		}
	})
}

// make sure it gets the v2
/* marker test?

Add proxy data with the special proxy/ prefix (see gopls/internal/test/marker/testdata/quickfix/unusedrequire.txt).
Invoke the organizeImports codeaction directly (see gopls/internal/test/marker/testdata/codeaction/imports.txt, but use the edit=golden named argument instead of result= to minimize the size of the golden output.
*/
func Test58382(t *testing.T) {
	files := `-- main.go --
package main
import "fmt"
func main() {
	fmt.Println(xurls.Relaxed().FindAllString())
}
-- go.mod --
module demo
go 1.20
`
	cache := `-- mvdan.cc/xurls@v2.5.0/xurls.go --
package xurls
func Relaxed() *regexp.Regexp {
return nil
}
--  github.com/mvdan/xurls/v2@v1.1.0/xurls.go --
package xurls
func Relaxed() *regexp.Regexp {
return nil
}
`
	modcache := t.TempDir()
	defer CleanModCache(t, modcache)
	mx := fake.UnpackTxt(cache)

	for k, v := range mx {
		fname := filepath.Join(modcache, k)
		dir := filepath.Dir(fname)
		os.MkdirAll(dir, 0777) // ignore error
		if err := os.WriteFile(fname, v, 0644); err != nil {
			t.Fatal(err)
		}
		if true {
			// for diagnosing flakiness
			t.Logf("wrote %s:%d", fname, len(v))
		}
	}
	WithOptions(
		EnvVars{"GOMODCACHE": modcache},
		WriteGoSum("."),
		NoLogsOnError(),
	).Run(t, files, func(t *testing.T, env *Env) {

		env.OpenFile("main.go")
		env.SaveBuffer("main.go")
		out := env.BufferText("main.go")
		if !strings.Contains(out, "xurls/v2") {
			t.Errorf("did not get v2 in %q", out)
			// golang/go#77552 finds this test flaky, so print out more
			ix, err := modindex.Read(modcache)
			if err != nil {
				t.Fatal(err)
			}
			t.Logf("%d entries", len(ix.Entries))
			for i, e := range ix.Entries {
				t.Logf("%d: %s, %s, %d", i, e.ImportPath, e.Dir, len(e.Names))
			}
		}
	})
}

// get the version requested in the go.mod file, not /v2
func Test61208(t *testing.T) {
	files := `-- main.go --
package main
import "fmt"
func main() {
	fmt.Println(xurls.Relaxed().FindAllString())
}
-- go.mod --
module demo
go 1.20
require github.com/mvdan/xurls v1.1.0
`
	cache := `-- mvdan.cc/xurls/v2@v2.5.0/a/xurls.go --
package xurls
func Relaxed() *regexp.Regexp {
return nil
}
--  github.com/mvdan/xurls@v1.1.0/a/xurls.go --
package xurls
func Relaxed() *regexp.Regexp {
return nil
}
`
	modcache := t.TempDir()
	defer CleanModCache(t, modcache)
	mx := fake.UnpackTxt(cache)
	for k, v := range mx {
		fname := filepath.Join(modcache, k)
		dir := filepath.Dir(fname)
		os.MkdirAll(dir, 0777) // ignore error
		if err := os.WriteFile(fname, v, 0644); err != nil {
			t.Fatal(err)
		}
	}
	WithOptions(
		EnvVars{"GOMODCACHE": modcache},
		WriteGoSum("."),
		NoLogsOnError(),
	).Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("main.go")
		if true {
			// according to https://github.com/golang/go/issues/78680
			// this test is flaky. Print some possibly helpful diagnostic
			// information. Logf is fine as the test will fail later.
			ix, err := modindex.Read(modcache)
			if err != nil {
				t.Logf("could not read modcache index: %v", err)
			} else if len(ix.Entries) != 2 {
				t.Logf("%d modcache entries", len(ix.Entries))
				if len(ix.Entries) == 0 {
					fis, err := os.ReadDir(modcache)
					if err != nil {
						t.Logf("could not read modcache dir: %v", err)
					}
					t.Logf("%d modcache files", len(fis))
				} else { // let's see what it looks like
					for i, e := range ix.Entries {
						t.Logf("ix %d: %s, %s, %q", i, e.ImportPath, e.Dir, e.Names)
					}
				}
			}
			env.SaveBuffer("main.go")
			out := env.BufferText("main.go")
			if !strings.Contains(out, "github.com/mvdan/xurls") {
				/* Perhaps the index is bad? The correct result is:
					0: github.com/mvdan/xurls/a, github.com/mvdan/xurls@v1.1.0/a, ["Relaxed F 1"]
				   1: mvdan.cc/xurls/v2/a, mvdan.cc/xurls/v2@v2.5.0/a, ["Relaxed F 1"]
				*/
				for i, e := range ix.Entries {
					t.Errorf("%d: %s, %s, %q", i, e.ImportPath, e.Dir, e.Names)
				}
			}
		}
		out := env.BufferText("main.go")
		if !strings.Contains(out, "github.com/mvdan/xurls") {
			t.Errorf("did not get github.com/mvdan/xurls in %q", out)
		}
	})
}

// get the version already used in the module
func Test60663(t *testing.T) {
	files := `-- main.go --
package main
import "fmt"
func main() {
	fmt.Println(xurls.Relaxed().FindAllString())
}
-- go.mod --
module demo
go 1.20
-- a.go --
package main
import "github.com/mvdan/xurls"
var _ = xurls.Relaxed()
`
	cache := `-- mvdan.cc/xurls/v2@v2.5.0/xurls.go --
package xurls
func Relaxed() *regexp.Regexp {
return nil
}
--  github.com/mvdan/xurls@v1.1.0/xurls.go --
package xurls
func Relaxed() *regexp.Regexp {
return nil
}
`
	modcache := t.TempDir()
	defer CleanModCache(t, modcache)
	mx := fake.UnpackTxt(cache)
	for k, v := range mx {
		fname := filepath.Join(modcache, k)
		dir := filepath.Dir(fname)
		os.MkdirAll(dir, 0777) // ignore error
		if err := os.WriteFile(fname, v, 0644); err != nil {
			t.Fatal(err)
		}
	}
	WithOptions(
		EnvVars{"GOMODCACHE": modcache},
		WriteGoSum("."),
	).Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("main.go")
		env.SaveBuffer("main.go")
		out := env.BufferText("main.go")
		if !strings.Contains(out, "github.com/mvdan/xurls") {
			t.Errorf("did not get github.com/mvdan/xurls in %q", out)
		}
	})
}

// use the import from a different package in the same module
func Test44510(t *testing.T) {
	const files = `-- go.mod --
module test
go 1.19
-- foo/foo.go --
package main
import strs "strings"
var _ = strs.Count
-- bar/bar.go --
package main
var _ = strs.Builder
`
	WithOptions(
		WriteGoSum("."),
	).Run(t, files, func(T *testing.T, env *Env) {
		env.OpenFile("bar/bar.go")
		env.SaveBuffer("bar/bar.go")
		buf := env.BufferText("bar/bar.go")
		if !strings.Contains(buf, "strs") {
			t.Error(buf)
		}
	})
}

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

go 1.20

require example.com v1.2.3

-- main.go --
package main

import "example.com/x"

var _, _ = x.X, y.Y
`
	modcache := t.TempDir()
	base := filepath.Base(modcache)
	defer CleanModCache(t, modcache)

	// Construct a very unclean module cache whose length exceeds the length of
	// the clean directory path, to reproduce the crash in golang/go#67156
	const sep = string(filepath.Separator)
	modcache += strings.Repeat(sep+".."+sep+base, 10)

	opts := []RunOption{
		EnvVars{"GOMODCACHE": modcache},
		ProxyFiles(exampleProxy),
		WriteGoSum("."),
	}

	t.Run("setup", func(t *testing.T) {
		// Force go list to populate GOMODCACHE.
		WithOptions(opts...).Run(t, files, func(t *testing.T, env *Env) {})

		// Update module index.
		if ix, err := modindex.Update(modcache); err != nil {
			t.Fatalf("failed to obtain updated module index: %v", err)
		} else if len(ix.Entries) != 2 {
			t.Fatalf("got %v, want 2 entries", ix)
		}
	})

	WithOptions(opts...).Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("main.go")
		env.AfterChange(Diagnostics(env.AtRegexp("main.go", `y.Y`)))
		env.SaveBuffer("main.go") // => OrganizeImports
		env.AfterChange(NoDiagnostics(ForFile("main.go")))
	})
}

// Tests golang/go#40685.
func TestAcceptImportsQuickFixTestVariant(t *testing.T) {
	const pkg = `
-- go.mod --
module mod.com

go 1.12
-- a/a.go --
package a

import (
	"fmt"
)

func _() {
	fmt.Println("")
	os.Stat("")
}
-- a/a_test.go --
package a

import (
	"os"
	"testing"
)

func TestA(t *testing.T) {
	os.Stat("")
}
`
	Run(t, pkg, func(t *testing.T, env *Env) {
		env.OpenFile("a/a.go")
		var d protocol.PublishDiagnosticsParams
		env.AfterChange(
			Diagnostics(env.AtRegexp("a/a.go", "os.Stat")),
			ReadDiagnostics("a/a.go", &d),
		)
		env.ApplyQuickFixes("a/a.go", d.Diagnostics)
		env.AfterChange(
			NoDiagnostics(ForFile("a/a.go")),
		)
	})
}

// Test of golang/go#70755
func TestQuickFixIssue70755(t *testing.T) {
	const files = `
-- go.mod --
module mod.com
go 1.19.0 // with go 1.23.0 this fails on some builders
-- bar/bar.go --
package notbar
type NotBar struct {}
-- baz/baz.go --
package baz
type Baz struct {}
-- foo/foo.go --
package foo
type Foo struct {
	bar notbar.NotBar
	baz baz.Baz
}`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("foo/foo.go")
		var d protocol.PublishDiagnosticsParams
		env.AfterChange(ReadDiagnostics("foo/foo.go", &d))
		env.ApplyQuickFixes("foo/foo.go", d.Diagnostics)
		// at this point 'import notbar "mod.com/bar"' has been added
		// but it's still missing the import of "mod.com/baz"
		y := env.BufferText("foo/foo.go")
		if !strings.Contains(y, `notbar "mod.com/bar"`) {
			t.Error("quick fix did not find notbar")
		}
		env.SaveBuffer("foo/foo.go")
		env.AfterChange(NoDiagnostics(ForFile("foo/foo.go")))
	})
}

// Test for golang/go#52784
func TestGoWorkImports(t *testing.T) {
	const pkg = `
-- go.work --
go 1.19

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

go 1.18

require mod.com v0.0.0

replace mod.com => ../mod
-- caller/caller.go --
package main

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

go 1.18
-- mod/a/a.go --
package a

func Test() {
}
`
	Run(t, pkg, func(t *testing.T, env *Env) {
		env.OpenFile("caller/caller.go")
		env.AfterChange(Diagnostics(env.AtRegexp("caller/caller.go", "a.Test")))

		// Saving caller.go should trigger goimports, which should find a.Test in
		// the mod.com module, thanks to the go.work file.
		env.SaveBuffer("caller/caller.go")
		env.AfterChange(NoDiagnostics(ForFile("caller/caller.go")))
	})
}

// prefer the undeprecated alternative 70736
func TestDeprecated70736(t *testing.T) {
	t.Logf("GOOS %s, GARCH %s version %s", runtime.GOOS, runtime.GOARCH, runtime.Version())
	files := `-- main.go --
package main
func main() {
	var v = xurls.Relaxed().FindAllString()
	var w = xurls.A
}
-- go.mod --
module demo
go 1.20
`
	cache := `-- mvdan.cc/xurls/v2@v2.5.0/xurls.go --
package xurls
// Deprecated:
func Relaxed() *regexp.Regexp {
return nil
}
var A int
--  github.com/mvdan/xurls@v1.1.0/xurls.go --
package xurls
func Relaxed() *regexp.Regexp {
return nil
}
var A int
`
	modcache := t.TempDir()
	defer CleanModCache(t, modcache)
	mx := fake.UnpackTxt(cache)
	for k, v := range mx {
		fname := filepath.Join(modcache, k)
		dir := filepath.Dir(fname)
		os.MkdirAll(dir, 0777) // ignore error
		if err := os.WriteFile(fname, v, 0644); err != nil {
			t.Fatal(err)
		}
	}
	WithOptions(
		EnvVars{"GOMODCACHE": modcache},
		WriteGoSum("."),
	).Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("main.go")
		env.SaveBuffer("main.go")
		out := env.BufferText("main.go")
		if strings.Contains(out, "xurls/v2") {
			t.Errorf("chose deprecated v2 in %q", out)
		}
	})
}

// Find the non-test package asked for in a test
func TestTestImports(t *testing.T) {
	const pkg = `
-- go.work --
go 1.19

use (
        ./caller
        ./mod
		./xxx
)
-- caller/go.mod --
module caller.com

go 1.18

require mod.com v0.0.0
require xxx.com v0.0.0

replace mod.com => ../mod
replace xxx.com => ../xxx
-- caller/caller_test.go --
package main

var _ = a.Test
-- xxx/go.mod --
module xxx.com

go 1.18
-- xxx/a/a_test.go --
package a

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

go 1.18
-- mod/a/a.go --
package a

func Test() {
}
`
	WithOptions(Modes(Default)).Run(t, pkg, func(t *testing.T, env *Env) {
		env.OpenFile("caller/caller_test.go")
		env.AfterChange(Diagnostics(env.AtRegexp("caller/caller_test.go", "a.Test")))

		// Saving caller_test.go should trigger goimports, which should find a.Test in
		// the mod.com module, thanks to the go.work file.
		env.SaveBuffer("caller/caller_test.go")
		env.AfterChange(NoDiagnostics(ForFile("caller/caller_test.go")))
		buf := env.BufferText("caller/caller_test.go")
		if !strings.Contains(buf, "mod.com/a") {
			t.Errorf("got %q, expected a mod.com/a", buf)
		}
	})
}

// this test replaces 'package bar' with 'package foo'
// saves the file, and then looks for the import in the main package.s
func Test67973(t *testing.T) {
	const files = `-- go.mod --
module hello
go 1.19
-- hello.go --
package main
var _ = foo.Bar
-- internal/foo/foo.go --
package bar
func Bar() {}
`
	Run(t, files, func(t *testing.T, env *Env) {
		env.OpenFile("hello.go")
		env.AfterChange(env.DoneWithOpen())
		env.SaveBuffer("hello.go")
		env.OpenFile("internal/foo/foo.go")
		env.RegexpReplace("internal/foo/foo.go", "bar", "foo")
		env.SaveBuffer("internal/foo/foo.go")
		env.SaveBuffer("hello.go")
		buf := env.BufferText("hello.go")
		if !strings.Contains(buf, "internal/foo") {
			t.Errorf(`expected import "hello/internal/foo" but got %q`, buf)
		}
	})
}
