package runlive_test

import (
	"context"
	"path/filepath"
	"testing"
	"time"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/event"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/orchestrator"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/runlive"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/store"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/transport"
)

// TestOrchestration_HierarchicalScripted is the regression net for "the full
// loop closes." It runs hierarchical-v1.yaml under the scripted-auto runtime,
// dispatches a real prompt, and asserts on the event timeline that the
// orchestration produced. If a future change breaks the loop (e.g. another
// deadlock), this test fails immediately.
func TestOrchestration_HierarchicalScripted(t *testing.T) {
	tmp := t.TempDir()
	st, err := store.Open(filepath.Join(tmp, "harness.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer st.Close()
	bus := event.NewBus(st)
	q := transport.New(st, bus)
	orch := orchestrator.New(st, bus)

	// Collect every event into a slice for post-hoc assertions.
	sub, cancel := bus.Subscribe(2048)
	defer cancel()
	var events []event.Event
	collectorDone := make(chan struct{})
	go func() {
		defer close(collectorDone)
		for ev := range sub {
			events = append(events, ev)
		}
	}()

	root := repoRoot(t)
	orgPath := filepath.Join(root, ".td", "orgs", "hierarchical-v1.yaml")

	// Sandbox into a temp ProjectRoot so this test never writes to the
	// real repo. Production-grade isolation: tests are hermetic by
	// construction, not by hope.
	testRoot := filepath.Join(tmp, "project")

	ctx, ctxCancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer ctxCancel()

	res, err := runlive.Run(ctx, st, bus, q, orch, runlive.Config{
		OrgPath:      orgPath,
		Prompt:       "add a tooltip to the dashboard",
		RuntimeMode:  "scripted",
		MaxWall:      10 * time.Second,
		StallTimeout: 10 * time.Second,
		ProjectRoot:  testRoot,
	})
	if err != nil {
		t.Fatalf("runlive.Run: %v", err)
	}

	cancel()
	<-collectorDone

	// (1) Run completed (no kill_reason).
	if res.Status != "completed" {
		t.Errorf("run status = %s (kill=%s), want completed", res.Status, res.KillReason)
	}
	// (2) Exactly 1 task created and 1 task done.
	if res.TasksTotal != 1 || res.TasksDone != 1 {
		t.Errorf("tasks = %d done / %d total, want 1/1", res.TasksDone, res.TasksTotal)
	}

	// (3) Required event kinds in the timeline. If any of these go missing,
	// orchestration is broken in a specific way.
	required := []event.Kind{
		event.KindRunStarted,
		"run.runtime_selected",
		"agent.spawn.routing",
		"agent.spawn.calling_runtime",
		event.KindAgentSpawned,
		event.KindTaskCreated,
		event.KindTaskAssigned,
		event.KindTaskStateChanged,
		event.KindMessageEnqueued,
		event.KindMessageDelivered,
		"agent.handle_one.started",
		"agent.llm.call_started",
		"agent.llm.call_ended",
		"agent.tool.called",
		"agent.handle_one.acked",
		event.KindMessageAcked,
		event.KindTaskCompleted,
		event.KindRunEnded,
	}
	seen := map[event.Kind]int{}
	for _, ev := range events {
		seen[ev.Kind]++
	}
	for _, k := range required {
		if seen[k] == 0 {
			t.Errorf("missing required event: %s (seen counts: %v)", k, seen)
		}
	}

	// (4) No spawn.failed, no stalled, no policy violations.
	for _, bad := range []event.Kind{
		"agent.spawn.failed", "run.stalled", "run.spawn_failed",
		event.KindCoordinationLoop, event.KindCoordinationThrash,
		"agent.llm.call_failed", "agent.tool.failed",
	} {
		if seen[bad] > 0 {
			t.Errorf("unexpected event %s (count=%d)", bad, seen[bad])
		}
	}

	// (5) Five roles spawned (master + fe-lead + fe-worker + be-lead + be-worker).
	if seen[event.KindAgentSpawned] != 5 {
		t.Errorf("agent.spawned count = %d, want 5", seen[event.KindAgentSpawned])
	}

	// (6) Master ProcessInbox emitted task.completed (via Transition).
	var taskCompletedRun string
	for _, ev := range events {
		if ev.Kind == event.KindTaskCompleted {
			taskCompletedRun = ev.RunID
			break
		}
	}
	if taskCompletedRun != res.RunID {
		t.Errorf("task.completed event run_id = %q, want %q", taskCompletedRun, res.RunID)
	}
}

// TestOrchestration_WatchdogKillsStalledRun verifies the watchdog fires when
// nothing emits events on a run. We simulate this by *not* dispatching a
// prompt — there will be no event traffic, the watchdog should fire.
//
// Skipped: hard to test cleanly without making the runlive API allow
// "spawn agents but don't dispatch." Left as a placeholder — the integration
// path above already verifies the happy case; the watchdog is exercised by
// the deadlock-class repro from the bug report.

func repoRoot(t *testing.T) string {
	t.Helper()
	wd, _ := osGetwd()
	root := wd
	for i := 0; i < 6; i++ {
		if pathExists(filepath.Join(root, ".td", "orgs", "hierarchical-v1.yaml")) {
			return root
		}
		root = filepath.Dir(root)
	}
	t.Fatalf("could not find repo root from %s", wd)
	return ""
}
