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"
)

// TestUnroutedPromptDoesNotReportSuccess is the regression test for the
// original bug surfaced in run_1779247646933116000: a prompt the classifier
// can't categorise produced a run with status='completed' and zero tasks.
// The harness now must mark such a run as status='unrouted' with a typed
// failure_category, NOT 'completed'.
//
// The brief's invariant: "every prompt produces a closed loop — dispatched,
// clarified, or refused. Never silently completed."
func TestUnroutedPromptDoesNotReportSuccess(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)

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

	// Truly unclassifiable prompt: no imperative verb, no task reference,
	// no introspection trigger, no question mark. The deterministic rule
	// classifier must return kind=unknown and the run must take the
	// no-dispatch path. (The previous version of this test used a
	// "Let the frontend work identify itself" prompt — that's now
	// classified as KindIntrospection by SR2 since "identify itself"
	// triggers the fan-out rule.)
	prompt := "The weather in Paris."

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

	res, err := runlive.Run(ctx, st, bus, q, orch, runlive.Config{
		OrgPath:      orgPath,
		Prompt:       prompt,
		RuntimeMode:  "scripted",
		MaxWall:      10 * time.Second,
		StallTimeout: 8 * time.Second,
		ProjectRoot:  tmp,
	})
	if err != nil {
		t.Fatalf("runlive.Run: %v", err)
	}

	// 1. Run must NOT be 'completed' — that's the original bug.
	if res.Status == "completed" {
		t.Fatalf("run.status = completed for an unroutable prompt — this is the regression we are guarding against")
	}
	// 2. Status must be the typed 'unrouted' terminal state.
	if res.Status != "unrouted" {
		t.Errorf("run.status = %q, want \"unrouted\"", res.Status)
	}
	if res.FailureCategory != "unrouted" {
		t.Errorf("run.failure_category = %q, want \"unrouted\"", res.FailureCategory)
	}
	// 3. Zero tasks were dispatched — by definition of unrouted.
	if res.TasksTotal != 0 {
		t.Errorf("tasks_total = %d, want 0 (unrouted should not create tasks)", res.TasksTotal)
	}
	// 4. The user_notifications row must exist so the user sees the refusal.
	var notifs int
	_ = st.DB().QueryRow(`SELECT COUNT(*) FROM user_notifications WHERE run_id=? AND kind='unrouted'`, res.RunID).Scan(&notifs)
	if notifs < 1 {
		t.Errorf("no unrouted user_notifications row written — user is not informed of the refusal")
	}
	// 5. master.no_dispatch event was emitted (operator-visible audit trail).
	var noDispatch int
	_ = st.DB().QueryRow(`SELECT COUNT(*) FROM events WHERE run_id=? AND kind='master.no_dispatch'`, res.RunID).Scan(&noDispatch)
	if noDispatch < 1 {
		t.Errorf("master.no_dispatch event not emitted")
	}
	// 6. runs.root_task_id remains NULL — the schema invariant for unrouted.
	var rootTask *string
	_ = st.DB().QueryRow(`SELECT root_task_id FROM runs WHERE id=?`, res.RunID).Scan(&rootTask)
	if rootTask != nil {
		t.Errorf("root_task_id = %v, want NULL (unrouted must not have a root task)", *rootTask)
	}

	// 7. Cleanup invariant (L5): no agent for this run may still be 'running'
	// or 'spawning' after the run ends. The agents table must reflect the
	// real-world termination.
	var aliveAgents int
	_ = st.DB().QueryRow(
		`SELECT COUNT(*) FROM agents WHERE run_id=? AND status NOT IN ('terminated','failed','cleaned_up')`,
		res.RunID,
	).Scan(&aliveAgents)
	if aliveAgents != 0 {
		t.Errorf("after run ended, %d agents still show non-terminal status — agents table lies about termination", aliveAgents)
	}
	var terminatedAt int
	_ = st.DB().QueryRow(
		`SELECT COUNT(*) FROM agents WHERE run_id=? AND terminated_at IS NOT NULL`,
		res.RunID,
	).Scan(&terminatedAt)
	if terminatedAt == 0 {
		t.Errorf("no agent has terminated_at set — cleanup did not record termination times")
	}
}
