package main

import (
	"fmt"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"
)

func BenchmarkGenerateGuardSourceSmall(b *testing.B) {
	rnd := rand.New(rand.NewSource(42))
	for i := 0; i < b.N; i++ {
		_ = generateGuardSourceSmall(rnd)
	}
}

func BenchmarkGenerateGuardSourceScaled(b *testing.B) {
	rnd := rand.New(rand.NewSource(42))
	for i := 0; i < b.N; i++ {
		_ = generateGuardSource(rnd, "main", 1.0)
	}
}

func BenchmarkGenerateGuardSourceBallastOnly(b *testing.B) {
	rnd := rand.New(rand.NewSource(42))
	for i := 0; i < b.N; i++ {
		_ = generateGuardSourceBallastOnly(rnd, "main", 1.0)
	}
}

var benchBallastSrc string

func init() {
	rnd := rand.New(rand.NewSource(42))
	benchBallastSrc = generateGuardSource(rnd, "bench", 0.1)
}

func BenchmarkBallastGenerationThroughput(b *testing.B) {
	b.ReportAllocs()
	b.SetBytes(1)
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		src := generateGuardSource(rnd, "main", 0.05)
		_ = len(src)
	}
}

func BenchmarkCompileTimeSmallGuard(b *testing.B) {
	if testing.Short() {
		b.Skip("skipping compile benchmark in short mode")
	}
	tmpDir := b.TempDir()
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		src := generateGuardSourceSmall(rnd)
		if err := os.WriteFile(filepath.Join(tmpDir, "guard.go"), []byte(src), 0644); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkMemoryPressureHeavy(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string)}
		g.emitWithParams("main",
			50+rand.Intn(50),
			5600+rand.Intn(2400),
			50+rand.Intn(50),
			50+rand.Intn(50),
			1000+rand.Intn(1000),
		)
		_ = g.sb.Len()
	}
}

func BenchmarkStringBuilderGrowth(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string)}
		g.emitHeavyBallast("_benchHeavy", []string{"_t1", "_t2", "_t3"})
		_ = g.sb.String()
	}
}

func BenchmarkStringBuilderGrowthMedium(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string)}
		g.emitMediumBallast("_benchMedium", []string{"_t1", "_t2"}, []string{"_prev1"})
		_ = g.sb.String()
	}
}

func BenchmarkStringBuilderGrowthLight(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string), lookupMask: 127}
		g.emitLightBallast("_benchLight", []string{"_t1"}, []string{"_prev1"})
		_ = g.sb.String()
	}
}

func TestBuildMetrics(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping build metrics in short mode")
	}

	scales := []struct {
		name  string
		scale float64
	}{
		{"tiny", 0.01},
		{"small", 0.1},
		{"medium", 0.5},
		{"full", 1.0},
	}

	for _, sc := range scales {
		t.Run(sc.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			rnd := rand.New(rand.NewSource(42))
			src := generateGuardSource(rnd, "main", sc.scale)

			mainGo := `package main
import "fmt"
func main() { fmt.Println("ok") }
`
			if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(mainGo), 0644); err != nil {
				t.Fatal(err)
			}
			if err := os.WriteFile(filepath.Join(tmpDir, "guard.go"), []byte(src), 0644); err != nil {
				t.Fatal(err)
			}
			if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test\ngo 1.21\n"), 0644); err != nil {
				t.Fatal(err)
			}

			start := time.Now()
			cmd := exec.Command("go", "build", "-o", "test.exe")
			cmd.Dir = tmpDir
			cmd.Env = os.Environ()
			out, err := cmd.CombinedOutput()
			elapsed := time.Since(start)

			if err != nil {
				t.Fatalf("build failed: %v\n%s", err, out)
			}

			info, err := os.Stat(filepath.Join(tmpDir, "test.exe"))
			if err != nil {
				t.Fatal(err)
			}

			t.Logf("scale=%.2f build_time=%.2fs size=%d bytes (%.2f MB) lines=%d",
				sc.scale, elapsed.Seconds(), info.Size(), float64(info.Size())/1024/1024,
				strings.Count(src, "\n"))
		})
	}
}

func TestBallastOnlyBuildMetrics(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping build metrics in short mode")
	}

	scales := []struct {
		name  string
		scale float64
	}{
		{"tiny", 0.01},
		{"small", 0.1},
		{"medium", 0.5},
		{"full", 1.0},
	}

	for _, sc := range scales {
		t.Run(sc.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			rnd := rand.New(rand.NewSource(42))
			src := generateGuardSourceBallastOnly(rnd, "main", sc.scale)

			mainGo := `package main
import "fmt"
func main() { fmt.Println("ok") }
`
			if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(mainGo), 0644); err != nil {
				t.Fatal(err)
			}
			if err := os.WriteFile(filepath.Join(tmpDir, "guard.go"), []byte(src), 0644); err != nil {
				t.Fatal(err)
			}
			if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test\ngo 1.21\n"), 0644); err != nil {
				t.Fatal(err)
			}

			start := time.Now()
			cmd := exec.Command("go", "build", "-o", "test.exe")
			cmd.Dir = tmpDir
			cmd.Env = os.Environ()
			out, err := cmd.CombinedOutput()
			elapsed := time.Since(start)

			if err != nil {
				t.Fatalf("build failed: %v\n%s", err, out)
			}

			info, err := os.Stat(filepath.Join(tmpDir, "test.exe"))
			if err != nil {
				t.Fatal(err)
			}

			t.Logf("scale=%.2f build_time=%.2fs size=%d bytes (%.2f MB) lines=%d",
				sc.scale, elapsed.Seconds(), info.Size(), float64(info.Size())/1024/1024,
				strings.Count(src, "\n"))
		})
	}
}

func BenchmarkRuntimeExecution(b *testing.B) {
	if testing.Short() {
		b.Skip("skipping runtime execution benchmark in short mode")
	}

	rnd := rand.New(rand.NewSource(42))
	src := generateGuardSource(rnd, "main", 0.05)

	tmpDir := b.TempDir()
	mainGo := `package main
import "fmt"
func main() { fmt.Println("ok") }
`
	os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(mainGo), 0644)
	os.WriteFile(filepath.Join(tmpDir, "guard.go"), []byte(src), 0644)
	os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test\ngo 1.21\n"), 0644)

	cmd := exec.Command("go", "build", "-o", "test.exe")
	cmd.Dir = tmpDir
	cmd.Env = os.Environ()
	if out, err := cmd.CombinedOutput(); err != nil {
		b.Fatalf("build failed: %v\n%s", err, out)
	}

	exePath := filepath.Join(tmpDir, "test.exe")
	b.ResetTimer()
	b.ReportAllocs()

	for i := 0; i < b.N; i++ {
		cmd := exec.Command(exePath)
		cmd.Env = os.Environ()
		out, err := cmd.CombinedOutput()
		if err != nil {
			b.Fatalf("run failed: %v\n%s", err, out)
		}
		_ = out
	}
}

func BenchmarkLookupTableGeneration(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string)}
		g.emitLookupTables(36, 5600)
		_ = g.sb.Len()
	}
}

func BenchmarkLookupTableGenerationLarge(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string)}
		g.emitLookupTables(54, 8000)
		_ = g.sb.Len()
	}
}

func TestSourceCodeSizeAnalysis(t *testing.T) {
	scales := []float64{0.01, 0.1, 0.5, 1.0}
	for _, sc := range scales {
		rnd := rand.New(rand.NewSource(42))
		src := generateGuardSource(rnd, "main", sc)
		lines := strings.Count(src, "\n")
		t.Logf("scale=%.2f source_lines=%d source_bytes=%d", sc, lines, len(src))
	}
}

func BenchmarkEmitHeavyBallast(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string), lookupMask: 127}
		g.emitHeavyBallast("_h", []string{"_t1", "_t2", "_t3"})
		_ = g.sb.Len()
	}
}

func BenchmarkEmitMediumBallast(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string), lookupMask: 127}
		g.emitMediumBallast("_m", []string{"_t1", "_t2"}, []string{"_h1"})
		_ = g.sb.Len()
	}
}

func BenchmarkEmitLightBallast(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rnd := rand.New(rand.NewSource(int64(i)))
		g := &ggen{r: rnd, ids: make(map[string]string), lookupMask: 127}
		g.emitLightBallast("_l", []string{"_t1"}, []string{"_h1", "_m1"})
		_ = g.sb.Len()
	}
}

func TestMemoryProfile(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping memory profile in short mode")
	}

	var m1, m2 runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&m1)

	rnd := rand.New(rand.NewSource(42))
	src := generateGuardSource(rnd, "main", 1.0)

	runtime.GC()
	runtime.ReadMemStats(&m2)

	alloc := m2.TotalAlloc - m1.TotalAlloc
	t.Logf("full guard generation: alloc=%d bytes (%.2f MB) source_size=%d bytes",
		alloc, float64(alloc)/1024/1024, len(src))
}

func BenchmarkConcurrentGeneration(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		done := make(chan struct{}, runtime.GOMAXPROCS(0))
		for w := 0; w < runtime.GOMAXPROCS(0); w++ {
			go func(seed int64) {
				rnd := rand.New(rand.NewSource(seed))
				src := generateGuardSource(rnd, "main", 0.1)
				_ = len(src)
				done <- struct{}{}
			}(int64(i*1000 + w))
		}
		for w := 0; w < runtime.GOMAXPROCS(0); w++ {
			<-done
		}
	}
}

func TestBenchmarkReportHeader(t *testing.T) {
	t.Log("========================================================================")
	t.Log("GUARD/BALLAST PERFORMANCE BENCHMARK SUITE")
	t.Log("========================================================================")
	t.Logf("Go Version: %s", runtime.Version())
	t.Logf("GOOS/GOARCH: %s/%s", runtime.GOOS, runtime.GOARCH)
	t.Logf("GOMAXPROCS: %d", runtime.GOMAXPROCS(0))
	t.Logf("NumCPU: %d", runtime.NumCPU())
	t.Log("========================================================================")
}

func TestComponentLineCounts(t *testing.T) {
	rnd := rand.New(rand.NewSource(42))
	g := &ggen{r: rnd, ids: make(map[string]string)}

	components := []struct {
		name string
		fn   func()
	}{
		{"header", func() { g.emitHeader("main") }},
		{"globals", func() { g.emitGlobals() }},
		{"fail", func() { g.emitFail() }},
		{"primitives", func() { g.emitPrimitives() }},
		{"exePath", func() { g.emitExePath() }},
		{"validatePath", func() { g.emitValidatePath() }},
		{"openExe", func() { g.emitOpenExe() }},
		{"getSize", func() { g.emitGetSize() }},
		{"readTrailer", func() { g.emitReadTrailer() }},
		{"magicCheck", func() { g.emitMagicCheck() }},
		{"sizeCheck", func() { g.emitSizeCheck() }},
		{"sha256", func() { g.emitSHA256() }},
		{"hashBody", func() { g.emitHashBody() }},
		{"cmpHash", func() { g.emitCmpHash() }},
	}

	for _, c := range components {
		rnd := rand.New(rand.NewSource(42))
		g := &ggen{r: rnd, ids: make(map[string]string), lookupMask: 127}
		c.fn()
		lines := strings.Count(g.sb.String(), "\n")
		t.Logf("component=%s lines=%d", c.name, lines)
	}

	ballastTests := []struct {
		name      string
		numHeavy  int
		numMedium int
		numLight  int
	}{
		{"minimal", 1, 1, 1},
		{"small", 5, 5, 10},
		{"medium", 20, 20, 100},
		{"large", 50, 50, 1000},
	}

	for _, bt := range ballastTests {
		rnd := rand.New(rand.NewSource(42))
		g := &ggen{r: rnd, ids: make(map[string]string), lookupMask: 127}
		tables := []string{"_t1", "_t2", "_t3"}
		names := g.emitBallastFunctions(tables, bt.numHeavy, bt.numMedium, bt.numLight)
		g.emitBallastChain(names)
		lines := strings.Count(g.sb.String(), "\n")
		t.Logf("ballast config=%s heavy=%d medium=%d light=%d total_funcs=%d lines=%d",
			bt.name, bt.numHeavy, bt.numMedium, bt.numLight, len(names), lines)
	}
}

func TestScaleFactorImpact(t *testing.T) {
	scales := []float64{0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0}
	for _, sc := range scales {
		rnd := rand.New(rand.NewSource(42))
		start := time.Now()
		src := generateGuardSource(rnd, "main", sc)
		genTime := time.Since(start)

		lines := strings.Count(src, "\n")
		funcs := strings.Count(src, "func ")
		vars := strings.Count(src, "var ")

		t.Logf("scale=%.3f gen_time_ms=%.1f lines=%d funcs=%d vars=%d",
			sc, float64(genTime.Nanoseconds())/1e6, lines, funcs, vars)
	}
}

func TestCurrentVsOptimizedSizes(t *testing.T) {
	rnd := rand.New(rand.NewSource(42))
	baseline := generateGuardSource(rnd, "main", 1.0)

	t.Logf("BASELINE: bytes=%d lines=%d funcs=%d",
		len(baseline), strings.Count(baseline, "\n"), strings.Count(baseline, "func "))

}

func TestTableSizeImpact(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping table size impact in short mode")
	}

	sizes := []int{1000, 2000, 4000, 8000, 16000}
	for _, sz := range sizes {
		t.Run(fmt.Sprintf("size_%d", sz), func(t *testing.T) {
			tmpDir := t.TempDir()
			rnd := rand.New(rand.NewSource(42))
			g := &ggen{r: rnd, ids: make(map[string]string)}
			g.emitWithParams("main", 4, sz, 2, 2, 2)
			src := g.sb.String()

			mainGo := `package main
import "fmt"
func main() { fmt.Println("ok") }
`
			os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(mainGo), 0644)
			os.WriteFile(filepath.Join(tmpDir, "guard.go"), []byte(src), 0644)
			os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test\ngo 1.21\n"), 0644)

			start := time.Now()
			cmd := exec.Command("go", "build", "-o", "test.exe")
			cmd.Dir = tmpDir
			cmd.Env = os.Environ()
			out, err := cmd.CombinedOutput()
			elapsed := time.Since(start)

			if err != nil {
				t.Fatalf("build failed: %v\n%s", err, out)
			}

			info, _ := os.Stat(filepath.Join(tmpDir, "test.exe"))
			t.Logf("table_size=%d build_time=%.2fs binary_size=%d bytes (%.2f MB) source_lines=%d",
				sz, elapsed.Seconds(), info.Size(), float64(info.Size())/1024/1024,
				strings.Count(src, "\n"))
		})
	}
}
