// @vitest-environment jsdom
/**
 * SET 58 — the new shell's liveness proof: live → finished → record-only, plus
 * the first law (a render crash shows a crash card, never a blank page).
 */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import { Shell } from '../web/src/shell/Shell';
import { CrashCard } from '../web/src/shell/ErrorShield';
import { __resetStoreForTests } from '../web/src/shell/store';

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,
  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,
};

let phase: 'live' | 'finished' | 'record-only' = 'live';
/** SET 58c — dead-channel scenarios (Andy's machine). */
let channelMode: 'normal' | 'live-dead' | 'both-dead' = 'normal';
let checkpointEvents: typeof EVENTS = [];

function scripted(msg: { action?: string }): unknown {
  if (channelMode !== 'normal') {
    switch (msg.action) {
      case 'tier1:meta':
        return { unlocked: true, running: false, vaultVersion: '1.7.38', active: { name: 'mock', type: 'local', model: 'mock-model', mode: 'chat' } };
      case 'tier1:live':
        return { liveId: null, running: false }; // the broken 56a channel
      case 'tier1:status':
        if (channelMode === 'both-dead') throw new Error('status channel dead');
        return { id: 'X', running: true, prompt: 'demo prompt', model: 'mock-model', mode: 'rha', events: EVENTS, metrics: METRICS, cost: { usdTotal: 0.01 }, error: null };
      case 'tier1:record':
        return { id: 'X', ts: T0, model: 'mock-model', prompt: 'demo prompt', mode: 'rha', events: checkpointEvents, metrics: METRICS, status: 'running', running: true, finalized: false };
      case 'tier1:runs':
        return { runs: [{ id: 'X', ts: T0, prompt: 'demo prompt', model: 'mock-model', status: 'running' }] };
      default:
        return {};
    }
  }
  switch (msg.action) {
    case 'tier1:meta':
      return {
        unlocked: true, running: phase === 'live', vaultVersion: '1.7.36',
        active: { name: 'mock', type: 'local', model: 'mock-model', mode: 'chat' },
      };
    case 'tier1:live':
      return phase === 'live' ? { liveId: 'X', running: true } : { liveId: null, running: false };
    case 'tier1:status':
      if (phase === 'record-only') throw new Error('no offscreen document');
      return {
        id: 'X', running: phase === 'live', hasResult: phase === 'finished',
        prompt: 'demo prompt', model: 'mock-model', mode: 'rha',
        events: EVENTS, metrics: METRICS, 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, 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: phase === 'live' ? 'running' : 'complete',
        }],
      };
    default:
      return {};
  }
}

beforeEach(() => {
  __resetStoreForTests();
  phase = 'live';
  channelMode = 'normal';
  checkpointEvents = [];
  (globalThis as Record<string, unknown>).fetch = async () => ({
    ok: true, status: 200, json: async () => ({}), text: async () => '{}',
  }) 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 = scripted(msg);
          setTimeout(() => cb(r), 0);
        } catch (e) {
          // chrome reports handler throws via lastError + undefined response
          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);
        }
      },
    },
  };
});

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

describe('SET 58 — shell liveness', () => {
  it('live → finished → record-only all keep the full log + report on screen', async () => {
    const { container } = render(<Shell />);

    // LIVE: rows, tones, one active stage, report numbers.
    await waitFor(() => {
      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(container.querySelectorAll('.stage-active').length).toBe(1);
      expect(container.querySelector('[data-rha-shell="run-report"]')?.textContent).toContain('IR CONTRACT');
    }, { timeout: 5000 });

    // FINISHED (offscreen still up): log intact, no lying empty labels.
    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(/waiting for first event/i)).toBeNull();
    }, { timeout: 6000 });

    // RECORD-ONLY (offscreen dead): the persisted record carries the screen.
    phase = 'record-only';
    await new Promise(r => setTimeout(r, 2600));
    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);
    // And the failing status channel is VISIBLE, not swallowed.
    expect(container.querySelector('[data-rha-shell="diagnostics"]')?.textContent).toContain('no offscreen document');
  });

  it('detects the extension from the dataset signal ALONE (the 1.7.36 regression)', async () => {
    // Andy's machine: nothing ever pre-sets window.__RHA_EXT__ — the content
    // script's dataset attribute is the only boot-time signal.
    delete window.__RHA_EXT__;
    document.documentElement.dataset.rhaExt = JSON.stringify({ version: '9.9.9', extensionId: 'test-ext' });
    const { container } = render(<Shell />);
    await waitFor(() => {
      expect(container.textContent).not.toContain('extension not detected');
      expect(container.textContent).toContain('mock-model');
    }, { timeout: 5000 });
    delete document.documentElement.dataset.rhaExt;
  });

  it('detects the extension from the rha-ext-meta postMessage alone', async () => {
    delete window.__RHA_EXT__;
    const { container } = render(<Shell />);
    await waitFor(() => {
      expect(container.textContent).toContain('extension not detected');
    }, { timeout: 4000 });
    window.postMessage({ type: 'rha-ext-meta', source: 'rha-provider-vault', version: '9.9.9', extensionId: 'test-ext' }, '*');
    await waitFor(() => {
      expect(container.textContent).not.toContain('extension not detected');
    }, { timeout: 5000 });
  });

  it("Andy's machine A: tier1:live dead — status override still auto-opens the live run", async () => {
    channelMode = 'live-dead';
    window.__RHA_EXT__ = { version: '9.9.9', extensionId: 'test-ext' };
    const { container } = render(<Shell />);
    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(container.querySelector('[data-rha-shell="run-report"]')?.textContent).toContain('IR CONTRACT');
      expect(container.textContent).toContain('demo prompt');
    }, { timeout: 6000 });
  });

  it("Andy's machine B: live AND status dead — checkpoint-follow renders and advances the log", async () => {
    channelMode = 'both-dead';
    checkpointEvents = EVENTS.slice(0, 1);
    window.__RHA_EXT__ = { version: '9.9.9', extensionId: 'test-ext' };
    const { container } = render(<Shell />);
    // The runs list arrives via tier1:runs; select the running tile.
    await waitFor(() => {
      expect(container.querySelector('.sh-run')).toBeTruthy();
    }, { timeout: 6000 });
    (container.querySelector('.sh-run') as HTMLButtonElement).click();
    // First checkpoint renders + the honest banner shows.
    await waitFor(() => {
      const texts = [...container.querySelectorAll('.logrow')].map(r => r.textContent ?? '');
      expect(texts.some(t => t.includes('encoding prompt into feature-coded spec'))).toBe(true);
      expect(container.textContent).toContain('following the checkpointed record');
    }, { timeout: 6000 });
    // The next checkpoint lands; the follower must pick it up.
    checkpointEvents = EVENTS;
    await waitFor(() => {
      const texts = [...container.querySelectorAll('.logrow')].map(r => r.textContent ?? '');
      expect(texts.some(t => t.includes('built 1/1 owned files'))).toBe(true);
    }, { timeout: 9000 });
  });

  it('first law: a crash renders the crash card with the error text', () => {
    const { container } = render(<CrashCard error={new Error('boom: synthetic render failure')} />);
    expect(container.querySelector('[data-rha-shell="crash"]')).toBeTruthy();
    expect(container.textContent).toContain('boom: synthetic render failure');
  });
});
