package test

import (
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"
	_ "github.com/valyala/quicktemplate"

	"github.com/golangci/golangci-lint/pkg/exitcodes"
	"github.com/golangci/golangci-lint/test/testshared"
)

const minimalPkg = "minimalpkg"

func TestAutogeneratedNoIssues(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithTargetPath(testdataDir, "autogenerated").
		Runner().
		Install().
		Run().
		ExpectNoIssues()
}

func TestEmptyDirRun(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithEnviron("GO111MODULE=off").
		WithTargetPath(testdataDir, "nogofiles").
		Runner().
		Install().
		Run().
		ExpectExitCode(exitcodes.NoGoFiles).
		ExpectOutputContains(": no go files to analyze")
}

func TestNotExistingDirRun(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithEnviron("GO111MODULE=off").
		WithTargetPath(testdataDir, "no_such_dir").
		Runner().
		Install().
		Run().
		ExpectExitCode(exitcodes.Failure).
		ExpectOutputContains("cannot find package").
		ExpectOutputContains(testshared.NormalizeFileInString("/testdata/no_such_dir"))
}

func TestSymlinkLoop(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithTargetPath(testdataDir, "symlink_loop", "...").
		Runner().
		Install().
		Run().
		ExpectNoIssues()
}

func TestTimeout(t *testing.T) {
	projectRoot := filepath.Join("..", "...")

	testshared.NewRunnerBuilder(t).
		WithArgs("--timeout=1ms").
		WithTargetPath(projectRoot).
		Runner().
		Install().
		Run().
		ExpectExitCode(exitcodes.Timeout).
		ExpectOutputContains(`Timeout exceeded: try increasing it by passing --timeout option`)
}

func TestTimeoutInConfig(t *testing.T) {
	binPath := testshared.InstallGolangciLint(t)

	cfg := `
				run:
					timeout: 1ms
			`

	// Run with disallowed option set only in config
	testshared.NewRunnerBuilder(t).
		WithConfig(cfg).
		WithTargetPath(testdataDir, minimalPkg).
		WithBinPath(binPath).
		Runner().
		Run().
		ExpectExitCode(exitcodes.Timeout).
		ExpectOutputContains(`Timeout exceeded: try increasing it by passing --timeout option`)
}

func TestTestsAreLintedByDefault(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithTargetPath(testdataDir, "withtests").
		Runner().
		Install().
		Run().
		ExpectHasIssue("don't use `init` function")
}

func TestCgoOk(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs("--timeout=3m",
			"--enable-all",
		).
		WithTargetPath(testdataDir, "cgo").
		Runner().
		Install().
		Run().
		ExpectNoIssues()
}

func TestCgoWithIssues(t *testing.T) {
	binPath := testshared.InstallGolangciLint(t)

	testCases := []struct {
		desc     string
		args     []string
		dir      string
		expected string
	}{
		{
			desc:     "govet",
			args:     []string{"--no-config", "--disable-all", "-Egovet"},
			dir:      "cgo_with_issues",
			expected: "Printf format %t has arg cs of wrong type",
		},
		{
			desc:     "staticcheck",
			args:     []string{"--no-config", "--disable-all", "-Estaticcheck"},
			dir:      "cgo_with_issues",
			expected: "SA5009: Printf format %t has arg #1 of wrong type",
		},
		{
			desc:     "gofmt",
			args:     []string{"--no-config", "--disable-all", "-Egofmt"},
			dir:      "cgo_with_issues",
			expected: "File is not `gofmt`-ed with `-s` (gofmt)",
		},
		{
			desc:     "revive",
			args:     []string{"--no-config", "--disable-all", "-Erevive"},
			dir:      "cgo_with_issues",
			expected: "indent-error-flow: if block ends with a return statement",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			testshared.NewRunnerBuilder(t).
				WithArgs(test.args...).
				WithTargetPath(testdataDir, test.dir).
				WithBinPath(binPath).
				Runner().
				Run().
				ExpectHasIssue(test.expected)
		})
	}
}

// https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
func TestLineDirective(t *testing.T) {
	binPath := testshared.InstallGolangciLint(t)

	testCases := []struct {
		desc       string
		args       []string
		configPath string
		targetPath string
		expected   string
	}{
		{
			desc: "dupl",
			args: []string{
				"-Edupl",
				"--disable-all",
			},
			configPath: "testdata/linedirective/dupl.yml",
			targetPath: "linedirective",
			expected:   "21-23 lines are duplicate of `testdata/linedirective/hello.go:25-27` (dupl)",
		},
		{
			desc: "gofmt",
			args: []string{
				"-Egofmt",
				"--disable-all",
			},
			targetPath: "linedirective",
			expected:   "File is not `gofmt`-ed with `-s` (gofmt)",
		},
		{
			desc: "goimports",
			args: []string{
				"-Egoimports",
				"--disable-all",
			},
			targetPath: "linedirective",
			expected:   "File is not `goimports`-ed (goimports)",
		},
		{
			desc: "gomodguard",
			args: []string{
				"-Egomodguard",
				"--disable-all",
			},
			configPath: "testdata/linedirective/gomodguard.yml",
			targetPath: "linedirective",
			expected: "import of package `golang.org/x/tools/go/analysis` is blocked because the module is not " +
				"in the allowed modules list. (gomodguard)",
		},
		{
			desc: "lll",
			args: []string{
				"-Elll",
				"--disable-all",
			},
			configPath: "testdata/linedirective/lll.yml",
			targetPath: "linedirective",
			expected:   "the line is 57 characters long, which exceeds the maximum of 50 characters. (lll)",
		},
		{
			desc: "misspell",
			args: []string{
				"-Emisspell",
				"--disable-all",
			},
			configPath: "",
			targetPath: "linedirective",
			expected:   "is a misspelling of `language` (misspell)",
		},
		{
			desc: "wsl",
			args: []string{
				"-Ewsl",
				"--disable-all",
			},
			configPath: "",
			targetPath: "linedirective",
			expected:   "block should not start with a whitespace (wsl)",
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			testshared.NewRunnerBuilder(t).
				WithArgs(test.args...).
				WithTargetPath(testdataDir, test.targetPath).
				WithConfigFile(test.configPath).
				WithBinPath(binPath).
				Runner().
				Run().
				ExpectHasIssue(test.expected)
		})
	}
}

// https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
func TestLineDirectiveProcessedFiles(t *testing.T) {
	binPath := testshared.InstallGolangciLint(t)

	testCases := []struct {
		desc     string
		args     []string
		target   string
		expected []string
	}{
		{
			desc: "lite loading",
			args: []string{
				"--print-issued-lines=false",
				"--exclude-use-default=false",
				"-Erevive",
			},
			target: "quicktemplate",
			expected: []string{
				"testdata/quicktemplate/hello.qtpl.go:10:1: package-comments: should have a package comment (revive)",
				"testdata/quicktemplate/hello.qtpl.go:26:1: exported: exported function StreamHello should have comment or be unexported (revive)",
				"testdata/quicktemplate/hello.qtpl.go:39:1: exported: exported function WriteHello should have comment or be unexported (revive)",
				"testdata/quicktemplate/hello.qtpl.go:50:1: exported: exported function Hello should have comment or be unexported (revive)",
			},
		},
		{
			desc: "full loading",
			args: []string{
				"--print-issued-lines=false",
				"--exclude-use-default=false",
				"-Erevive,govet",
			},
			target: "quicktemplate",
			expected: []string{
				"testdata/quicktemplate/hello.qtpl.go:10:1: package-comments: should have a package comment (revive)",
				"testdata/quicktemplate/hello.qtpl.go:26:1: exported: exported function StreamHello should have comment or be unexported (revive)",
				"testdata/quicktemplate/hello.qtpl.go:39:1: exported: exported function WriteHello should have comment or be unexported (revive)",
				"testdata/quicktemplate/hello.qtpl.go:50:1: exported: exported function Hello should have comment or be unexported (revive)",
			},
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			testshared.NewRunnerBuilder(t).
				WithNoConfig().
				WithArgs(test.args...).
				WithTargetPath(testdataDir, test.target).
				WithBinPath(binPath).
				Runner().
				Run().
				ExpectExitCode(exitcodes.IssuesFound).
				ExpectOutputContains(test.expected...)
		})
	}
}

func TestUnsafeOk(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs("--enable-all").
		WithTargetPath(testdataDir, "unsafe").
		Runner().
		Install().
		Run().
		ExpectNoIssues()
}

func TestSortedResults(t *testing.T) {
	testCases := []struct {
		opt  string
		want string
	}{
		{
			opt: "--sort-results=false",
			want: "testdata/sort_results/main.go:15:13: Error return value is not checked (errcheck)" + "\n" +
				"testdata/sort_results/main.go:12:5: var `db` is unused (unused)",
		},
		{
			opt: "--sort-results=true",
			want: "testdata/sort_results/main.go:12:5: var `db` is unused (unused)" + "\n" +
				"testdata/sort_results/main.go:15:13: Error return value is not checked (errcheck)",
		},
	}

	binPath := testshared.InstallGolangciLint(t)

	for _, test := range testCases {
		t.Run(test.opt, func(t *testing.T) {
			t.Parallel()

			testshared.NewRunnerBuilder(t).
				WithNoConfig().
				WithArgs("--print-issued-lines=false", test.opt).
				WithTargetPath(testdataDir, "sort_results").
				WithBinPath(binPath).
				Runner().
				Run().
				ExpectExitCode(exitcodes.IssuesFound).ExpectOutputEq(test.want + "\n")
		})
	}
}

func TestSkippedDirsNoMatchArg(t *testing.T) {
	dir := filepath.Join(testdataDir, "skipdirs", "skip_me", "nested")

	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs(
			"--print-issued-lines=false",
			"--exclude-dirs", dir,
			"-Erevive",
		).
		WithTargetPath(dir).
		Runner().
		Install().
		Run().
		ExpectExitCode(exitcodes.IssuesFound).
		ExpectOutputEq("testdata/skipdirs/skip_me/nested/with_issue.go:8:9: " +
			"indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)\n")
}

func TestSkippedDirsTestdata(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs(
			"--print-issued-lines=false",
			"-Erevive",
		).
		WithTargetPath(testdataDir, "skipdirs", "...").
		Runner().
		Install().
		Run().
		ExpectNoIssues() // all was skipped because in testdata
}

func TestIdentifierUsedOnlyInTests(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs("--disable-all", "-Eunused").
		WithTargetPath(testdataDir, "used_only_in_tests").
		Runner().
		Install().
		Run().
		ExpectNoIssues()
}

func TestUnusedCheckExported(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithConfigFile("testdata_etc/unused_exported/golangci.yml").
		WithTargetPath("testdata_etc/unused_exported/...").
		Runner().
		Install().
		Run().
		ExpectNoIssues()
}

func TestConfigFileIsDetected(t *testing.T) {
	binPath := testshared.InstallGolangciLint(t)

	testCases := []struct {
		desc       string
		targetPath string
	}{
		{
			desc:       "explicit",
			targetPath: filepath.Join(testdataDir, "withconfig", "pkg"),
		},
		{
			desc:       "recursive",
			targetPath: filepath.Join(testdataDir, "withconfig", "..."),
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			testshared.NewRunnerBuilder(t).
				// WithNoConfig().
				WithTargetPath(test.targetPath).
				WithBinPath(binPath).
				Runner().
				Run().
				ExpectExitCode(exitcodes.Success).
				// test config contains InternalTest: true, it triggers such output
				ExpectOutputEq("test\n")
		})
	}
}

func TestEnableAllFastAndEnableCanCoexist(t *testing.T) {
	binPath := testshared.InstallGolangciLint(t)

	testCases := []struct {
		desc     string
		args     []string
		expected []int
	}{
		{
			desc:     "fast",
			args:     []string{"--fast", "--enable-all", "--enable=typecheck"},
			expected: []int{exitcodes.Success, exitcodes.IssuesFound},
		},
		{
			desc:     "all",
			args:     []string{"--enable-all", "--enable=typecheck"},
			expected: []int{exitcodes.Failure},
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			testshared.NewRunnerBuilder(t).
				WithNoConfig().
				WithArgs(test.args...).
				WithTargetPath(testdataDir, minimalPkg).
				WithBinPath(binPath).
				Runner().
				Run().
				ExpectExitCode(test.expected...)
		})
	}
}

func TestEnabledPresetsAreNotDuplicated(t *testing.T) {
	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs("-v", "-p", "style,bugs").
		WithTargetPath(testdataDir, minimalPkg).
		Runner().
		Install().
		Run().
		ExpectOutputContains("Active presets: [bugs style]")
}

func TestAbsPathDirAnalysis(t *testing.T) {
	dir := filepath.Join("testdata_etc", "abspath") // abs paths don't work with testdata dir
	absDir, err := filepath.Abs(dir)
	require.NoError(t, err)

	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs(
			"--print-issued-lines=false",
			"-Erevive",
		).
		WithTargetPath(absDir).
		Runner().
		Install().
		Run().
		ExpectHasIssue("testdata_etc/abspath/with_issue.go:8:9: " +
			"indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)")
}

func TestAbsPathFileAnalysis(t *testing.T) {
	dir := filepath.Join("testdata_etc", "abspath", "with_issue.go") // abs paths don't work with testdata dir
	absDir, err := filepath.Abs(dir)
	require.NoError(t, err)

	testshared.NewRunnerBuilder(t).
		WithNoConfig().
		WithArgs(
			"--print-issued-lines=false",
			"-Erevive",
		).
		WithTargetPath(absDir).
		Runner().
		Install().
		Run().
		ExpectHasIssue("indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)")
}

func TestPathPrefix(t *testing.T) {
	testCases := []struct {
		desc    string
		args    []string
		pattern string
	}{
		{
			desc:    "empty",
			pattern: "^testdata/withtests/",
		},
		{
			desc:    "prefixed",
			args:    []string{"--path-prefix=cool"},
			pattern: "^cool/testdata/withtests",
		},
	}

	binPath := testshared.InstallGolangciLint(t)

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			testshared.NewRunnerBuilder(t).
				WithArgs(test.args...).
				WithTargetPath(testdataDir, "withtests").
				WithBinPath(binPath).
				Runner().
				Run().
				ExpectOutputRegexp(test.pattern)
		})
	}
}
