//go:build !windows && linux

package infra

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// readOwnProcStart reads field 22 (starttime) of /proc/<pid>/stat for the
// current test process, so a fixture can be marked Alive deterministically.
func readOwnProcStart(t *testing.T, pid int) string {
	t.Helper()
	data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
	if err != nil {
		t.Fatalf("read own /proc/%d/stat: %v", pid, err)
	}
	s := string(data)
	close := strings.LastIndex(s, ")")
	if close < 0 {
		t.Fatalf("malformed stat line: %q", s)
	}
	rest := strings.Fields(s[close+1:])
	const startTimeIdx = 19 // field 22 == rest[22-3]
	if len(rest) <= startTimeIdx {
		t.Fatalf("stat has too few fields after comm: %d", len(rest))
	}
	return rest[startTimeIdx]
}

func writeFile(t *testing.T, path, content string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("mkdir %q: %v", filepath.Dir(path), err)
	}
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("write %q: %v", path, err)
	}
}

func TestListClaudeFleetFrom(t *testing.T) {
	tmp := t.TempDir()
	sessions := filepath.Join(tmp, "sessions")
	goals := filepath.Join(tmp, "goals")

	livePID := os.Getpid()
	liveProcStart := readOwnProcStart(t, livePID)

	const deadPID = 2147480000 // implausibly high; no such process

	// Session A: alive (own PID), with a goal -> rename = truncated goal,
	// status idle. cwd basename = fn_registry.
	writeFile(t, filepath.Join(sessions, fmt.Sprintf("%d.json", livePID)),
		fmt.Sprintf(`{"pid":%d,"sessionId":"aaaaaaaa-1111-2222-3333-444444444444","cwd":"/home/enmanuel/fn_registry","procStart":%q,"status":"idle","updatedAt":1000}`,
			livePID, liveProcStart))
	writeFile(t, filepath.Join(goals, "aaaaaaaa-1111-2222-3333-444444444444.json"),
		`{"goal":"Recomendar stack tecnologico para la nueva app de inventario y validar dependencias","phase":"investigando","history":["haciendo","investigando"],"dod_contract":"build verde + tests pasan + tipo expuesto","dod_status":"pending"}`)

	// Session B: alive (own PID again — same process, valid procStart), no
	// goal sidecar -> rename = basename(cwd) = projectx, status busy.
	writeFile(t, filepath.Join(sessions, "b.json"),
		fmt.Sprintf(`{"pid":%d,"sessionId":"bbbbbbbb-5555","cwd":"/var/tmp/projectx","procStart":%q,"status":"busy","updatedAt":2000}`,
			livePID, liveProcStart))

	// Session C: dead PID -> Alive=false, status waiting, has goal.
	writeFile(t, filepath.Join(sessions, fmt.Sprintf("%d.json", deadPID)),
		fmt.Sprintf(`{"pid":%d,"sessionId":"cccccccc-9999-0000","cwd":"/srv/work/zeta","procStart":"99999999","status":"waiting","updatedAt":3000}`,
			deadPID))
	writeFile(t, filepath.Join(goals, "cccccccc-9999-0000.json"),
		`{"goal":"limpiar logs","phase":"haciendo"}`)

	// Noise files that must be ignored.
	writeFile(t, filepath.Join(sessions, "notjson.txt"), "ignore me")
	writeFile(t, filepath.Join(sessions, "broken.json"), "{ this is not json")

	fleet, err := ListClaudeFleetFrom(tmp)
	if err != nil {
		t.Fatalf("ListClaudeFleetFrom: %v", err)
	}
	if len(fleet) != 3 {
		t.Fatalf("expected 3 sessions, got %d: %+v", len(fleet), fleet)
	}

	by := map[string]ClaudeFleet{}
	for _, f := range fleet {
		by[f.SessionID] = f
	}

	// --- Session A assertions ---
	a := by["aaaaaaaa-1111-2222-3333-444444444444"]
	if !a.Alive {
		t.Errorf("session A: expected Alive=true (own PID + matching procStart)")
	}
	if a.Goal != "Recomendar stack tecnologico para la nueva app de inventario y validar dependencias" {
		t.Errorf("session A: goal join failed, got %q", a.Goal)
	}
	if a.Phase != "investigando" {
		t.Errorf("session A: phase join failed, got %q", a.Phase)
	}
	if a.DodContract != "build verde + tests pasan + tipo expuesto" {
		t.Errorf("session A: dod_contract join failed, got %q", a.DodContract)
	}
	if a.DodStatus != "pending" {
		t.Errorf("session A: dod_status join failed, got %q", a.DodStatus)
	}
	// Rename = goal truncated to 48 runes.
	wantRename := string([]rune(a.Goal)[:48])
	if a.Rename != wantRename {
		t.Errorf("session A: rename = %q, want truncated goal %q", a.Rename, wantRename)
	}
	if len([]rune(a.Rename)) != 48 {
		t.Errorf("session A: rename should be 48 runes, got %d", len([]rune(a.Rename)))
	}
	if a.Target != "aaaaaaaa@fn_registry" {
		t.Errorf("session A: target = %q, want %q", a.Target, "aaaaaaaa@fn_registry")
	}

	// --- Session B assertions: no goal -> fallback rename = basename(cwd) ---
	b := by["bbbbbbbb-5555"]
	if b.Goal != "" || b.Phase != "" {
		t.Errorf("session B: expected empty goal/phase, got goal=%q phase=%q", b.Goal, b.Phase)
	}
	if b.DodContract != "" || b.DodStatus != "" {
		t.Errorf("session B: expected empty dod fields (no sidecar), got contract=%q status=%q", b.DodContract, b.DodStatus)
	}
	if b.Rename != "projectx" {
		t.Errorf("session B: rename = %q, want basename(cwd) %q", b.Rename, "projectx")
	}
	if b.Target != "bbbbbbbb@projectx" {
		t.Errorf("session B: target = %q, want %q", b.Target, "bbbbbbbb@projectx")
	}
	if !b.Alive {
		t.Errorf("session B: expected Alive=true (own PID + matching procStart)")
	}

	// --- Session C assertions: dead PID ---
	c := by["cccccccc-9999-0000"]
	if c.Alive {
		t.Errorf("session C: expected Alive=false for dead PID %d", deadPID)
	}
	if c.Target != "cccccccc@zeta" {
		t.Errorf("session C: target = %q, want %q", c.Target, "cccccccc@zeta")
	}

	// --- Ordering: status rank idle(0) < waiting(1) < busy(2) ---
	// A=idle, C=waiting, B=busy => expected order A, C, B.
	wantOrder := []string{
		"aaaaaaaa-1111-2222-3333-444444444444",
		"cccccccc-9999-0000",
		"bbbbbbbb-5555",
	}
	for i, want := range wantOrder {
		if fleet[i].SessionID != want {
			t.Errorf("order[%d] = %q (status %q), want %q", i, fleet[i].SessionID, fleet[i].Status, want)
		}
	}
}

func TestListClaudeFleetFromMissingDir(t *testing.T) {
	tmp := t.TempDir()
	fleet, err := ListClaudeFleetFrom(filepath.Join(tmp, "nope"))
	if err != nil {
		t.Fatalf("expected nil error for missing sessions dir, got %v", err)
	}
	if len(fleet) != 0 {
		t.Fatalf("expected empty fleet, got %d", len(fleet))
	}
}
