package main

// P8-REVIEW axis 3 probe. Reproduces the SCHEDULE half of the sweep composition: `sweep`'s tick runs
// its five passes one after another on ONE goroutine, so the RUNS pass — the only thing that settles
// money, restarts a run a reboot lost and re-issues a stop the systemd call dropped — runs once per
// (sum of every pass's duration), not once per TM_PLATFORM_SWEEP_EVERY.
//
// Scaled, and only in the direction that makes it observable: the readmodel pass here is slowed by a
// fake engine that takes 400 ms per book instead of the real one's minutes, and the tick is 100 ms
// instead of 15 s. No constant of the zone is changed. The real numbers substitute directly:
// refreshSweepBudget 10m + intakeSweepBudget 21m + two 2m passes on top of the runs pass's own 2m.
//
// The runs pass is counted by the one side effect it has on a stopped run: `reconcile` re-issues
// `Runner.Stop` on every pass while the unit is alive and an intent stands (reconcile.go:387-397).

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"log/slog"
	"net/url"
	"os"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/jackc/pgx/v5"

	"textmachine/platform/internal/ingest"
	"textmachine/platform/internal/metrics"
	"textmachine/platform/internal/money"
	"textmachine/platform/internal/pgstore"
	"textmachine/platform/internal/readmodel"
	"textmachine/platform/internal/runner"
	"textmachine/platform/internal/runs"
)

type countingRunner struct{ stops atomic.Int64 }

func (c *countingRunner) Start(context.Context, runner.Spec) error { return nil }
func (c *countingRunner) Stop(context.Context, string) error       { c.stops.Add(1); return nil }
func (c *countingRunner) Alive(context.Context, string) (bool, error) {
	return true, nil
}

// slowEngine is a readmodel engine that answers, slowly, and then says no. Slow enough to dominate a
// pass and fast enough for a test — the real one is bounded by readmodel.MaterializeBudget (5 min).
type slowEngine struct{ d time.Duration }

func (s slowEngine) Manifest(ctx context.Context, binary, workdir string) (ingest.Manifest, error) {
	select {
	case <-time.After(s.d):
	case <-ctx.Done():
	}
	return ingest.Manifest{}, os.ErrDeadlineExceeded
}
func (s slowEngine) Export(ctx context.Context, binary, workdir string) (ingest.Export, error) {
	return ingest.Export{}, os.ErrDeadlineExceeded
}

func probeStore(t *testing.T) (*pgstore.Store, context.Context) {
	t.Helper()
	admin := os.Getenv("TM_PLATFORM_TEST_DSN")
	if admin == "" {
		t.Skip("TM_PLATFORM_TEST_DSN not set")
	}
	ctx := t.Context()
	var suffix [6]byte
	if _, err := rand.Read(suffix[:]); err != nil {
		t.Fatal(err)
	}
	name := "tm_p8rev_sweep_" + hex.EncodeToString(suffix[:])
	conn, err := pgx.Connect(ctx, admin)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := conn.Exec(ctx, "create database "+pgx.Identifier{name}.Sanitize()); err != nil {
		conn.Close(ctx)
		t.Skipf("cannot create a scratch database: %v", err)
	}
	t.Cleanup(func() {
		c, cancel := context.WithTimeout(context.Background(), 15*time.Second)
		defer cancel()
		_, _ = conn.Exec(c, "drop database if exists "+pgx.Identifier{name}.Sanitize()+" with (force)")
		conn.Close(c)
	})
	u, _ := url.Parse(admin)
	u.Path = "/" + name
	if err := pgstore.Migrate(ctx, u.String()); err != nil {
		t.Fatal(err)
	}
	s, err := pgstore.Open(ctx, u.String())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(s.Close)
	return s, ctx
}

// oneStoppedRun leaves the database with exactly one live run whose stop is on file and whose unit
// still exists: the shape whose only cure is the sweep re-issuing the signal.
func oneStoppedRun(t *testing.T, db *pgstore.Store, ctx context.Context, now time.Time) {
	t.Helper()
	if _, err := db.Pool().Exec(ctx, `insert into users (id, email) values ('u1','u1@example.org')`); err != nil {
		t.Fatal(err)
	}
	amount, _ := money.ParseUSD("50")
	if _, err := db.Grant(ctx, "u1", amount, "test", "seed", "", now); err != nil {
		t.Fatal(err)
	}
	bookID, err := db.AddBook(ctx, pgstore.NewBook{OwnerID: "u1", Title: "b", SourceLang: "zh",
		TargetLang: "ru", ChapterCount: 100, Workdir: t.TempDir(), Now: now})
	if err != nil {
		t.Fatal(err)
	}
	started, err := db.StartRun(ctx, pgstore.StartRunInput{UserID: "u1", BookID: bookID,
		CeilingChapters: 10, Ceiling: money.MicroUSD(1_000_000), Now: now}, 0,
		func(context.Context, pgstore.Tx, string) error { return nil })
	if err != nil {
		t.Fatal(err)
	}
	ok, err := db.RecordSpawn(ctx, pgstore.SpawnRecord{AttemptID: started.AttemptID,
		Unit: "tm-run-x-1", Binary: "/opt/tm/tmctl", EngineRunID: pgstore.EngineStreamID(started.ID, 1),
		Ceiling: money.MicroUSD(1_000_000), CeilingArg: money.MicroUSD(1_000_000), Baseline: 0})
	if err != nil || !ok {
		t.Fatalf("record spawn: %v %v", ok, err)
	}
	if _, _, err := db.RequestStop(ctx, "u1", started.ID, now); err != nil {
		t.Fatal(err)
	}
}

// booksOwingAReadingSurface leaves n quiet books whose surface is due, which is what the readmodel
// pass works through.
func booksOwingAReadingSurface(t *testing.T, db *pgstore.Store, ctx context.Context, now time.Time, n int) {
	t.Helper()
	for range n {
		id, err := db.AddBook(ctx, pgstore.NewBook{OwnerID: "u1", Title: "owed", SourceLang: "zh",
			TargetLang: "ru", ChapterCount: 10, Workdir: t.TempDir(), Now: now})
		if err != nil {
			t.Fatal(err)
		}
		if _, err := db.Pool().Exec(ctx,
			`update books set read_model_owed_at = now() where id = $1`, id); err != nil {
			t.Fatal(err)
		}
	}
}

func TestProbeTheRunsPassRunsOncePerSumOfEveryPass(t *testing.T) {
	const window = 4 * time.Second
	const tick = 100 * time.Millisecond
	run := func(t *testing.T, withReadModel bool) int64 {
		db, ctx := probeStore(t)
		now := time.Now().UTC().Truncate(time.Millisecond)
		oneStoppedRun(t, db, ctx, now)
		rn := &countingRunner{}
		svc := &runs.Service{Store: db, Runner: rn, Now: func() time.Time { return now }}
		s := sweeps{runs: svc, db: db, metrics: metrics.New()}
		if withReadModel {
			booksOwingAReadingSurface(t, db, ctx, now, 8)
			s.reader = &readmodel.Service{Store: db, Engine: slowEngine{400 * time.Millisecond},
				Binary: "/opt/tm/tmctl"}
		}
		c, cancel := context.WithCancel(ctx)
		var wg sync.WaitGroup
		wg.Add(1)
		go func() { defer wg.Done(); sweep(c, s, tick, 2*time.Minute, slog.New(slog.DiscardHandler)) }()
		time.Sleep(window)
		cancel()
		wg.Wait()
		return rn.stops.Load()
	}
	var alone, behind int64
	t.Run("runs pass alone", func(t *testing.T) { alone = run(t, false) })
	t.Run("runs pass behind a busy readmodel pass", func(t *testing.T) { behind = run(t, true) })
	t.Logf("in %v at a %v tick: runs passes alone = %d, behind the readmodel pass = %d",
		window, tick, alone, behind)
	if behind >= alone {
		t.Logf("NOT REPRODUCED: the runs pass kept its cadence")
	}
}
