package crashpool

import (
	"encoding/gob"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/parser"
)

const testWorkerEnv = "GORTEX_CRASHPOOL_TESTWORKER"

// TestMain lets the test binary re-execute itself as a worker
// subprocess: a Pool spawned with Argv={os.Args[0]} and the env var set
// runs one of the helper workers below instead of the test suite.
func TestMain(m *testing.M) {
	switch os.Getenv(testWorkerEnv) {
	case "real":
		_ = RunWorker(os.Stdin, os.Stdout)
		os.Exit(0)
	case "crash":
		testCrashWorker()
		os.Exit(0)
	case "slow":
		testSlowWorker()
		os.Exit(0)
	default:
		os.Exit(m.Run())
	}
}

// mockExtractor is a minimal parser.Extractor for worker tests.
type mockExtractor struct {
	lang   string
	exts   []string
	nodes  int
	panics bool
}

func (m *mockExtractor) Language() string     { return m.lang }
func (m *mockExtractor) Extensions() []string { return m.exts }
func (m *mockExtractor) Extract(filePath string, _ []byte) (*parser.ExtractionResult, error) {
	if m.panics {
		panic("mock extractor boom")
	}
	nodes := make([]*graph.Node, m.nodes)
	for i := range nodes {
		nodes[i] = &graph.Node{
			ID:   fmt.Sprintf("%s::n%d", filePath, i),
			Kind: graph.KindFunction,
			Name: fmt.Sprintf("n%d", i),
		}
	}
	return &parser.ExtractionResult{Nodes: nodes}, nil
}

func mockRegistry() *parser.Registry {
	reg := parser.NewRegistry()
	reg.Register(&mockExtractor{lang: "mock", exts: []string{".mock"}, nodes: 2})
	reg.Register(&mockExtractor{lang: "mockpanic", exts: []string{".mp"}, panics: true})
	return reg
}

// testCrashWorker serves mock extractions but hard-exits on any file
// whose path contains "BOOM" — simulating a SIGSEGV mid-parse.
func testCrashWorker() {
	reg := mockRegistry()
	dec := gob.NewDecoder(os.Stdin)
	enc := gob.NewEncoder(os.Stdout)
	for {
		var req extractRequest
		if dec.Decode(&req) != nil {
			return
		}
		if strings.Contains(req.RelPath, "BOOM") {
			os.Exit(99)
		}
		resp := serveOne(reg, req)
		if enc.Encode(&resp) != nil {
			return
		}
	}
}

// testSlowWorker sleeps before every response — used to exercise the
// per-request timeout.
func testSlowWorker() {
	dec := gob.NewDecoder(os.Stdin)
	enc := gob.NewEncoder(os.Stdout)
	for {
		var req extractRequest
		if dec.Decode(&req) != nil {
			return
		}
		time.Sleep(3 * time.Second)
		if enc.Encode(&extractResponse{Seq: req.Seq}) != nil {
			return
		}
	}
}

func newTestPool(t *testing.T, mode string, cfg Config) *Pool {
	t.Helper()
	cfg.Argv = []string{os.Args[0]}
	cfg.Env = []string{testWorkerEnv + "=" + mode}
	if cfg.Workers == 0 {
		cfg.Workers = 2
	}
	p, err := NewPool(cfg)
	require.NoError(t, err)
	t.Cleanup(p.Close)
	return p
}

func TestServeOne_Normal(t *testing.T) {
	resp := serveOne(mockRegistry(), extractRequest{Seq: 1, RelPath: "x.mock", Language: "mock"})
	require.Equal(t, uint64(1), resp.Seq)
	require.False(t, resp.Panicked)
	require.Empty(t, resp.Err)
	require.Len(t, resp.Nodes, 2)
}

func TestServeOne_PanicRecovered(t *testing.T) {
	resp := serveOne(mockRegistry(), extractRequest{Seq: 7, RelPath: "x.mp", Language: "mockpanic"})
	require.Equal(t, uint64(7), resp.Seq)
	require.True(t, resp.Panicked)
	require.Contains(t, resp.Err, "boom")
	require.Nil(t, resp.Nodes)
}

func TestServeOne_NoExtractor(t *testing.T) {
	resp := serveOne(mockRegistry(), extractRequest{Seq: 1, Language: "nonesuch"})
	require.Contains(t, resp.Err, "no extractor")
}

func TestPool_NormalExtraction(t *testing.T) {
	p := newTestPool(t, "real", Config{Workers: 2})
	res := p.Submit("main.go", "go", []byte("package main\n\nfunc Hello() {}\n"))
	require.False(t, res.Bad(), "unexpected failure: %s", res.Err)
	require.NotEmpty(t, res.Nodes)
}

// TestPool_CrashIsolation is the headline guarantee: a worker that dies
// mid-parse is detected, the file is reported crashed, and the pool
// respawns so every other file still indexes.
func TestPool_CrashIsolation(t *testing.T) {
	p := newTestPool(t, "crash", Config{Workers: 2})

	ok := p.Submit("a.mock", "mock", []byte("x"))
	require.False(t, ok.Bad())
	require.Len(t, ok.Nodes, 2)

	bad := p.Submit("dir/BOOM.mock", "mock", []byte("x"))
	require.True(t, bad.Crashed)
	require.True(t, bad.Bad())
	require.Contains(t, bad.Err, "crashed")

	// The pool respawned — every later file still indexes.
	for i := 0; i < 8; i++ {
		r := p.Submit(fmt.Sprintf("after%d.mock", i), "mock", []byte("x"))
		require.False(t, r.Bad(), "submit %d after crash failed: %s", i, r.Err)
		require.Len(t, r.Nodes, 2)
	}

	spawns, crashes := p.Stats()
	require.GreaterOrEqual(t, crashes, int64(1))
	require.GreaterOrEqual(t, spawns, int64(3)) // 2 initial + >=1 respawn
}

func TestPool_PanicSurvivesWorker(t *testing.T) {
	p := newTestPool(t, "crash", Config{Workers: 1})

	bad := p.Submit("p.mp", "mockpanic", []byte("x"))
	require.True(t, bad.Panicked)
	require.False(t, bad.Crashed) // recovered panic — worker stays alive

	// Same worker keeps serving.
	ok := p.Submit("ok.mock", "mock", []byte("x"))
	require.False(t, ok.Bad())
	require.Len(t, ok.Nodes, 2)

	_, crashes := p.Stats()
	require.Equal(t, int64(0), crashes)
}

func TestPool_RequestTimeout(t *testing.T) {
	p := newTestPool(t, "slow", Config{Workers: 1, RequestTimeout: 250 * time.Millisecond})
	res := p.Submit("slow.mock", "mock", []byte("x"))
	require.True(t, res.Crashed)
	require.Contains(t, res.Err, "timed out")
}

func TestPool_ClosedRejects(t *testing.T) {
	p := newTestPool(t, "crash", Config{Workers: 1})
	p.Close()
	res := p.Submit("x.mock", "mock", []byte("x"))
	require.True(t, res.Bad())
	require.Contains(t, res.Err, "closed")
}

func TestNewPool_EmptyArgv(t *testing.T) {
	_, err := NewPool(Config{Workers: 1})
	require.Error(t, err)
}
