// Copyright 2018 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 unitchecker_test

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"golang.org/x/tools/go/analysis/passes/assign"
	"golang.org/x/tools/go/analysis/passes/findcall"
	"golang.org/x/tools/go/analysis/passes/printf"
	"golang.org/x/tools/go/analysis/unitchecker"
	"golang.org/x/tools/internal/testenv"
	"golang.org/x/tools/internal/testfiles"
	"golang.org/x/tools/txtar"
)

func TestMain(m *testing.M) {
	// child process?
	switch os.Getenv("ENTRYPOINT") {
	case "vet":
		vetmain()
		panic("unreachable")
	case "minivet":
		minivet()
		panic("unreachable")
	case "worker":
		worker() // see ExampleSeparateAnalysis
		panic("unreachable")
	}

	// test process
	flag.Parse()
	os.Exit(m.Run())
}

// minivet is a vet-like tool with a few analyzers, for testing.
func minivet() {
	unitchecker.Main(
		findcall.Analyzer,
		printf.Analyzer,
		assign.Analyzer,
	)
}

// This is a very basic integration test of modular
// analysis with facts using unitchecker under "go vet".
// It fork/execs the main function above.
func TestIntegration(t *testing.T) {
	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
		t.Skipf("skipping fork/exec test on this platform")
	}

	const src = `
-- go.mod --
module golang.org/fake
go 1.24

-- a/a.go --
package a

func _() {
	MyFunc123()
}

func MyFunc123() {}

-- b/b.go --
package b

import "golang.org/fake/a"

func _() {
	a.MyFunc123()
	MyFunc123()
}

func MyFunc123() {}

-- c/c.go --
package c

func _() {
    i := 5
    i = i
}

-- d/d.go --
package d

import "fmt"

var (
	msg string
	_ = fmt.Sprintf(msg)
)

-- d/dgen.go --
// Code generated by hand. DO NOT EDIT.

package d

import "fmt"

var _ = fmt.Sprintf(msg)
`
	// Expand archive into tmp tree.
	fs, err := txtar.FS(txtar.Parse([]byte(src)))
	if err != nil {
		t.Fatal(err)
	}
	tmpdir := testfiles.CopyToTmp(t, fs)

	// -- operators --

	// vet runs "go vet" with the specified arguments (plus -findcall.name=MyFunc123).
	vet := func(t *testing.T, args ...string) (exitcode int, stdout, stderr string) {
		cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123")
		cmd.Stdout = new(strings.Builder)
		cmd.Stderr = new(strings.Builder)
		cmd.Args = append(cmd.Args, args...)
		cmd.Env = append(os.Environ(), "ENTRYPOINT=minivet")
		cmd.Dir = tmpdir
		if err := cmd.Run(); err != nil {
			exitErr, ok := err.(*exec.ExitError)
			if !ok {
				t.Fatalf("couldn't exec %v: %v", cmd, err)
			}
			exitcode = exitErr.ExitCode()
		}

		// Sanitize filenames; this is imperfect due to
		// (e.g.) /private/tmp -> /tmp symlink on macOS.
		stdout = strings.ReplaceAll(fmt.Sprint(cmd.Stdout), tmpdir, "TMPDIR")
		stderr = strings.ReplaceAll(fmt.Sprint(cmd.Stderr), tmpdir, "TMPDIR")

		// Show vet information on failure.
		t.Cleanup(func() {
			if t.Failed() {
				t.Logf("command: %v", cmd)
				t.Logf("exit code: %d", exitcode)
				t.Logf("stdout: %s", stdout)
				t.Logf("stderr: %s", stderr)
			}
		})
		return
	}

	// exitcode asserts that the exit code was "want".
	exitcode := func(t *testing.T, got, want int) {
		if got != want {
			t.Fatalf("vet tool exit code was %d", got)
		}
	}

	// parseJSON parses the JSON diagnostics into a simple line-oriented form.
	parseJSON := func(t *testing.T, stdout string) string {
		var v map[string]map[string][]map[string]any
		if err := json.Unmarshal([]byte(stdout), &v); err != nil {
			t.Fatalf("invalid JSON: %v", err)
		}
		var res strings.Builder
		for pkgpath, v := range v {
			for analyzer, v := range v {
				for _, v := range v {
					fmt.Fprintf(&res, "%s: [%s@%s] %v\n",
						v["posn"],
						analyzer, pkgpath,
						v["message"])
				}
			}
		}
		// Show parsed JSON information on failure.
		t.Cleanup(func() {
			if t.Failed() {
				t.Logf("json: %s", &res)
			}
		})
		return res.String()
	}

	// substring asserts that the labeled output contained the substring.
	substring := func(t *testing.T, label, output, substr string) {
		if !strings.Contains(output, substr) {
			t.Fatalf("%s: expected substring %q", label, substr)
		}
	}

	// contains asserts that the specified file contains the substring.
	contains := func(t *testing.T, filename, substr string) {
		content, err := os.ReadFile(filepath.Join(tmpdir, filename))
		if err != nil {
			t.Fatalf("can't read %s: %v", filename, err)
		}
		t.Cleanup(func() {
			if t.Failed() {
				t.Logf("content of %s: <<%s>>", filename, content)
			}
		})
		substring(t, filename, string(content), substr)
	}

	// -- scenarios --

	t.Run("a", func(t *testing.T) {
		code, _, stderr := vet(t, "golang.org/fake/a")
		exitcode(t, code, 1)
		substring(t, "stderr", stderr, "a/a.go:4:11: call of MyFunc123")
	})
	t.Run("b", func(t *testing.T) {
		code, _, stderr := vet(t, "golang.org/fake/b")
		exitcode(t, code, 1)
		substring(t, "stderr", stderr, "b/b.go:6:13: call of MyFunc123")
		substring(t, "stderr", stderr, "b/b.go:7:11: call of MyFunc123")
	})
	t.Run("c", func(t *testing.T) {
		code, _, stderr := vet(t, "golang.org/fake/c")
		exitcode(t, code, 1)
		substring(t, "stderr", stderr, "c/c.go:5:5: self-assignment of i")
	})
	t.Run("ab", func(t *testing.T) {
		code, _, stderr := vet(t, "golang.org/fake/a", "golang.org/fake/b")
		exitcode(t, code, 1)
		substring(t, "stderr", stderr, "a/a.go:4:11: call of MyFunc123")
		substring(t, "stderr", stderr, "b/b.go:6:13: call of MyFunc123")
		substring(t, "stderr", stderr, "b/b.go:7:11: call of MyFunc123")
	})
	t.Run("a-json", func(t *testing.T) {
		code, stdout, _ := vet(t, "-json", "golang.org/fake/a")
		exitcode(t, code, 0)
		testenv.NeedsGo1Point(t, 26) // depends on CL 702815 (go vet -json => stdout)
		json := parseJSON(t, stdout)
		substring(t, "json", json, "a/a.go:4:11: [findcall@golang.org/fake/a] call of MyFunc123")
	})
	t.Run("c-json", func(t *testing.T) {
		code, stdout, _ := vet(t, "-json", "golang.org/fake/c")
		exitcode(t, code, 0)
		testenv.NeedsGo1Point(t, 26) // depends on CL 702815 (go vet -json => stdout)
		json := parseJSON(t, stdout)
		substring(t, "json", json, "c/c.go:5:5: [assign@golang.org/fake/c] self-assignment of i")
	})
	t.Run("a-context", func(t *testing.T) {
		code, _, stderr := vet(t, "-c=0", "golang.org/fake/a")
		exitcode(t, code, 1)
		substring(t, "stderr", stderr, "a/a.go:4:11: call of MyFunc123")
		substring(t, "stderr", stderr, "4		MyFunc123")
	})
	t.Run("d-fix", func(t *testing.T) {
		testenv.NeedsGo1Point(t, 26)
		code, _, stderr := vet(t, "-fix", "-v", "golang.org/fake/d")
		exitcode(t, code, 0)
		contains(t, "d/d.go", `fmt.Sprintf("%s", msg)`) // fixed
		contains(t, "d/dgen.go", `fmt.Sprintf(msg)`)    // fix not applied to generated file
		// TODO(adonovan): plumb -v from go vet/fix down to unitchecker.
		if false {
			substring(t, "stderr", stderr, "skipped 1 fix that would edit generated files")
			substring(t, "stderr", stderr, "applied 1 of 2 fixes; 1 files updated")
		}
	})
}
