package process

import (
	"os"
	"path/filepath"
	"strconv"
	"testing"
)

// fakeProber is a test double for processProber.
type fakeProber struct {
	pids  map[string][]int // pattern → PIDs
	comms map[int]string   // PID → comm name
	alive map[int]bool     // PID → is alive
}

func newFakeProber() *fakeProber {
	return &fakeProber{
		pids:  make(map[string][]int),
		comms: make(map[int]string),
		alive: make(map[int]bool),
	}
}

func (f *fakeProber) pgrepPIDs(pattern string) []int { return f.pids[pattern] }
func (f *fakeProber) processComm(pid int) string      { return f.comms[pid] }
func (f *fakeProber) isAlive(pid int) bool             { return f.alive[pid] }

// testManager creates a Manager with a temp dir, fake prober, and a config file.
func testManager(t *testing.T, fp *fakeProber) (*Manager, string) {
	t.Helper()
	dir := t.TempDir()
	runDir := filepath.Join(dir, "run")
	agentsDir := filepath.Join(dir, "agents", "test-bot")
	_ = os.MkdirAll(runDir, 0o755)
	_ = os.MkdirAll(agentsDir, 0o755)

	// Minimal config.yaml so Scan() and configPathFor() work.
	cfgPath := filepath.Join(agentsDir, "config.yaml")
	_ = os.WriteFile(cfgPath, []byte(`agent:
  id: test-bot
  name: Test Bot
  version: "0.1"
  enabled: true
`), 0o644)

	glob := filepath.Join(dir, "agents", "*", "config.yaml")
	m := &Manager{
		runDir:     runDir,
		agentsGlob: glob,
		binPath:    "/bin/true", // won't actually run
		envFile:    "",
		prober:     fp,
	}
	return m, cfgPath
}

func TestFindProcessPIDs_FiltersGoWrapper(t *testing.T) {
	fp := newFakeProber()
	m, cfgPath := testManager(t, fp)

	// Simulate pgrep returning 2 PIDs: go wrapper (100) + real launcher (200).
	pattern := "launcher.*-c.*" + cfgPath
	fp.pids[pattern] = []int{100, 200}
	fp.comms[100] = "go"
	fp.comms[200] = "launcher"

	pids := m.findProcessPIDs("test-bot")

	if len(pids) != 1 {
		t.Fatalf("expected 1 PID, got %d: %v", len(pids), pids)
	}
	if pids[0] != 200 {
		t.Errorf("expected PID 200, got %d", pids[0])
	}
}

func TestFindProcessPIDs_NoPIDs(t *testing.T) {
	fp := newFakeProber()
	m, _ := testManager(t, fp)

	pids := m.findProcessPIDs("test-bot")
	if len(pids) != 0 {
		t.Fatalf("expected 0 PIDs, got %d", len(pids))
	}
}

func TestStatus_SingleInstance(t *testing.T) {
	fp := newFakeProber()
	m, cfgPath := testManager(t, fp)

	pattern := "launcher.*-c.*" + cfgPath
	fp.pids[pattern] = []int{42}
	fp.comms[42] = "launcher"

	info := AgentInfo{ID: "test-bot", Name: "Test", ConfigPath: cfgPath, Enabled: true}
	st := m.Status(info)

	if !st.Running {
		t.Error("expected Running=true")
	}
	if st.PID != 42 {
		t.Errorf("expected PID=42, got %d", st.PID)
	}
	if st.Instances != 1 {
		t.Errorf("expected Instances=1, got %d", st.Instances)
	}
}

func TestStatus_NoInstances(t *testing.T) {
	fp := newFakeProber()
	m, cfgPath := testManager(t, fp)

	info := AgentInfo{ID: "test-bot", Name: "Test", ConfigPath: cfgPath, Enabled: true}
	st := m.Status(info)

	if st.Running {
		t.Error("expected Running=false")
	}
	if st.Instances != 0 {
		t.Errorf("expected Instances=0, got %d", st.Instances)
	}
}

func TestStart_RejectsWhenAlreadyRunning(t *testing.T) {
	fp := newFakeProber()
	m, cfgPath := testManager(t, fp)

	pattern := "launcher.*-c.*" + cfgPath
	fp.pids[pattern] = []int{99}
	fp.comms[99] = "launcher"

	info := AgentInfo{ID: "test-bot", Name: "Test", ConfigPath: cfgPath, Enabled: true}
	err := m.Start(info)
	if err == nil {
		t.Fatal("expected error when agent already running")
	}
	if got := err.Error(); got != `agent "test-bot" is already running (PID 99)` {
		t.Errorf("unexpected error: %s", got)
	}
}

func TestResolveRunningPID_RepairsStale(t *testing.T) {
	fp := newFakeProber()
	m, cfgPath := testManager(t, fp)

	// Write a stale PID file (PID 999 is dead).
	_ = os.MkdirAll(m.runDir, 0o755)
	_ = os.WriteFile(m.pidPath("test-bot"), []byte("999"), 0o644)
	fp.alive[999] = false

	// But the real process is at PID 42.
	pattern := "launcher.*-c.*" + cfgPath
	fp.pids[pattern] = []int{42}
	fp.comms[42] = "launcher"

	pid := m.resolveRunningPID("test-bot")
	if pid != 42 {
		t.Errorf("expected repaired PID=42, got %d", pid)
	}

	// Verify PID file was repaired.
	data, err := os.ReadFile(m.pidPath("test-bot"))
	if err != nil {
		t.Fatalf("read pid file: %v", err)
	}
	if got, _ := strconv.Atoi(string(data)); got != 42 {
		t.Errorf("expected PID file to contain 42, got %d", got)
	}
}

func TestResolveRunningPID_CleansUpStalePIDFile(t *testing.T) {
	fp := newFakeProber()
	m, _ := testManager(t, fp)

	// Write a stale PID file, no real process running.
	_ = os.MkdirAll(m.runDir, 0o755)
	_ = os.WriteFile(m.pidPath("test-bot"), []byte("999"), 0o644)
	fp.alive[999] = false

	pid := m.resolveRunningPID("test-bot")
	if pid != 0 {
		t.Errorf("expected 0 for dead process, got %d", pid)
	}

	// PID file should be removed.
	if _, err := os.Stat(m.pidPath("test-bot")); !os.IsNotExist(err) {
		t.Error("expected stale PID file to be removed")
	}
}
