// Integration tests for the limiter limp path's stale-reclaim-only claim
// (ReclaimStaleNext). They run against a live TiDB (skipped when unreachable, the
// same as the other *_integration tests) and prove the two over-claim-safety
// properties the limp design relies on:
//
//  1. ReclaimStaleNext claims ONLY a stale-processing row, never a fresh pending
//     row — even when the pending row is OLDER (which a normal ClaimNext would
//     pick first). A degraded tick recovers a stranded lease without admitting
//     new work past maxProcessingRows.
//  2. With no reclaimable stale row, ReclaimStaleNext claims NOTHING; it never
//     falls through to admit a pending/failed row.
//
// The candidate SELECT, ordering guard, ORDER BY and the UPDATE-side CAS are
// identical to ClaimNext — only the status predicate is narrowed — so a passing
// ClaimNext after the reclaim also proves the normal path is untouched.
package connector_events

import (
	"context"
	"testing"
	"time"
)

func TestReclaimStaleNext_ReclaimsStaleNotOlderPending_Integration(t *testing.T) {
	db, cleanup := openTiDB(t)
	defer cleanup()
	if _, err := db.Exec(laneHeadTestDDL); err != nil {
		t.Fatalf("create table: %v", err)
	}
	ctx := context.Background()

	const pack = "pack-limp"
	base := time.Date(2026, 6, 25, 10, 0, 0, 0, time.UTC)
	now := base.Add(100000 * time.Second)
	staleCutoff := now.Add(-StaleProcessingSeconds * time.Second)

	// P: an OLDER pending row (earliest CreatedDate), in its own lane. A normal
	// ClaimNext would pick this first. ReclaimStaleNext must NOT touch it.
	if _, err := db.ExecContext(ctx, `INSERT INTO _System_ConnectorEvent
		(Id, Namespace, InstalledPackId, EventKind, IdempotencyKey, Status, RouteVersion,
		 DeliverySequence, EnqueueOrdinal, SourceLaneKey, CreatedDate, LastModifiedDate)
		VALUES (?, 'ns', ?, 'webhook.event', ?, 'pending', 1, 0, 1, ?, ?, ?)`,
		"P-pending", pack, "ik-P", "lane-P", base, base); err != nil {
		t.Fatalf("insert pending P: %v", err)
	}
	// S: a stale-processing row in a different lane, CreatedDate LATER than P, with
	// LastModifiedDate before the stale cutoff (reclaimable).
	staleLMD := staleCutoff.Add(-time.Hour)
	if _, err := db.ExecContext(ctx, `INSERT INTO _System_ConnectorEvent
		(Id, Namespace, InstalledPackId, EventKind, IdempotencyKey, Status, RouteVersion,
		 DeliverySequence, EnqueueOrdinal, SourceLaneKey, ClaimedBy, ClaimToken, CreatedDate, LastModifiedDate)
		VALUES (?, 'ns', ?, 'webhook.event', ?, 'processing', 1, 0, 2, ?, 'dead-worker', 'old-token', ?, ?)`,
		"S-stale", pack, "ik-S", "lane-S", base.Add(time.Hour), staleLMD); err != nil {
		t.Fatalf("insert stale S: %v", err)
	}

	svc := NewService(db, deterministicID("reclaim-token", "normal-token"), func() time.Time { return now })

	// ReclaimStaleNext must pick S (stale), NOT the older pending P.
	ev, ok, err := svc.ReclaimStaleNext(ctx, pack, "limp-worker")
	if err != nil {
		t.Fatalf("ReclaimStaleNext: %v", err)
	}
	if !ok || ev == nil {
		t.Fatalf("ReclaimStaleNext: ok=%v ev=%v, want it to reclaim the stale row", ok, ev)
	}
	if ev.ID != "S-stale" {
		t.Fatalf("ReclaimStaleNext claimed %q, want S-stale (must reclaim stale, never the older pending P)", ev.ID)
	}

	// P is untouched: a normal ClaimNext now claims it, proving ReclaimStaleNext
	// admitted no new work — it left the pending row alone.
	ev2, ok2, err := svc.ClaimNext(ctx, pack, "normal-worker")
	if err != nil {
		t.Fatalf("ClaimNext after reclaim: %v", err)
	}
	if !ok2 || ev2 == nil || ev2.ID != "P-pending" {
		t.Fatalf("ClaimNext after reclaim = %v ok=%v, want P-pending untouched", ev2, ok2)
	}
}

func TestReclaimStaleNext_NoStaleRowsClaimsNothing_Integration(t *testing.T) {
	db, cleanup := openTiDB(t)
	defer cleanup()
	if _, err := db.Exec(laneHeadTestDDL); err != nil {
		t.Fatalf("create table: %v", err)
	}
	ctx := context.Background()

	const pack = "pack-nostale"
	base := time.Date(2026, 6, 25, 10, 0, 0, 0, time.UTC)
	now := base.Add(100000 * time.Second)

	// Only fresh pending/failed rows — nothing reclaimable.
	if _, err := db.ExecContext(ctx, `INSERT INTO _System_ConnectorEvent
		(Id, Namespace, InstalledPackId, EventKind, IdempotencyKey, Status, RouteVersion,
		 DeliverySequence, EnqueueOrdinal, SourceLaneKey, CreatedDate, LastModifiedDate)
		VALUES (?, 'ns', ?, 'webhook.event', ?, 'pending', 1, 0, 1, ?, ?, ?)`,
		"P-only", pack, "ik-Ponly", "lane-P", base, base); err != nil {
		t.Fatalf("insert pending: %v", err)
	}

	svc := NewService(db, deterministicID("unused-token"), func() time.Time { return now })

	ev, ok, err := svc.ReclaimStaleNext(ctx, pack, "limp-worker")
	if err != nil {
		t.Fatalf("ReclaimStaleNext: %v", err)
	}
	if ok || ev != nil {
		t.Fatalf("ReclaimStaleNext claimed %v (ok=%v) with no stale rows — must claim nothing, never a pending row", ev, ok)
	}
}
