// Copyright 2017 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 main_test

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"testing"

	"golang.org/x/tools/internal/testenv"
)

type test struct {
	offset, from, to string // specify the arguments
	fileSpecified    bool   // true if the offset or from args specify a specific file
	pkgs             map[string][]string
	wantErr          bool
	wantOut          string              // a substring expected to be in the output
	packages         map[string][]string // a map of the package name to the files contained within, which will be numbered by i.go where i is the index
}

// Test that renaming that would modify cgo files will produce an error and not modify the file.
func TestGeneratedFiles(t *testing.T) {
	testenv.NeedsTool(t, "go")
	testenv.NeedsTool(t, "cgo")

	tmp, bin, cleanup := buildGorename(t)
	defer cleanup()

	srcDir := filepath.Join(tmp, "src")
	err := os.Mkdir(srcDir, os.ModePerm)
	if err != nil {
		t.Fatal(err)
	}

	var env = []string{fmt.Sprintf("GOPATH=%s", tmp)}
	for _, envVar := range os.Environ() {
		if !strings.HasPrefix(envVar, "GOPATH=") {
			env = append(env, envVar)
		}
	}
	// gorename currently requires GOPATH mode.
	env = append(env, "GO111MODULE=off")

	// Testing renaming in packages that include cgo files:
	for iter, renameTest := range []test{
		{
			// Test: variable not used in any cgo file -> no error
			from: `"mytest"::f`, to: "g",
			packages: map[string][]string{
				"mytest": []string{`package mytest; func f() {}`,
					`package mytest
// #include <stdio.h>
import "C"

func z() {C.puts(nil)}`},
			},
			wantErr: false,
			wantOut: "Renamed 1 occurrence in 1 file in 1 package.",
		}, {
			// Test: to name used in cgo file -> rename error
			from: `"mytest"::f`, to: "g",
			packages: map[string][]string{
				"mytest": []string{`package mytest; func f() {}`,
					`package mytest
// #include <stdio.h>
import "C"

func g() {C.puts(nil)}`},
			},
			wantErr: true,
			wantOut: "conflicts with func in same block",
		},
		{
			// Test: from name in package in cgo file -> error
			from: `"mytest"::f`, to: "g",
			packages: map[string][]string{
				"mytest": []string{`package mytest

// #include <stdio.h>
import "C"

func f() { C.puts(nil); }
`},
			},
			wantErr: true,
			wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:",
		}, {
			// Test: from name in cgo file -> error
			from: filepath.Join("mytest", "0.go") + `::f`, to: "g",
			fileSpecified: true,
			packages: map[string][]string{
				"mytest": []string{`package mytest

// #include <stdio.h>
import "C"

func f() { C.puts(nil); }
`},
			},
			wantErr: true,
			wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:",
		}, {
			// Test: offset in cgo file -> identifier in cgo error
			offset: filepath.Join("main", "0.go") + `:#78`, to: "bar",
			fileSpecified: true,
			wantErr:       true,
			packages: map[string][]string{
				"main": {`package main

// #include <unistd.h>
import "C"
import "fmt"

func main() {
	foo := 1
	C.close(2)
	fmt.Println(foo)
}
`},
			},
			wantOut: "cannot rename identifiers in generated file containing DO NOT EDIT marker:",
		}, {
			// Test: from identifier appears in cgo file in another package -> error
			from: `"test"::Foo`, to: "Bar",
			packages: map[string][]string{
				"test": []string{
					`package test

func Foo(x int) (int){
	return x * 2
}
`,
				},
				"main": []string{
					`package main

import "test"
import "fmt"
// #include <unistd.h>
import "C"

func fun() {
	x := test.Foo(3)
	C.close(3)
	fmt.Println(x)
}
`,
				},
			},
			wantErr: true,
			wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:",
		}, {
			// Test: from identifier doesn't appear in cgo file that includes modified package -> rename successful
			from: `"test".Foo::x`, to: "y",
			packages: map[string][]string{
				"test": []string{
					`package test

func Foo(x int) (int){
	return x * 2
}
`,
				},
				"main": []string{
					`package main
import "test"
import "fmt"
// #include <unistd.h>
import "C"

func fun() {
	x := test.Foo(3)
	C.close(3)
	fmt.Println(x)
}
`,
				},
			},
			wantErr: false,
			wantOut: "Renamed 2 occurrences in 1 file in 1 package.",
		}, {
			// Test: from name appears in cgo file in same package -> error
			from: `"mytest"::f`, to: "g",
			packages: map[string][]string{
				"mytest": []string{`package mytest; func f() {}`,
					`package mytest
// #include <stdio.h>
import "C"

func z() {C.puts(nil); f()}`,
					`package mytest
// #include <unistd.h>
import "C"

func foo() {C.close(3); f()}`,
				},
			},
			wantErr: true,
			wantOut: "gorename: refusing to modify generated files containing DO NOT EDIT marker:",
		}, {
			// Test: from name in file, identifier not used in cgo file -> rename successful
			from: filepath.Join("mytest", "0.go") + `::f`, to: "g",
			fileSpecified: true,
			packages: map[string][]string{
				"mytest": []string{`package mytest; func f() {}`,
					`package mytest
// #include <stdio.h>
import "C"

func z() {C.puts(nil)}`},
			},
			wantErr: false,
			wantOut: "Renamed 1 occurrence in 1 file in 1 package.",
		}, {
			// Test: from identifier imported to another package but does not modify cgo file -> rename successful
			from: `"test".Foo`, to: "Bar",
			packages: map[string][]string{
				"test": []string{
					`package test

func Foo(x int) (int){
	return x * 2
}
`,
				},
				"main": []string{
					`package main
// #include <unistd.h>
import "C"

func fun() {
	C.close(3)
}
`,
					`package main
import "test"
import "fmt"
func g() { fmt.Println(test.Foo(3)) }
`,
				},
			},
			wantErr: false,
			wantOut: "Renamed 2 occurrences in 2 files in 2 packages.",
		},
	} {
		// Write the test files
		testCleanup := setUpPackages(t, srcDir, renameTest.packages)

		// Set up arguments
		var args []string

		var arg, val string
		if renameTest.offset != "" {
			arg, val = "-offset", renameTest.offset
		} else {
			arg, val = "-from", renameTest.from
		}

		prefix := fmt.Sprintf("%d: %s %q -to %q", iter, arg, val, renameTest.to)

		if renameTest.fileSpecified {
			// add the src dir to the value of the argument
			val = filepath.Join(srcDir, val)
		}

		args = append(args, arg, val, "-to", renameTest.to)

		// Run command
		cmd := exec.Command(bin, args...)
		cmd.Args[0] = "gorename"
		cmd.Env = env

		// Check the output
		out, err := cmd.CombinedOutput()
		// errors should result in no changes to files
		if err != nil {
			if !renameTest.wantErr {
				t.Errorf("%s: received unexpected error %s", prefix, err)
			}
			// Compare output
			if ok := strings.Contains(string(out), renameTest.wantOut); !ok {
				t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut)
			}
			// Check that no files were modified
			if modified := modifiedFiles(t, srcDir, renameTest.packages); len(modified) != 0 {
				t.Errorf("%s: files unexpectedly modified: %s", prefix, modified)
			}

		} else {
			if !renameTest.wantErr {
				if ok := strings.Contains(string(out), renameTest.wantOut); !ok {
					t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut)
				}
			} else {
				t.Errorf("%s: command succeeded unexpectedly, output: %s", prefix, out)
			}
		}
		testCleanup()
	}
}

// buildGorename builds the gorename executable.
// It returns its path, and a cleanup function.
func buildGorename(t *testing.T) (tmp, bin string, cleanup func()) {
	if runtime.GOOS == "android" {
		t.Skipf("the dependencies are not available on android")
	}

	tmp, err := os.MkdirTemp("", "gorename-regtest-")
	if err != nil {
		t.Fatal(err)
	}

	defer func() {
		if cleanup == nil { // probably, go build failed.
			os.RemoveAll(tmp)
		}
	}()

	bin = filepath.Join(tmp, "gorename")
	if runtime.GOOS == "windows" {
		bin += ".exe"
	}
	cmd := exec.Command("go", "build", "-o", bin)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("Building gorename: %v\n%s", err, out)
	}
	return tmp, bin, func() { os.RemoveAll(tmp) }
}

// setUpPackages sets up the files in a temporary directory provided by arguments.
func setUpPackages(t *testing.T, dir string, packages map[string][]string) (cleanup func()) {
	var pkgDirs []string

	for pkgName, files := range packages {
		// Create a directory for the package.
		pkgDir := filepath.Join(dir, pkgName)
		pkgDirs = append(pkgDirs, pkgDir)

		if err := os.Mkdir(pkgDir, os.ModePerm); err != nil {
			t.Fatal(err)
		}
		// Write the packages files
		for i, val := range files {
			file := filepath.Join(pkgDir, strconv.Itoa(i)+".go")
			if err := os.WriteFile(file, []byte(val), os.ModePerm); err != nil {
				t.Fatal(err)
			}
		}
	}
	return func() {
		for _, dir := range pkgDirs {
			os.RemoveAll(dir)
		}
	}
}

// modifiedFiles returns a list of files that were renamed (without the prefix dir).
func modifiedFiles(t *testing.T, dir string, packages map[string][]string) (results []string) {

	for pkgName, files := range packages {
		pkgDir := filepath.Join(dir, pkgName)

		for i, val := range files {
			file := filepath.Join(pkgDir, strconv.Itoa(i)+".go")
			// read file contents and compare to val
			if contents, err := os.ReadFile(file); err != nil {
				t.Fatalf("File missing: %s", err)
			} else if string(contents) != val {
				results = append(results, strings.TrimPrefix(dir, file))
			}
		}
	}
	return results
}
