/**
 * SET 56a — live-truth sweep unit tests + mock e2e (storage / offscreen sim).
 */
import { describe, expect, it } from 'vitest';
import {
  SWEEP_AGE_MS,
  SWEEP_STALE_MSG,
  applySweepToRecord,
  isZombieRunning,
  sweepStaleRunningRecords,
  type SweepableRecord,
} from '../src/tier1/sweepStaleRunning';

const NOW = Date.parse('2026-07-27T12:00:00.000Z');
const FRESH = new Date(NOW - 30_000).toISOString(); // 30s ago
const STALE = new Date(NOW - SWEEP_AGE_MS - 5_000).toISOString(); // >120s ago

function rec(partial: Partial<SweepableRecord> & { id: string }): SweepableRecord {
  return {
    ts: STALE,
    events: [{ ts: STALE, phase: 'build', featureId: null, message: 'working' }],
    ...partial,
  };
}

describe('SET 56a sweepStaleRunning — unit', () => {
  it('spares the live id even when status=running and aged', () => {
    const live = rec({
      id: 'live-1',
      status: 'running',
      running: true,
      finalized: false,
      checkpointAt: STALE,
    });
    expect(isZombieRunning(live, 'live-1', NOW)).toBe(false);
    const out = sweepStaleRunningRecords([live], 'live-1', NOW);
    expect(out.changed).toBe(false);
    expect(out.records[0].status).toBe('running');
    expect(out.records[0].sweptAt).toBeUndefined();
  });

  it('spares a non-live record with checkpoint < 120s old', () => {
    const young = rec({
      id: 'young-1',
      status: 'running',
      running: true,
      finalized: false,
      checkpointAt: FRESH,
      ts: FRESH,
    });
    expect(isZombieRunning(young, null, NOW)).toBe(false);
    const out = sweepStaleRunningRecords([young], null, NOW);
    expect(out.changed).toBe(false);
    expect(out.records[0].status).toBe('running');
  });

  it('converts a stale non-live running record to interrupted with sweptAt + exact message', () => {
    const zombie = rec({
      id: 'zombie-1',
      status: 'running',
      running: true,
      finalized: false,
      checkpointAt: STALE,
      ts: STALE,
      events: [{ ts: STALE, phase: 'build', featureId: 'f1', message: 'prior' }],
    });
    expect(isZombieRunning(zombie, null, NOW)).toBe(true);
    const out = sweepStaleRunningRecords([zombie], null, NOW);
    expect(out.changed).toBe(true);
    const swept = out.records[0];
    expect(swept.status).toBe('interrupted');
    expect(swept.running).toBe(false);
    expect(swept.finalized).toBe(true);
    expect(swept.sweptAt).toBe(new Date(NOW).toISOString());
    expect(swept.events![0].message).toBe('prior'); // prior events untouched / not reordered
    expect(swept.events![swept.events!.length - 1]).toEqual({
      ts: new Date(NOW).toISOString(),
      phase: 'done',
      featureId: null,
      message: SWEEP_STALE_MSG,
    });
    expect(SWEEP_STALE_MSG).toBe(
      'interrupted — swept as stale (no live job, checkpoint aged out)',
    );
  });

  it('is idempotent — second call is a no-op', () => {
    const zombie = rec({
      id: 'zombie-2',
      status: 'running',
      running: true,
      finalized: false,
      checkpointAt: STALE,
    });
    const first = sweepStaleRunningRecords([zombie], null, NOW);
    expect(first.changed).toBe(true);
    const second = sweepStaleRunningRecords(first.records, null, NOW);
    expect(second.changed).toBe(false);
    expect(second.records).toEqual(first.records);
    // applySweep twice does not duplicate the sweep event
    const again = applySweepToRecord(first.records[0], new Date(NOW).toISOString());
    expect(again.events!.filter(e => e.message === SWEEP_STALE_MSG)).toHaveLength(1);
  });

  it('treats finalized!==true + unset status as zombie when aged', () => {
    const orphan = rec({
      id: 'orphan-1',
      // status unset
      finalized: false,
      checkpointAt: STALE,
    });
    expect(isZombieRunning(orphan, 'other', NOW)).toBe(true);
  });
});

describe('SET 56a mock e2e — kill offscreen, restart SW', () => {
  /**
   * Simulates: start run → persist running checkpoint → kill offscreen
   * (liveId=null) → SW restart sweep on tier1:runs. Viewer sees interrupted,
   * zero running chips, tier1:live = { liveId: null, running: false }.
   */
  it('after offscreen death, sweep marks interrupted and live is null', () => {
    type ListEntry = { id: string; ts: string; title: string; status: string | null };
    const store = new Map<string, SweepableRecord>();
    let list: ListEntry[] = [];

    // Start run + checkpoint (SET 51 register-at-start).
    const id = 'run-kill';
    const started = STALE;
    const runningRec: SweepableRecord = {
      id,
      ts: started,
      status: 'running',
      running: true,
      finalized: false,
      checkpointAt: started,
      events: [
        { ts: started, phase: 'encode', featureId: null, message: 'started' },
        { ts: started, phase: 'build', featureId: null, message: 'checkpoint' },
      ],
    };
    store.set(`tier1Run:${id}`, runningRec);
    list = [{ id, ts: started, title: 'kill mid', status: 'running' }];

    // Kill offscreen → no in-memory job.
    const live = { liveId: null as string | null, running: false };
    expect(live).toEqual({ liveId: null, running: false });

    // SW restart / tier1:runs top → sweep.
    const records = [...store.values()];
    const swept = sweepStaleRunningRecords(records, live.liveId, NOW);
    expect(swept.changed).toBe(true);
    for (const r of swept.records) {
      store.set(`tier1Run:${r.id}`, r);
      list = list.map(e => (e.id === r.id ? { ...e, status: r.status ?? null } : e));
    }

    // tier1:runs shows interrupted
    expect(list.every(e => e.status === 'interrupted')).toBe(true);
    expect(store.get(`tier1Run:${id}`)?.status).toBe('interrupted');
    expect(store.get(`tier1Run:${id}`)?.finalized).toBe(true);
    expect(store.get(`tier1Run:${id}`)?.sweptAt).toBeTruthy();

    // Zero running chips: viewer rule is entry.id === liveId
    const runningChips = list.filter(e => e.id === live.liveId);
    expect(runningChips).toHaveLength(0);

    // tier1:live stays null/false with no offscreen
    expect(live).toEqual({ liveId: null, running: false });
  });
});
