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

// No testdata on Android.

//go:build !android

package main

import (
	"bytes"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

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

// TODO(adonovan):
// - test introduction of renaming imports.
// - test induced failures of rewriteFile.

// Guide to the test packages:
//
// new.com/one		-- canonical name for old.com/one
// old.com/one		-- non-canonical; has import comment "new.com/one"
// old.com/bad		-- has a parse error
// fruit.io/orange	\
// fruit.io/banana	 } orange -> pear -> banana -> titanic.biz/bar
// fruit.io/pear	/
// titanic.biz/bar	-- domain is sinking; package has jumped ship to new.com/bar
// titanic.biz/foo	-- domain is sinking but package has no import comment yet

var gopath = filepath.Join(cwd, "testdata")

func init() {
	if err := os.Setenv("GOPATH", gopath); err != nil {
		log.Fatal(err)
	}

	// This test currently requires GOPATH mode.
	// Explicitly disabling module mode should suffix, but
	// we'll also turn off GOPROXY just for good measure.
	if err := os.Setenv("GO111MODULE", "off"); err != nil {
		log.Fatal(err)
	}
	if err := os.Setenv("GOPROXY", "off"); err != nil {
		log.Fatal(err)
	}
}

func TestFixImports(t *testing.T) {
	if os.Getenv("GO_BUILDER_NAME") == "plan9-arm" {
		t.Skipf("skipping test that times out on plan9-arm; see https://go.dev/issue/50775")
	}
	testenv.NeedsTool(t, "go")

	defer func() {
		stderr = os.Stderr
		*badDomains = "code.google.com"
		*replaceFlag = ""
	}()

	for i, test := range []struct {
		packages    []string // packages to rewrite, "go list" syntax
		badDomains  string   // -baddomains flag
		replaceFlag string   // -replace flag
		wantOK      bool
		wantStderr  string
		wantRewrite map[string]string
	}{
		// #0. No errors.
		{
			packages:   []string{"all"},
			badDomains: "code.google.com",
			wantOK:     true,
			wantStderr: `
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
fruit.io/banana
	fixed: old.com/one -> new.com/one
	fixed: titanic.biz/bar -> new.com/bar
`,
			wantRewrite: map[string]string{
				"$GOPATH/src/fruit.io/banana/banana.go": `package banana

import (
	_ "new.com/bar"
	_ "new.com/one"
	_ "titanic.biz/foo"
)`,
			},
		},
		// #1. No packages needed rewriting.
		{
			packages:   []string{"titanic.biz/...", "old.com/...", "new.com/..."},
			badDomains: "code.google.com",
			wantOK:     true,
			wantStderr: `
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
`,
		},
		// #2. Some packages without import comments matched bad domains.
		{
			packages:   []string{"all"},
			badDomains: "titanic.biz",
			wantOK:     false,
			wantStderr: `
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
fruit.io/banana
	testdata/src/fruit.io/banana/banana.go:6: import "titanic.biz/foo"
	fixed: old.com/one -> new.com/one
	fixed: titanic.biz/bar -> new.com/bar
	ERROR: titanic.biz/foo has no import comment
	imported directly by:
		fruit.io/pear
	imported indirectly by:
		fruit.io/orange
`,
			wantRewrite: map[string]string{
				"$GOPATH/src/fruit.io/banana/banana.go": `package banana

import (
	_ "new.com/bar"
	_ "new.com/one"
	_ "titanic.biz/foo"
)`,
			},
		},
		// #3. The -replace flag lets user supply missing import comments.
		{
			packages:    []string{"all"},
			replaceFlag: "titanic.biz/foo=new.com/foo",
			wantOK:      true,
			wantStderr: `
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
fruit.io/banana
	fixed: old.com/one -> new.com/one
	fixed: titanic.biz/bar -> new.com/bar
	fixed: titanic.biz/foo -> new.com/foo
`,
			wantRewrite: map[string]string{
				"$GOPATH/src/fruit.io/banana/banana.go": `package banana

import (
	_ "new.com/bar"
	_ "new.com/foo"
	_ "new.com/one"
)`,
			},
		},
		// #4. The -replace flag supports wildcards.
		//     An explicit import comment takes precedence.
		{
			packages:    []string{"all"},
			replaceFlag: "titanic.biz/...=new.com/...",
			wantOK:      true,
			wantStderr: `
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
fruit.io/banana
	fixed: old.com/one -> new.com/one
	fixed: titanic.biz/bar -> new.com/bar
	fixed: titanic.biz/foo -> new.com/foo
`,
			wantRewrite: map[string]string{
				"$GOPATH/src/fruit.io/banana/banana.go": `package banana

import (
	_ "new.com/bar"
	_ "new.com/foo"
	_ "new.com/one"
)`,
			},
		},
		// #5. The -replace flag trumps -baddomains.
		{
			packages:    []string{"all"},
			badDomains:  "titanic.biz",
			replaceFlag: "titanic.biz/foo=new.com/foo",
			wantOK:      true,
			wantStderr: `
testdata/src/old.com/bad/bad.go:2:43: expected 'package', found 'EOF'
fruit.io/banana
	fixed: old.com/one -> new.com/one
	fixed: titanic.biz/bar -> new.com/bar
	fixed: titanic.biz/foo -> new.com/foo
`,
			wantRewrite: map[string]string{
				"$GOPATH/src/fruit.io/banana/banana.go": `package banana

import (
	_ "new.com/bar"
	_ "new.com/foo"
	_ "new.com/one"
)`,
			},
		},
	} {
		*badDomains = test.badDomains
		*replaceFlag = test.replaceFlag

		stderr = new(bytes.Buffer)
		gotRewrite := make(map[string]string)
		writeFile = func(filename string, content []byte, mode os.FileMode) error {
			filename = strings.Replace(filename, gopath, "$GOPATH", 1)
			filename = filepath.ToSlash(filename)
			gotRewrite[filename] = string(bytes.TrimSpace(content))
			return nil
		}

		if runtime.GOOS == "windows" {
			test.wantStderr = strings.Replace(test.wantStderr, `testdata/src/old.com/bad/bad.go`, `testdata\src\old.com\bad\bad.go`, -1)
			test.wantStderr = strings.Replace(test.wantStderr, `testdata/src/fruit.io/banana/banana.go`, `testdata\src\fruit.io\banana\banana.go`, -1)
		}
		test.wantStderr = strings.TrimSpace(test.wantStderr)

		// Check status code.
		if fiximports(test.packages...) != test.wantOK {
			t.Errorf("#%d. fiximports() = %t", i, !test.wantOK)
		}

		// Compare stderr output.
		if got := strings.TrimSpace(stderr.(*bytes.Buffer).String()); got != test.wantStderr {
			if strings.Contains(got, "vendor/golang_org/x/text/unicode/norm") {
				t.Skip("skipping known-broken test; see golang.org/issue/17417")
			}
			t.Errorf("#%d. stderr: got <<\n%s\n>>, want <<\n%s\n>>",
				i, got, test.wantStderr)
		}

		// Compare rewrites.
		for k, v := range gotRewrite {
			if test.wantRewrite[k] != v {
				t.Errorf("#%d. rewrite[%s] = <<%s>>, want <<%s>>",
					i, k, v, test.wantRewrite[k])
			}
			delete(test.wantRewrite, k)
		}
		for k, v := range test.wantRewrite {
			t.Errorf("#%d. rewrite[%s] missing, want <<%s>>", i, k, v)
		}
	}
}

// TestDryRun tests that the -n flag suppresses calls to writeFile.
func TestDryRun(t *testing.T) {
	if os.Getenv("GO_BUILDER_NAME") == "plan9-arm" {
		t.Skipf("skipping test that times out on plan9-arm; see https://go.dev/issue/50775")
	}
	testenv.NeedsTool(t, "go")

	*dryrun = true
	defer func() { *dryrun = false }() // restore
	stderr = new(bytes.Buffer)
	writeFile = func(filename string, content []byte, mode os.FileMode) error {
		t.Fatalf("writeFile(%s) called in dryrun mode", filename)
		return nil
	}

	if !fiximports("all") {
		t.Fatalf("fiximports failed: %s", stderr)
	}
}
