package runs

// P8-REVIEW axis 3 probe. READ-ONLY over the zone: this file adds no production code and is not a
// proposed pin — it is the reproduction of two claims about the SETTLEMENT half of the P8-FIX
// treatment, run against the same fixture the zone's own tests use.
//
// Claim A: a settlement that fails CHEAPLY (the engine answers, and its report carries no committed
// figure — "absent is not zero", PD-40) is retried on EVERY pass forever. Each retry is a real
// `tmctl status` against the book: seconds of engine CPU every 15 s, for as long as the row exists.
// Nothing counts it, so `reconcile_failures` stays 0 and StalledAfter is unreachable.
//
// Claim B: even a settlement that IS counted — deferred to the threshold — is invisible to every
// operator surface the pack built: `StalledRuns` and the `tm_platform_runs_stalled` gauge both join
// `a.ended_at is null and r.finished_at is null`, and `AbandonRun` refuses a finished run outright.

import (
	"errors"
	"testing"
	"time"

	"textmachine/platform/internal/ingest"
	"textmachine/platform/internal/pgstore"
	"textmachine/platform/internal/runner"
)

func TestProbeACheapSettlementFailureIsRetriedForeverAndNeverCounted(t *testing.T) {
	f := newFixture(t, "20", 500)
	run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), CeilingChapters: 10})
	if err != nil {
		t.Fatal(err)
	}
	if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
		t.Fatal(err)
	}
	a := f.lastAttempt(t, run.ID)
	if err := writeExitMarker(t, f, run.ID, a); err != nil {
		t.Fatal(err)
	}
	// The engine ANSWERS, quickly, and its report carries no committed figure. This is the state the
	// zone's own fixture uses for "the money cannot be closed" (stalled_test.go:74, :613).
	f.engine.set(ingest.StatusReport{TotalUnits: 10, Done: 10}, nil)
	if err := f.svc.Sweep(f.ctx); err != nil {
		t.Fatal(err)
	}
	open, err := f.store.UnsettledRuns(f.ctx, f.svc.now())
	if err != nil {
		t.Fatal(err)
	}
	if len(open) != 1 {
		t.Fatalf("%d unsettled runs, want the one this probe is about", len(open))
	}
	// Five more passes, the clock moving so nothing is skipped for the wrong reason.
	before := f.engine.called()
	for i := range 5 {
		later := f.now.Add(time.Duration(i+1) * time.Minute)
		f.svc.Now = func() time.Time { return later }
		if err := f.svc.Sweep(f.ctx); err != nil {
			t.Fatal(err)
		}
	}
	t.Logf("engine calls over five passes of one unsettleable run: %d", f.engine.called()-before)
	var failures int
	var after *time.Time
	if err := f.store.Pool().QueryRow(f.ctx,
		`select reconcile_failures, reconcile_after from run_attempts where id = $1`, open[0].AttemptID).
		Scan(&failures, &after); err != nil {
		t.Fatal(err)
	}
	t.Logf("reconcile_failures=%d reconcile_after=%v after five passes", failures, after)
	stalled, err := f.store.StalledRuns(f.ctx, StalledAfter)
	if err != nil {
		t.Fatal(err)
	}
	o, err := f.store.Observe(f.ctx, StalledAfter)
	if err != nil {
		t.Fatalf("observe: %v", err)
	}
	t.Logf("StalledRuns(%d)=%d gauge=%d oldest_hold_seconds=%.0f",
		StalledAfter, len(stalled), o.StalledRuns, o.OldestHoldSeconds)
	if failures != 0 {
		t.Logf("NOT REPRODUCED: the cheap failure IS counted")
	}
}

func TestProbeAStalledSettlementIsInvisibleToEveryOperatorSurface(t *testing.T) {
	f := newFixture(t, "20", 500)
	run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), CeilingChapters: 10})
	if err != nil {
		t.Fatal(err)
	}
	if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
		t.Fatal(err)
	}
	a := f.lastAttempt(t, run.ID)
	if err := writeExitMarker(t, f, run.ID, a); err != nil {
		t.Fatal(err)
	}
	f.engine.set(ingest.StatusReport{TotalUnits: 10, Done: 10}, nil)
	if err := f.svc.Sweep(f.ctx); err != nil {
		t.Fatal(err)
	}
	open, err := f.store.UnsettledRuns(f.ctx, f.svc.now())
	if err != nil {
		t.Fatal(err)
	}
	if len(open) != 1 {
		t.Fatalf("%d unsettled runs", len(open))
	}
	// Now count it to the threshold the way an EXPENSIVE settlement wedge would: the settlement phase
	// and the reconciliation phase share `deferItem`, so this is the same write settleOne performs.
	for i := 1; i <= StalledAfter; i++ {
		got, err := f.store.DeferRun(f.ctx, open[0].AttemptID, f.svc.now().Add(backoff(i)), "the engine did not answer")
		if err != nil {
			t.Fatal(err)
		}
		if got != i {
			t.Fatalf("after %d deferrals the row counts %d", i, got)
		}
	}
	// The three surfaces the pack built for exactly this state.
	stalled, err := f.store.StalledRuns(f.ctx, StalledAfter)
	if err != nil {
		t.Fatal(err)
	}
	all, err := f.store.StalledRuns(f.ctx, 0)
	if err != nil {
		t.Fatal(err)
	}
	o, err := f.store.Observe(f.ctx, StalledAfter)
	if err != nil {
		t.Fatal(err)
	}
	abandon := f.store.AbandonRun(f.ctx, run.ID, "the engine will never answer", true, f.now)
	t.Logf("attempt %d: reconcile_failures=%d", open[0].AttemptID, StalledAfter)
	t.Logf("tmplatformctl runs --stalled  -> StalledRuns(%d) = %d rows", StalledAfter, len(stalled))
	t.Logf("tmplatformctl runs            -> StalledRuns(0)  = %d rows", len(all))
	t.Logf("tm_platform_runs_stalled gauge -> %d", o.StalledRuns)
	t.Logf("tm_platform_oldest_hold_seconds -> %.0f", o.OldestHoldSeconds)
	t.Logf("tmplatformctl run abandon      -> %v (ErrNoRun=%v)", abandon, errors.Is(abandon, pgstore.ErrNoRun))
	if len(stalled) != 0 || o.StalledRuns != 0 {
		t.Logf("NOT REPRODUCED: the stalled settlement IS visible")
	}
}

// writeExitMarker ends an attempt's unit cleanly, the way the zone's own tests do.
func writeExitMarker(t *testing.T, f *fixture, runID string, a pgstore.LiveRun) error {
	t.Helper()
	return runner.WriteMarker(f.svc.markerPath(runID, a.AttemptNo),
		runner.Marker{Unit: a.UnitName, Result: "exit-code", Code: "exited", Status: "0"})
}

// Claim C: `run abandon`'s SECOND guard is not readable in advance from the operator's own table.
// AbandonRun refuses on `unit_name != ” OR spend_baseline is not null` (pgstore/runs.go:465). The
// table prints the first (UNIT, and listRuns says so in as many words at runs.go:223-227) and never
// the second: the baseline reaches the operator only inverted, as SPENT being a figure rather than
// "?". So the row an operator reads as "no unit, safe to abandon" is refused with a message naming a
// unit the table said did not exist.
func TestProbeTheAbandonsSecondGuardIsNotReadableFromTheOperatorsTable(t *testing.T) {
	f := newFixture(t, "20", 500)
	run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), CeilingChapters: 10})
	if err != nil {
		t.Fatal(err)
	}
	if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
		t.Fatal(err)
	}
	// `ReleaseSpawnClaim` clears the name when the unit could NOT be created and deliberately keeps
	// the baseline as the tombstone (spawn.go:130, RecordSpawn). This is that row.
	if _, err := f.store.Pool().Exec(f.ctx,
		`update run_attempts set unit_name = null, reconcile_failures = $2 where run_id = $1`,
		run.ID, StalledAfter); err != nil {
		t.Fatal(err)
	}
	rows, err := f.store.StalledRuns(f.ctx, StalledAfter)
	if err != nil {
		t.Fatal(err)
	}
	if len(rows) != 1 {
		t.Fatalf("%d rows", len(rows))
	}
	unit := rows[0].UnitName
	if unit == "" {
		unit = "(none)" // exactly what cmd/tmplatformctl/runs.go prints
	}
	spent := "?"
	if rows[0].SpentMicroUSD != nil {
		spent = "a figure"
	}
	err = f.store.AbandonRun(f.ctx, run.ID, "the host is gone", false, f.now)
	t.Logf("the operator's row: UNIT=%s SPENT=%s FAILS=%d", unit, spent, rows[0].Failures)
	t.Logf("tmplatformctl run abandon on that row -> %v", err)
	t.Logf("refused = %v", errors.Is(err, pgstore.ErrRunMayHaveAProcess))
}
