// Copyright 2023 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 test

import (
	"bufio"
	"fmt"
	"internal/testenv"
	"os"
	"path/filepath"
	"regexp"
	"testing"
)

type devirtualization struct {
	pos    string
	callee string
}

const profFileName = "devirt.pprof"
const preProfFileName = "devirt.pprof.node_map"

// testPGODevirtualize tests that specific PGO devirtualize rewrites are performed.
func testPGODevirtualize(t *testing.T, dir string, want, nowant []devirtualization, pgoProfileName string) {
	testenv.MustHaveGoRun(t)
	t.Parallel()

	const pkg = "example.com/pgo/devirtualize"

	// Add a go.mod so we have a consistent symbol names in this temp dir.
	goMod := fmt.Sprintf(`module %s
go 1.21
`, pkg)
	if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil {
		t.Fatalf("error writing go.mod: %v", err)
	}

	// Run the test without PGO to ensure that the test assertions are
	// correct even in the non-optimized version.
	cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), "test", "."))
	cmd.Dir = dir
	b, err := cmd.CombinedOutput()
	t.Logf("Test without PGO:\n%s", b)
	if err != nil {
		t.Fatalf("Test failed without PGO: %v", err)
	}

	// Build the test with the profile.
	pprof := filepath.Join(dir, pgoProfileName)
	gcflag := fmt.Sprintf("-gcflags=-m=2 -pgoprofile=%s -d=pgodebug=3", pprof)
	out := filepath.Join(dir, "test.exe")
	cmd = testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), "test", "-o", out, gcflag, "."))
	cmd.Dir = dir

	pr, pw, err := os.Pipe()
	if err != nil {
		t.Fatalf("error creating pipe: %v", err)
	}
	defer pr.Close()
	cmd.Stdout = pw
	cmd.Stderr = pw

	err = cmd.Start()
	pw.Close()
	if err != nil {
		t.Fatalf("error starting go test: %v", err)
	}

	got := make(map[devirtualization]struct{})
	gotNoHot := make(map[devirtualization]struct{})

	devirtualizedLine := regexp.MustCompile(`(.*): PGO devirtualizing \w+ call .* to (.*)`)
	noHotLine := regexp.MustCompile(`(.*): call .*: no hot callee`)

	scanner := bufio.NewScanner(pr)
	for scanner.Scan() {
		line := scanner.Text()
		t.Logf("child: %s", line)

		m := devirtualizedLine.FindStringSubmatch(line)
		if m != nil {
			d := devirtualization{
				pos:    m[1],
				callee: m[2],
			}
			got[d] = struct{}{}
			continue
		}
		m = noHotLine.FindStringSubmatch(line)
		if m != nil {
			d := devirtualization{
				pos: m[1],
			}
			gotNoHot[d] = struct{}{}
		}
	}
	if err := cmd.Wait(); err != nil {
		t.Fatalf("error running go test: %v", err)
	}
	if err := scanner.Err(); err != nil {
		t.Fatalf("error reading go test output: %v", err)
	}

	if len(got) != len(want) {
		t.Errorf("mismatched devirtualization count; got %v want %v", got, want)
	}
	for _, w := range want {
		if _, ok := got[w]; ok {
			continue
		}
		t.Errorf("devirtualization %v missing; got %v", w, got)
	}
	for _, nw := range nowant {
		if _, ok := gotNoHot[nw]; !ok {
			t.Errorf("unwanted devirtualization %v; got %v", nw, got)
		}
	}

	// Run test with PGO to ensure the assertions are still true.
	cmd = testenv.CleanCmdEnv(testenv.Command(t, out))
	cmd.Dir = dir
	b, err = cmd.CombinedOutput()
	t.Logf("Test with PGO:\n%s", b)
	if err != nil {
		t.Fatalf("Test failed without PGO: %v", err)
	}
}

// TestPGODevirtualize tests that specific functions are devirtualized when PGO
// is applied to the exact source that was profiled.
func TestPGODevirtualize(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		t.Fatalf("error getting wd: %v", err)
	}
	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")

	// Copy the module to a scratch location so we can add a go.mod.
	dir := t.TempDir()
	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
		t.Fatalf("error creating dir: %v", err)
	}
	for _, file := range []string{"devirt.go", "devirt_test.go", profFileName, filepath.Join("mult.pkg", "mult.go")} {
		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
			t.Fatalf("error copying %s: %v", file, err)
		}
	}

	want := []devirtualization{
		// ExerciseIface
		{
			pos:    "./devirt.go:101:20",
			callee: "mult.Mult.Multiply",
		},
		{
			pos:    "./devirt.go:101:39",
			callee: "Add.Add",
		},
		// ExerciseFuncConcrete
		{
			pos:    "./devirt.go:173:36",
			callee: "AddFn",
		},
		{
			pos:    "./devirt.go:173:15",
			callee: "mult.MultFn",
		},
		// ExerciseFuncField
		{
			pos:    "./devirt.go:207:35",
			callee: "AddFn",
		},
		{
			pos:    "./devirt.go:207:19",
			callee: "mult.MultFn",
		},
		// ExerciseFuncClosure
		// TODO(prattmic): Closure callees not implemented.
		//{
		//	pos:    "./devirt.go:249:27",
		//	callee: "AddClosure.func1",
		//},
		//{
		//	pos:    "./devirt.go:249:15",
		//	callee: "mult.MultClosure.func1",
		//},
	}
	nowant := []devirtualization{
		// ExerciseIfaceZeroWeight
		{
			pos: "./devirt.go:256:29",
		},
		// ExerciseIndirCallZeroWeight
		{
			pos: "./devirt.go:282:37",
		},
	}

	testPGODevirtualize(t, dir, want, nowant, profFileName)
}

// TestPGOPreprocessDevirtualize tests that specific functions are devirtualized when PGO
// is applied to the exact source that was profiled. The input profile is PGO preprocessed file.
func TestPGOPreprocessDevirtualize(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		t.Fatalf("error getting wd: %v", err)
	}
	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")

	// Copy the module to a scratch location so we can add a go.mod.
	dir := t.TempDir()
	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
		t.Fatalf("error creating dir: %v", err)
	}
	for _, file := range []string{"devirt.go", "devirt_test.go", preProfFileName, filepath.Join("mult.pkg", "mult.go")} {
		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
			t.Fatalf("error copying %s: %v", file, err)
		}
	}

	want := []devirtualization{
		// ExerciseIface
		{
			pos:    "./devirt.go:101:20",
			callee: "mult.Mult.Multiply",
		},
		{
			pos:    "./devirt.go:101:39",
			callee: "Add.Add",
		},
		// ExerciseFuncConcrete
		{
			pos:    "./devirt.go:173:36",
			callee: "AddFn",
		},
		{
			pos:    "./devirt.go:173:15",
			callee: "mult.MultFn",
		},
		// ExerciseFuncField
		{
			pos:    "./devirt.go:207:35",
			callee: "AddFn",
		},
		{
			pos:    "./devirt.go:207:19",
			callee: "mult.MultFn",
		},
		// ExerciseFuncClosure
		// TODO(prattmic): Closure callees not implemented.
		//{
		//	pos:    "./devirt.go:249:27",
		//	callee: "AddClosure.func1",
		//},
		//{
		//	pos:    "./devirt.go:249:15",
		//	callee: "mult.MultClosure.func1",
		//},
	}
	nowant := []devirtualization{
		// ExerciseIfaceZeroWeight
		{
			pos: "./devirt.go:256:29",
		},
		// ExerciseIndirCallZeroWeight
		{
			pos: "./devirt.go:282:37",
		},
	}

	testPGODevirtualize(t, dir, want, nowant, preProfFileName)
}

// Regression test for https://go.dev/issue/65615. If a target function changes
// from non-generic to generic we can't devirtualize it (don't know the type
// parameters), but the compiler should not crash.
func TestLookupFuncGeneric(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		t.Fatalf("error getting wd: %v", err)
	}
	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")

	// Copy the module to a scratch location so we can add a go.mod.
	dir := t.TempDir()
	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
		t.Fatalf("error creating dir: %v", err)
	}
	for _, file := range []string{"devirt.go", "devirt_test.go", profFileName, filepath.Join("mult.pkg", "mult.go")} {
		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
			t.Fatalf("error copying %s: %v", file, err)
		}
	}

	// Change MultFn from a concrete function to a parameterized function.
	if err := convertMultToGeneric(filepath.Join(dir, "mult.pkg", "mult.go")); err != nil {
		t.Fatalf("error editing mult.go: %v", err)
	}

	// Same as TestPGODevirtualize except for MultFn, which we cannot
	// devirtualize to because it has become generic.
	//
	// Note that the important part of this test is that the build is
	// successful, not the specific devirtualizations.
	want := []devirtualization{
		// ExerciseIface
		{
			pos:    "./devirt.go:101:20",
			callee: "mult.Mult.Multiply",
		},
		{
			pos:    "./devirt.go:101:39",
			callee: "Add.Add",
		},
		// ExerciseFuncConcrete
		{
			pos:    "./devirt.go:173:36",
			callee: "AddFn",
		},
		// ExerciseFuncField
		{
			pos:    "./devirt.go:207:35",
			callee: "AddFn",
		},
		// ExerciseFuncClosure
		// TODO(prattmic): Closure callees not implemented.
		//{
		//	pos:    "./devirt.go:249:27",
		//	callee: "AddClosure.func1",
		//},
		//{
		//	pos:    "./devirt.go:249:15",
		//	callee: "mult.MultClosure.func1",
		//},
	}
	nowant := []devirtualization{
		// ExerciseIfaceZeroWeight
		{
			pos: "./devirt.go:256:29",
		},
		// ExerciseIndirCallZeroWeight
		{
			pos: "./devirt.go:282:37",
		},
	}

	testPGODevirtualize(t, dir, want, nowant, profFileName)
}

var multFnRe = regexp.MustCompile(`func MultFn\(a, b int64\) int64`)

func convertMultToGeneric(path string) error {
	content, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("error opening: %w", err)
	}

	if !multFnRe.Match(content) {
		return fmt.Errorf("MultFn not found; update regexp?")
	}

	// Users of MultFn shouldn't need adjustment, type inference should
	// work OK.
	content = multFnRe.ReplaceAll(content, []byte(`func MultFn[T int32|int64](a, b T) T`))

	return os.WriteFile(path, content, 0644)
}
