// @vitest-environment jsdom
/**
 * SET 57f — reporting liveness proof.
 *
 * Mounts the REAL App against a scripted extension-messaging seam and asserts
 * MID-RUN (running: true) that the surface actually reports: log rows with
 * tone classes, exactly one active pipeline stage, and the Run Report with
 * numbers. Three releases shipped green while the page showed nothing because
 * no test exercised the poll→render seam; this one failing blocks release.
 */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import { App } from '../web/src/App';

const T0 = '2026-07-27T10:00:00.000Z';
const EVENTS = [
  { ts: '2026-07-27T10:00:00.000Z', phase: 'encode', featureId: null, message: 'encoding prompt into feature-coded spec' },
  { ts: '2026-07-27T10:00:05.000Z', phase: 'ir', featureId: 'F1', message: 'frozen suite tests/f1.test.ts (clades 2:0; aggregate 2:0)' },
  { ts: '2026-07-27T10:00:09.000Z', phase: 'build', featureId: 'F1', message: 'built 1/1 owned files' },
];
const METRICS = {
  elapsedMs: 9000, live: true,
  featuresTotal: 2, featuresFrozenWithoutRedraw: 1, irRedraws: 1,
  nodesTotal: 2, nodesBuiltCleanFirstPass: 1,
  boundedLoopNodes: 1, boundedLoopIterations: 3,
  candidatesDrawn: 6, candidatesAccepted: 2,
  errorsTotal: 1, errorsParse: 1, errorsConformance: 0, errorsExecution: 0, errorsCrash: 0,
  deferredTotal: 1, deferredRelitigated: 1, deferredResolvedAfterRelitigation: 0, deferredPermanent: 1,
  nodesHealed: 1, fixesUnsuccessful: 2, fixesUnparseable: 0,
  tokensIn: 1200, tokensOut: 800, avgIn: 300, avgOut: 200, minIn: 100, minOut: 50, maxIn: 500, maxOut: 400,
};

/** Mutable script phase: 'live' → 'finished' (offscreen up) → 'record-only'
 * (offscreen torn down — the exact state every finished run lands in later). */
let phase: 'live' | 'finished' | 'record-only' = 'live';

function scriptedResponse(msg: { action?: string }): unknown {
  if (phase !== 'live') {
    switch (msg.action) {
      case 'tier1:live':
        return { liveId: null, running: false };
      case 'tier1:status':
        if (phase === 'record-only') throw new Error('no offscreen document');
        return {
          id: 'X', running: false, hasResult: true, prompt: 'demo prompt', model: 'mock-model',
          events: EVENTS, metrics: { ...METRICS, live: false }, mode: 'rha',
          cost: { usdTotal: 0.01 }, error: null,
        };
      case 'tier1:record':
        return {
          id: 'X', ts: T0, model: 'mock-model', prompt: 'demo prompt', mode: 'rha',
          events: EVENTS, metrics: { ...METRICS, live: false }, problems: 0,
          status: 'complete', finalized: true, running: false, cost: { usdTotal: 0.01 },
        };
      case 'tier1:runs':
        return { runs: [{ id: 'X', ts: T0, prompt: 'demo prompt', model: 'mock-model', status: 'complete' }] };
      case 'tier1:meta':
        return {
          unlocked: true, running: false, hasResult: phase === 'finished', vaultVersion: '1.7.35',
          active: { name: 'mock', type: 'local', model: 'mock-model', mode: 'chat' },
        };
      default:
        return {};
    }
  }
  switch (msg.action) {
    case 'tier1:meta':
      return {
        unlocked: true, running: true, hasResult: false, vaultVersion: '1.7.35',
        active: { name: 'mock', type: 'local', model: 'mock-model', mode: 'chat' },
      };
    case 'tier1:live':
      return { liveId: 'X', running: true };
    case 'tier1:status':
      return {
        id: 'X', running: true, prompt: 'demo prompt', model: 'mock-model',
        events: EVENTS, metrics: METRICS, mode: 'rha',
        cost: { usdTotal: 0.01 }, hasResult: false, error: null,
      };
    case 'tier1:runs':
      return { runs: [{ id: 'X', ts: T0, prompt: 'demo prompt', model: 'mock-model', status: 'running', running: true }] };
    default:
      return {};
  }
}

beforeEach(() => {
  phase = 'live';
  // Node's undici rejects relative URLs; the Tier-2 REST surface is not under
  // test here — serve every /api/* an empty-but-valid response.
  (globalThis as Record<string, unknown>).fetch = async (input: unknown) => {
    const url = String(input);
    const body = url.includes('/api/jobs') ? [] : {};
    return {
      ok: true,
      status: 200,
      json: async () => body,
      text: async () => JSON.stringify(body),
    } as unknown as Response;
  };
  window.__RHA_EXT__ = { version: '9.9.9', extensionId: 'test-ext' };
  (window as unknown as Record<string, unknown>).chrome = {
    runtime: {
      lastError: undefined,
      sendMessage: (_id: string, msg: { action?: string }, cb: (r: unknown) => void) => {
        try {
          const r = scriptedResponse(msg);
          setTimeout(() => cb(r), 0);
        } catch (e) {
          const rt = (window as unknown as { chrome: { runtime: { lastError?: unknown } } }).chrome.runtime;
          setTimeout(() => {
            rt.lastError = { message: e instanceof Error ? e.message : String(e) };
            cb(undefined);
            rt.lastError = undefined;
          }, 0);
        }
      },
    },
  };
  if (!Element.prototype.scrollIntoView) {
    Element.prototype.scrollIntoView = () => {};
  }
});

afterEach(() => {
  cleanup();
  delete window.__RHA_EXT__;
  delete (window as unknown as Record<string, unknown>).chrome;
});

describe('SET 57f — mid-run reporting liveness', () => {
  it('renders log rows, one active stage, and the Run Report while running', async () => {
    const { container } = render(<App />);

    // Mid-run: the pipeline log renders every scripted event with a tone class.
    await waitFor(() => {
      const rows = container.querySelectorAll('.logrow[class*="tone-"]');
      const texts = [...rows].map(r => r.textContent ?? '');
      expect(texts.some(t => t.includes('encoding prompt into feature-coded spec'))).toBe(true);
      expect(texts.some(t => t.includes('frozen suite tests/f1.test.ts'))).toBe(true);
      expect(texts.some(t => t.includes('built 1/1 owned files'))).toBe(true);
    }, { timeout: 5000 });

    // Stage stepper: exactly one active stage while running.
    await waitFor(() => {
      expect(container.querySelectorAll('.stage-active').length).toBe(1);
    }, { timeout: 5000 });

    // Run Report: scorecard tiles + category numbers from live metrics.
    await waitFor(() => {
      const report = container.querySelector('[data-rha-report="run"]');
      expect(report).toBeTruthy();
      const text = report!.textContent ?? '';
      expect(text).toContain('IR CONTRACT');
      expect(text).toContain('BUILD');
      expect(text).toContain('RHA');
      expect(text).toContain('50%'); // scoreIr 1/2 and scoreBuild 1/2
      expect(text).toContain('100%'); // scoreRha 1/1
      expect(text).toContain('BOUNDED LOOPS');
      expect(text).toContain('DEFERRED');
    }, { timeout: 5000 });

    // The screen never lies about liveness: running state shows, no
    // record-missing label for the live epoch.
    expect(screen.queryByText(/run record missing/i)).toBeNull();
  });

  it('FINISHED run keeps its full log on screen — with and without the offscreen document', async () => {
    const { container } = render(<App />);
    // Reach the live state first.
    await waitFor(() => {
      expect(container.querySelectorAll('.logrow[class*="tone-"]').length).toBeGreaterThanOrEqual(3);
    }, { timeout: 5000 });

    // Phase 2 — the run completes; offscreen still up. The log must NOT
    // vanish, and the done state must render.
    phase = 'finished';
    await waitFor(() => {
      const texts = [...container.querySelectorAll('.logrow')].map(r => r.textContent ?? '');
      expect(texts.some(t => t.includes('built 1/1 owned files'))).toBe(true);
      expect(screen.queryByText(/starting…|waiting for first event/i)).toBeNull();
    }, { timeout: 6000 });

    // Phase 3 — offscreen torn down (every finished run's long-term state).
    // The viewer must render the complete log from the persisted record.
    phase = 'record-only';
    await new Promise(r => setTimeout(r, 2500)); // let polls hit the dead offscreen
    const texts = [...container.querySelectorAll('.logrow')].map(r => r.textContent ?? '');
    expect(texts.some(t => t.includes('encoding prompt into feature-coded spec'))).toBe(true);
    expect(texts.some(t => t.includes('built 1/1 owned files'))).toBe(true);
    expect(screen.queryByText(/run record missing/i)).toBeNull();
    expect(screen.queryByText(/starting…/i)).toBeNull();
  });
});
