import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFakeChatSocket } from '@/spa/chat/test-support/fake-chat-socket';
import { MAX_MANIFEST_ITEMS } from '@/spa/chat/limits';

const { getSocketMock, destroySocketMock } = vi.hoisted(() => ({
  getSocketMock: vi.fn(),
  destroySocketMock: vi.fn(),
}));

vi.mock('@/lib/socket', () => ({
  getSocket: getSocketMock,
  destroySocket: destroySocketMock,
}));

import { ChatPage } from './chat';

function setValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): void {
  const proto =
    el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
  const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
  setter?.call(el, value);
  el.dispatchEvent(new Event('input', { bubbles: true }));
}

function selectValue(el: HTMLSelectElement, value: string): void {
  const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
  setter?.call(el, value);
  el.dispatchEvent(new Event('change', { bubbles: true }));
}

function findButton(container: HTMLElement, text: string): HTMLButtonElement {
  const button = [...container.querySelectorAll('button')].find((candidate) =>
    candidate.textContent?.includes(text),
  );
  if (!button) throw new Error(`Button with text "${text}" not found`);
  return button;
}

function jsonResponse(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'Content-Type': 'application/json' },
  });
}

const DEFAULT_CATALOG = {
  harnessId: 'pi',
  version: '2026-08-11',
  fingerprint: 'fp',
  models: [
    {
      harnessId: 'pi',
      providerId: 'openai',
      modelId: 'gpt-5',
      displayName: 'GPT-5',
      reasoningCapability: true,
      inputTypes: ['text'],
      authState: 'ready',
      availability: 'available',
    },
    {
      harnessId: 'pi',
      providerId: 'anthropic',
      modelId: 'claude',
      displayName: 'Claude',
      reasoningCapability: true,
      inputTypes: ['text'],
      authState: 'ready',
      availability: 'available',
    },
  ],
};

/** A harness/catalog/selection HTTP stub for the chat-api the selection hook
 * drives. `selection` seeds the persisted tuple returned by the GET (a valid
 * in-catalog tuple by default, so `canSend` settles true after mount). */
function harnessFetch(
  selection: unknown = { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' },
): typeof fetch {
  return vi.fn(async (input: unknown, init?: RequestInit) => {
    const url = String(input);
    const method = String(init?.method ?? 'GET').toUpperCase();
    if (url === '/api/harnesses') {
      return jsonResponse([{ id: 'pi', displayName: 'Pi', capabilities: [] }]);
    }
    if (url.startsWith('/api/harnesses/') && url.endsWith('/catalog')) {
      return jsonResponse(DEFAULT_CATALOG);
    }
    if (url === '/api/chat/preferences/selection' && method === 'GET') {
      return jsonResponse({ selection });
    }
    if (url === '/api/chat/preferences/selection' && method === 'PUT') {
      return jsonResponse({ selection: JSON.parse(String(init?.body)) });
    }
    return new Response('not found', { status: 404 });
  }) as unknown as typeof fetch;
}

/** Drains the selection hook's chained mount fetches (harnesses → selection →
 * catalog) and any pending PUT so derived `canSend` settles before assertions. */
async function flushAsync(times = 5): Promise<void> {
  for (let i = 0; i < times; i += 1) {
    await act(async () => {
      await Promise.resolve();
    });
  }
}

/** Deterministic idempotency key for the Task Five red-first page send test. */
const PAGE_UUID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';

/** Install a controllable `crypto.randomUUID` and return a restore fn. Uses
 * defineProperty on the crypto instance so it works whether or not the native
 * method is configurable (it lives on the prototype; an own property shadows it). */
function installRandomUUID(fn: () => string): () => void {
  const g = globalThis as { crypto?: { randomUUID?: () => string } };
  if (!g.crypto) {
    Object.defineProperty(g, 'crypto', { configurable: true, writable: true, value: {} });
  }
  const cryptoObj = g.crypto as { randomUUID?: () => string };
  const original = Object.getOwnPropertyDescriptor(cryptoObj, 'randomUUID');
  Object.defineProperty(cryptoObj, 'randomUUID', {
    configurable: true,
    writable: true,
    value: fn,
  });
  return () => {
    if (original) {
      Object.defineProperty(cryptoObj, 'randomUUID', original);
    } else {
      Reflect.deleteProperty(cryptoObj, 'randomUUID');
    }
  };
}

/**
 * Task Five MAJOR-1: the send path is PROTOCOL-driven — the browser may send only
 * as the server advertised, once per connection, over the server-to-client-only
 * `chat:send-capability`. Model that advertisement for THIS connection id so the
 * page send tests take the intended branch. `legacy-message` is the default
 * (advertised in `beforeEach`/`remountWithFetch`); the pi turn-runtime tests
 * reset the generation and re-advertise `turn-send` via the helper below.
 */
function advertiseSendCapability(protocol: 'legacy-message' | 'turn-send' | 'unavailable'): void {
  fake.serverEmit('chat:send-capability', { protocol, connectionId: fake.socket.id });
}

/** Reset the negotiated protocol to a fresh, unlocked generation (clearing the
 * default `legacy-message` advertisement + first-wins lock), then advertise the
 * pi turn-runtime `turn:send` protocol for this connection. The per-test override
 * for the page send tests that route through `turn:send`. */
async function advertiseTurnSendGeneration(): Promise<void> {
  await act(async () => {
    fake.simulateReconnect();
  });
  await act(async () => {
    advertiseSendCapability('turn-send');
  });
}

let fake: ReturnType<typeof createFakeChatSocket>;
let root: Root | null;
let container: HTMLElement;

beforeAll(() => {
  Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
    configurable: true,
    value: true,
  });
});

afterAll(() => {
  Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
});

beforeEach(async () => {
  fake = createFakeChatSocket();
  getSocketMock.mockReset().mockReturnValue(fake.socket);
  destroySocketMock.mockReset();
  vi.stubGlobal('fetch', harnessFetch());
  container = document.createElement('div');
  document.body.append(container);
  root = createRoot(container);
  await act(async () => {
    root?.render(<ChatPage />);
  });
  // Settle the selection hook's mount fetches so the default in-catalog tuple
  // persists and `canSend` is true for the existing send-path tests.
  await flushAsync();
  // Model the server's post-auth send-capability advertisement (MAJOR-1). Most
  // page send tests exercise the legacy `message` branch; the pi turn-runtime
  // tests override to `turn-send` via advertiseTurnSendGeneration().
  await act(async () => {
    advertiseSendCapability('legacy-message');
  });
});

afterEach(async () => {
  await act(async () => {
    root?.unmount();
  });
  document.body.replaceChildren();
  vi.unstubAllGlobals();
});

/** Re-mounts ChatPage against a custom fetch stub (e.g. an unset selection) for
 * tests that need a non-default selection scenario. */
async function remountWithFetch(fetchImpl: typeof fetch): Promise<void> {
  await act(async () => {
    root?.unmount();
  });
  vi.stubGlobal('fetch', fetchImpl);
  root = createRoot(container);
  await act(async () => {
    root?.render(<ChatPage />);
  });
  await flushAsync();
  // Re-advertise on the remounted connection — the prior generation's capability
  // does not carry across a remount (fresh hook instance, unadvertised protocol).
  await act(async () => {
    advertiseSendCapability('legacy-message');
  });
}

describe('ChatPage', () => {
  it('streams agent:text and agent:thinking, shows tool status, and finalizes on agent:end with usage', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('agent:start', { conversationId: 'c1' });
    });
    await act(async () => {
      fake.serverEmit('agent:thinking', { conversationId: 'c1', text: 'pondering…' });
      fake.serverEmit('agent:text', { conversationId: 'c1', text: 'Hel' });
      fake.serverEmit('agent:text', { conversationId: 'c1', text: 'lo!' });
      fake.serverEmit('agent:tool:start', {
        conversationId: 'c1',
        toolCallId: 't1',
        toolName: 'web_search',
      });
    });

    expect(container.textContent).toContain('pondering…');
    expect(container.textContent).toContain('Hello!');
    expect(container.textContent).toContain('web_search');
    expect(container.textContent).toMatch(/running/i);

    await act(async () => {
      fake.serverEmit('agent:tool:end', {
        conversationId: 'c1',
        toolCallId: 't1',
        toolName: 'web_search',
        isError: false,
      });
      fake.serverEmit('agent:end', {
        conversationId: 'c1',
        usage: {
          provider: 'anthropic',
          modelId: 'claude',
          thinkingLevel: 'medium',
          tokens: { input: 12, output: 34, cacheRead: 0, cacheWrite: 0, total: 46 },
          cost: 0.02,
          context: { percent: 3, window: 200000 },
        },
      });
    });

    expect(container.textContent).toMatch(/success/i);
    expect(container.textContent).toContain('Hello!');
    expect(container.textContent).toMatch(/46/);
  });

  it('renders the commands manifest and session info, and lets the user pick a thinking level', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('commands:manifest', {
        manifest: {
          commands: [
            {
              name: 'model',
              aliases: ['m'],
              description: 'Change the active model',
              scope: 'core',
              execution: 'socket',
              available: true,
            },
          ],
          skills: [],
          version: 1,
        },
      });
      fake.serverEmit('session:info', {
        conversationId: 'c1',
        provider: 'anthropic',
        modelId: 'claude',
        thinkingLevel: 'medium',
        availableThinkingLevels: ['low', 'medium', 'high'],
        routingDecision: {
          model: 'claude',
          provider: 'anthropic',
          ruleName: 'default',
          reason: 'default routing',
        },
      });
    });

    expect(container.textContent).toContain('model');
    expect(container.textContent).toContain('Change the active model');
    expect(container.textContent).toContain('anthropic');
    expect(container.textContent).toContain('default routing');

    const select = container.querySelector(
      'select[aria-label="Thinking level"]',
    ) as HTMLSelectElement;
    expect(select).toBeTruthy();
    expect([...select.options].map((o) => o.value)).toEqual(['low', 'medium', 'high']);

    await act(async () => {
      selectValue(select, 'high');
    });

    expect(fake.emitted).toContainEqual({
      event: 'set:thinking',
      payload: { conversationId: 'c1', level: 'high' },
    });
  });

  it('executes and approves commands with exact payloads and surfaces the approval affordance', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
    });

    const commandInput = container.querySelector(
      'input[aria-label="Command name"]',
    ) as HTMLInputElement;
    const argsInput = container.querySelector(
      'input[aria-label="Command arguments"]',
    ) as HTMLInputElement;

    await act(async () => {
      setValue(commandInput, 'model');
      setValue(argsInput, 'gpt-5');
    });
    await act(async () => {
      findButton(container, 'Run command').click();
    });

    expect(fake.emitted).toContainEqual({
      event: 'command:execute',
      payload: { conversationId: 'c1', command: 'model', args: 'gpt-5' },
    });

    await act(async () => {
      setValue(commandInput, 'deploy');
      setValue(argsInput, 'prod');
    });
    await act(async () => {
      findButton(container, 'Request approval').click();
    });

    expect(fake.emitted).toContainEqual({
      event: 'command:approve',
      payload: { conversationId: 'c1', command: 'deploy', args: 'prod' },
    });

    await act(async () => {
      fake.serverEmit('command:approval', {
        conversationId: 'c1',
        command: 'deploy',
        success: true,
        approvalId: 'ap1',
        expiresAt: '2026-01-01T00:00:00.000Z',
      });
    });

    expect(container.textContent).toMatch(/approved/i);

    await act(async () => {
      findButton(container, 'Run approved command').click();
    });

    expect(fake.emitted).toContainEqual({
      event: 'command:execute',
      payload: { conversationId: 'c1', command: 'deploy', args: 'prod', approvalId: 'ap1' },
    });
  });

  it('shows visible alert surfaces for a server error and the structured contract reason for a failed command result', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('error', { conversationId: 'c1', error: 'The model is unavailable' });
      fake.serverEmit('command:result', {
        conversationId: 'c1',
        command: 'model',
        success: false,
        message: 'Unknown model',
      });
    });

    const alerts = [...container.querySelectorAll('[role="alert"]')];
    const alertText = alerts.map((node) => node.textContent).join(' ');
    expect(alertText).toContain('The model is unavailable');
    // The structured, contract-provided denial reason is visibly rendered.
    expect(alertText).toContain('Unknown model');
  });

  it('falls back to a stable "Command failed." copy when a failed command result has no usable message', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmitRaw('command:result', {
        conversationId: 'c1',
        command: 'model',
        success: false,
        message: { bad: 'object' },
      });
    });

    const alerts = [...container.querySelectorAll('[role="alert"]')];
    const alertText = alerts.map((node) => node.textContent).join(' ');
    expect(alertText).toContain('Command failed.');
  });

  it('caps availableThinkingLevels before storing and rendering a hostile session payload', async () => {
    const hostileLevels = Array.from({ length: MAX_MANIFEST_ITEMS + 50 }, (_, i) => `level-${i}`);

    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('session:info', {
        conversationId: 'c1',
        provider: 'anthropic',
        modelId: 'claude',
        thinkingLevel: 'level-0',
        availableThinkingLevels: hostileLevels,
      });
    });

    const select = container.querySelector(
      'select[aria-label="Thinking level"]',
    ) as HTMLSelectElement;
    expect(select).toBeTruthy();
    expect(select.options.length).toBeLessThanOrEqual(MAX_MANIFEST_ITEMS);
  });

  it('renders a safe fallback when session:info arrives with a malformed (non-array) availableThinkingLevels, without throwing', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmitRaw('session:info', {
        conversationId: 'c1',
        provider: 'anthropic',
        modelId: 'claude',
        thinkingLevel: 'medium',
        availableThinkingLevels: null,
      });
    });

    expect(container.querySelector('section[aria-label="Session info"]')).toBeTruthy();
    const select = container.querySelector(
      'select[aria-label="Thinking level"]',
    ) as HTMLSelectElement;
    expect(select).toBeTruthy();
    // A malformed level list still shows a visible, safe placeholder option
    // rather than a silently empty select.
    expect([...select.options]).toHaveLength(1);
    expect(select.options[0]?.textContent).toMatch(/unavailable/i);

    await act(async () => {
      selectValue(select, '');
    });
    expect(fake.emitted.filter((e) => e.event === 'set:thinking')).toHaveLength(0);
  });

  it('renders honest unavailable labels — not fabricated zeros — when agent:end usage has malformed/missing numeric fields', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('agent:start', { conversationId: 'c1' });
      fake.serverEmitRaw('agent:end', {
        conversationId: 'c1',
        usage: {
          provider: { nested: 'object' },
          modelId: undefined,
          thinkingLevel: 'medium',
          tokens: { total: 'not-a-number' },
          cost: undefined,
          context: { percent: null, window: 200000 },
        },
      });
    });

    const usage = container.querySelector('[aria-label="Usage"]');
    expect(usage).toBeTruthy();
    expect(usage?.textContent).toContain('tokens unavailable');
    expect(usage?.textContent).toContain('cost unavailable');
    expect(usage?.textContent).not.toContain('0 tokens');
    expect(usage?.textContent).not.toContain('$0.0000');
    expect(usage?.textContent).toContain('unknown/unknown');
  });

  it('renders a safe fallback for message:ack when messageId is a malformed non-string value, without throwing', async () => {
    await expect(
      act(async () => {
        fake.serverEmitRaw('message:ack', { conversationId: 'c1', messageId: { bad: 'object' } });
      }),
    ).resolves.not.toThrow();

    const status = [...container.querySelectorAll('[role="status"]')].find((node) =>
      node.textContent?.includes('Message accepted'),
    );
    expect(status).toBeTruthy();
    // A malformed messageId gets a stable, visible fallback — never blank,
    // never the raw object.
    expect(status?.textContent).toContain('unknown');
  });

  it('renders safely and does not throw when system:reload.message is a malformed non-string value', async () => {
    await expect(
      act(async () => {
        fake.serverEmitRaw('system:reload', {
          commands: [],
          skills: [],
          providers: [],
          message: { bad: 'object' },
        });
      }),
    ).resolves.not.toThrow();

    const status = container.querySelector('[role="status"]');
    expect(status).toBeTruthy();
    // A malformed reload message renders a stable, visible fallback rather
    // than a silently empty status line.
    expect(status?.textContent).toContain('Commands reloaded.');
  });

  it('renders safely and does not throw when a scoped error carries a malformed non-string error value', async () => {
    await expect(
      act(async () => {
        fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
        fake.serverEmitRaw('error', { conversationId: 'c1', error: ['not', 'a', 'string'] });
      }),
    ).resolves.not.toThrow();

    expect(container.querySelector('[role="alert"]')).toBeTruthy();
  });

  it('renders harness and provider as separate selects (not merged) and no free-text provider/model inputs', async () => {
    // The old free-text inputs are gone.
    expect(container.querySelector('input[aria-label="Provider"]')).toBeNull();
    expect(container.querySelector('input[aria-label="Model"]')).toBeNull();

    const harnessSelect = container.querySelector(
      'select[aria-label="Harness"]',
    ) as HTMLSelectElement;
    const providerSelect = container.querySelector(
      'select[aria-label="Provider"]',
    ) as HTMLSelectElement;
    const modelSelect = container.querySelector('select[aria-label="Model"]') as HTMLSelectElement;
    expect(harnessSelect).toBeTruthy();
    expect(providerSelect).toBeTruthy();
    expect(modelSelect).toBeTruthy();
    // Harness and provider are distinct controls carrying distinct identifiers.
    expect(harnessSelect).not.toBe(providerSelect);
    expect([...harnessSelect.options].map((o) => o.value)).toContain('pi');
    expect([...providerSelect.options].map((o) => o.value)).toContain('openai');
    expect([...providerSelect.options].map((o) => o.value)).toContain('anthropic');
    // The model options are catalog-derived (not hardcoded) and scoped to the
    // selected provider (openai, from the persisted tuple) using a collision-safe
    // composite identity — the anthropic row is absent, not a bare 'claude'.
    const modelValues = [...modelSelect.options].map((o) => o.value);
    expect(modelValues).toContain('openai:gpt-5');
    expect(modelValues).not.toContain('anthropic:claude');
    expect(modelValues).not.toContain('claude');
  });

  it('sends provider/model derived from the persisted catalog tuple (never free text) and emits abort from Stop', async () => {
    const textarea = container.querySelector(
      'textarea[aria-label="Message"]',
    ) as HTMLTextAreaElement;

    const stopButtonBefore = container.querySelector(
      'button[aria-label="Stop"]',
    ) as HTMLButtonElement;
    expect(stopButtonBefore.disabled).toBe(true);

    // Choose a fresh tuple from the catalog and let it persist.
    const providerSelect = container.querySelector(
      'select[aria-label="Provider"]',
    ) as HTMLSelectElement;
    await act(async () => {
      selectValue(providerSelect, 'anthropic');
    });
    const modelSelect = container.querySelector('select[aria-label="Model"]') as HTMLSelectElement;
    await act(async () => {
      // Composite provider+model option identity (provider was switched to
      // anthropic above); the bare 'claude' no longer identifies an option.
      selectValue(modelSelect, 'anthropic:claude');
    });
    await flushAsync();

    await act(async () => {
      setValue(textarea, 'hello there');
    });
    await act(async () => {
      textarea.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
      );
    });

    // The projected provider/model come from the validated persisted tuple.
    expect(fake.emitted).toContainEqual({
      event: 'message',
      payload: {
        conversationId: undefined,
        content: 'hello there',
        provider: 'anthropic',
        modelId: 'claude',
      },
    });
    expect(container.textContent).toContain('hello there');

    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('agent:start', { conversationId: 'c1' });
    });

    const stopButtonDuring = container.querySelector(
      'button[aria-label="Stop"]',
    ) as HTMLButtonElement;
    expect(stopButtonDuring.disabled).toBe(false);

    await act(async () => {
      stopButtonDuring.click();
    });

    expect(fake.emitted).toContainEqual({ event: 'abort', payload: { conversationId: 'c1' } });
  });

  it('emits turn:send with the nested persisted selection tuple and a UUID idempotency key (never the legacy message event)', async () => {
    await advertiseTurnSendGeneration();
    const restore = installRandomUUID(() => PAGE_UUID);
    try {
      // Send is disabled without an active conversation — establish one first.
      await act(async () => {
        fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      });

      const textarea = container.querySelector(
        'textarea[aria-label="Message"]',
      ) as HTMLTextAreaElement;
      await act(async () => {
        setValue(textarea, 'hello there');
      });
      await act(async () => {
        textarea.dispatchEvent(
          new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
        );
      });
    } finally {
      restore();
    }

    const sends = fake.emitted.filter((e) => e.event === 'turn:send');
    expect(sends).toHaveLength(1);
    expect(sends[0]?.payload).toEqual({
      conversationId: 'c1',
      content: 'hello there',
      selection: { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' },
      idempotencyKey: PAGE_UUID,
    });
    // The pi-rpc page send must not emit the embedded `message` event, and must
    // never send a flat {provider, modelId} that drops the harnessId.
    expect(fake.emitted.some((e) => e.event === 'message')).toBe(false);
  });

  it('keeps the composer content and emits nothing when the send cannot mint an idempotency key, so the user can retry (composer clears only on success) — Task Five group 5', async () => {
    await advertiseTurnSendGeneration();
    const failing = installRandomUUID(() => {
      throw new Error('secure random unavailable');
    });
    try {
      await act(async () => {
        fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      });
      const textarea = container.querySelector(
        'textarea[aria-label="Message"]',
      ) as HTMLTextAreaElement;
      await act(async () => {
        setValue(textarea, 'keep me');
      });
      await act(async () => {
        textarea.dispatchEvent(
          new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
        );
      });

      // No wire traffic: neither the harness turn nor the legacy message.
      expect(fake.emitted.some((e) => e.event === 'turn:send')).toBe(false);
      expect(fake.emitted.some((e) => e.event === 'message')).toBe(false);
      // The composer retained its content — it clears ONLY on a successful send,
      // so the user can retry without retyping.
      expect(textarea.value).toBe('keep me');
      // A visible, safe notice explains why nothing was sent.
      expect(container.querySelector('[role="alert"]')).toBeTruthy();
    } finally {
      failing();
    }
  });

  it('clears the composer after a successful turn:send and never falls back to the legacy message event — Task Five group 5', async () => {
    await advertiseTurnSendGeneration();
    const restore = installRandomUUID(() => PAGE_UUID);
    try {
      await act(async () => {
        fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      });
      const textarea = container.querySelector(
        'textarea[aria-label="Message"]',
      ) as HTMLTextAreaElement;
      await act(async () => {
        setValue(textarea, 'ship it');
      });
      await act(async () => {
        textarea.dispatchEvent(
          new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
        );
      });

      const sends = fake.emitted.filter((e) => e.event === 'turn:send');
      expect(sends).toHaveLength(1);
      expect(fake.emitted.some((e) => e.event === 'message')).toBe(false);
      // On a successful send the composer clears.
      expect(textarea.value).toBe('');
    } finally {
      restore();
    }
  });

  it('sends the freshly persisted selection as a nested turn:send tuple after the user changes provider/model — never a stale default or flat fields — Task Five group 5', async () => {
    await advertiseTurnSendGeneration();
    const restore = installRandomUUID(() => PAGE_UUID);
    try {
      // Change the selection away from the mount default and let it persist.
      const providerSelect = container.querySelector(
        'select[aria-label="Provider"]',
      ) as HTMLSelectElement;
      await act(async () => {
        selectValue(providerSelect, 'anthropic');
      });
      const modelSelect = container.querySelector(
        'select[aria-label="Model"]',
      ) as HTMLSelectElement;
      await act(async () => {
        selectValue(modelSelect, 'anthropic:claude');
      });
      await flushAsync();

      await act(async () => {
        fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      });
      const textarea = container.querySelector(
        'textarea[aria-label="Message"]',
      ) as HTMLTextAreaElement;
      await act(async () => {
        setValue(textarea, 'routed');
      });
      await act(async () => {
        textarea.dispatchEvent(
          new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
        );
      });

      const sends = fake.emitted.filter((e) => e.event === 'turn:send');
      expect(sends).toHaveLength(1);
      // The nested tuple reflects the CURRENTLY persisted selection, not the
      // mount default {openai, gpt-5}, and never flat provider/model fields.
      expect(sends[0]?.payload).toEqual({
        conversationId: 'c1',
        content: 'routed',
        selection: { harnessId: 'pi', providerId: 'anthropic', modelId: 'claude' },
        idempotencyKey: PAGE_UUID,
      });
      expect(fake.emitted.some((e) => e.event === 'message')).toBe(false);
    } finally {
      restore();
    }
  });

  it('disables send until a selection has persisted — no send with an unset selection', async () => {
    await remountWithFetch(harnessFetch(null));

    const sendButton = findButton(container, 'Send');
    const textarea = container.querySelector(
      'textarea[aria-label="Message"]',
    ) as HTMLTextAreaElement;

    await act(async () => {
      setValue(textarea, 'should not send');
    });
    // Content present, but no selection persisted → Send stays disabled.
    expect(sendButton.disabled).toBe(true);

    await act(async () => {
      textarea.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
      );
    });
    expect(fake.emitted.filter((e) => e.event === 'message')).toHaveLength(0);
  });

  it('renders the session panel from a pre-ack session:info and keeps it visible after the later ack', async () => {
    const textarea = container.querySelector(
      'textarea[aria-label="Message"]',
    ) as HTMLTextAreaElement;

    await act(async () => {
      setValue(textarea, 'hello');
    });
    await act(async () => {
      textarea.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
      );
    });
    await act(async () => {
      fake.serverEmit('session:info', {
        conversationId: 'c1',
        provider: 'anthropic',
        modelId: 'claude',
        thinkingLevel: 'medium',
        availableThinkingLevels: ['low', 'medium', 'high'],
      });
    });

    expect(container.querySelector('section[aria-label="Session info"]')).toBeTruthy();
    expect(container.textContent).toContain('anthropic');

    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
    });

    expect(container.querySelector('section[aria-label="Session info"]')).toBeTruthy();
    expect(container.textContent).toContain('anthropic');
  });

  it('surfaces a pre-ack error as an alert without leaving the Stop control stuck active', async () => {
    const textarea = container.querySelector(
      'textarea[aria-label="Message"]',
    ) as HTMLTextAreaElement;

    await act(async () => {
      setValue(textarea, 'hello');
    });
    await act(async () => {
      textarea.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
      );
    });
    await act(async () => {
      fake.serverEmit('error', {
        conversationId: 'c1',
        error: 'Failed to start agent session. Please try again.',
      });
    });

    const alerts = [...container.querySelectorAll('[role="alert"]')];
    expect(alerts.some((node) => node.textContent?.includes('Failed to start agent session'))).toBe(
      true,
    );

    const stopButton = container.querySelector('button[aria-label="Stop"]') as HTMLButtonElement;
    expect(stopButton.disabled).toBe(true);
  });

  it('shows an accessible status once the message is acknowledged', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
    });

    const statuses = [...container.querySelectorAll('[role="status"]')];
    expect(statuses.some((node) => node.textContent?.includes('m1'))).toBe(true);
  });

  it('renders finalized thinking text in the transcript after agent:end, not only while streaming', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('agent:start', { conversationId: 'c1' });
      fake.serverEmit('agent:thinking', { conversationId: 'c1', text: 'reasoning about it' });
      fake.serverEmit('agent:text', { conversationId: 'c1', text: 'Done.' });
    });

    expect(container.textContent).toContain('reasoning about it');

    await act(async () => {
      fake.serverEmit('agent:end', { conversationId: 'c1' });
    });

    expect(container.textContent).toContain('reasoning about it');
    expect(container.textContent).toContain('Done.');
  });

  it('ignores a concurrent approval request and only executes the approved command once', async () => {
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
    });

    const commandInput = container.querySelector(
      'input[aria-label="Command name"]',
    ) as HTMLInputElement;
    const argsInput = container.querySelector(
      'input[aria-label="Command arguments"]',
    ) as HTMLInputElement;

    await act(async () => {
      setValue(commandInput, 'deploy');
      setValue(argsInput, 'prod');
    });
    await act(async () => {
      findButton(container, 'Request approval').click();
    });
    await act(async () => {
      setValue(argsInput, 'staging');
    });
    await act(async () => {
      findButton(container, 'Request approval').click();
    });

    expect(fake.emitted.filter((e) => e.event === 'command:approve')).toHaveLength(1);
    expect(fake.emitted).toContainEqual({
      event: 'command:approve',
      payload: { conversationId: 'c1', command: 'deploy', args: 'prod' },
    });

    await act(async () => {
      fake.serverEmit('command:approval', {
        conversationId: 'c1',
        command: 'deploy',
        success: true,
        approvalId: 'ap1',
        expiresAt: '2026-01-01T00:00:00.000Z',
      });
    });

    await act(async () => {
      findButton(container, 'Run approved command').click();
      findButton(container, 'Run approved command').click();
    });

    expect(fake.emitted.filter((e) => e.event === 'command:execute')).toHaveLength(1);
    expect(fake.emitted).toContainEqual({
      event: 'command:execute',
      payload: { conversationId: 'c1', command: 'deploy', args: 'prod', approvalId: 'ap1' },
    });
  });

  it('disables sending a second message while a turn is streaming', async () => {
    const textarea = container.querySelector(
      'textarea[aria-label="Message"]',
    ) as HTMLTextAreaElement;

    await act(async () => {
      setValue(textarea, 'first');
    });
    await act(async () => {
      textarea.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
      );
    });
    await act(async () => {
      fake.serverEmit('message:ack', { conversationId: 'c1', messageId: 'm1' });
      fake.serverEmit('agent:start', { conversationId: 'c1' });
    });

    const sendButton = findButton(container, 'Send');
    expect(sendButton.disabled).toBe(true);

    await act(async () => {
      setValue(textarea, 'second');
    });
    await act(async () => {
      textarea.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
      );
    });

    expect(fake.emitted.filter((e) => e.event === 'message')).toHaveLength(1);
  });

  it('scopes the model options to the intentionally selected provider (cross-provider models absent)', async () => {
    await remountWithFetch(harnessFetch(null));

    const harnessSelect = container.querySelector(
      'select[aria-label="Harness"]',
    ) as HTMLSelectElement;
    await act(async () => {
      selectValue(harnessSelect, 'pi');
    });
    await flushAsync();

    const providerSelect = container.querySelector(
      'select[aria-label="Provider"]',
    ) as HTMLSelectElement;
    await act(async () => {
      selectValue(providerSelect, 'openai');
    });

    const modelSelect = container.querySelector('select[aria-label="Model"]') as HTMLSelectElement;
    const optionValues = [...modelSelect.options].map((o) => o.value).filter((v) => v !== '');
    // Only the selected provider's models are offered — provider B's model
    // (anthropic:claude) is absent, so a user cannot pick across providers.
    expect(optionValues).toEqual(['openai:gpt-5']);
    expect(optionValues).not.toContain('anthropic:claude');
  });

  it('keeps identical modelIds under two providers distinct and resolves the pick to the exact tuple', async () => {
    const COLLIDING_CATALOG = {
      harnessId: 'pi',
      version: '2026-08-11',
      fingerprint: 'fp',
      models: [
        {
          harnessId: 'pi',
          providerId: 'alpha',
          modelId: 'gpt-x',
          displayName: 'Alpha GPT-X',
          reasoningCapability: true,
          inputTypes: ['text'],
          authState: 'ready',
          availability: 'available',
        },
        {
          harnessId: 'pi',
          providerId: 'beta',
          modelId: 'gpt-x',
          displayName: 'Beta GPT-X',
          reasoningCapability: true,
          inputTypes: ['text'],
          authState: 'ready',
          availability: 'available',
        },
      ],
    };
    const collidingFetch = vi.fn(async (input: unknown, init?: RequestInit) => {
      const url = String(input);
      const method = String(init?.method ?? 'GET').toUpperCase();
      if (url === '/api/harnesses') {
        return jsonResponse([{ id: 'pi', displayName: 'Pi', capabilities: [] }]);
      }
      if (url.startsWith('/api/harnesses/') && url.endsWith('/catalog')) {
        return jsonResponse(COLLIDING_CATALOG);
      }
      if (url === '/api/chat/preferences/selection' && method === 'GET') {
        return jsonResponse({ selection: null });
      }
      if (url === '/api/chat/preferences/selection' && method === 'PUT') {
        return jsonResponse({ selection: JSON.parse(String(init?.body)) });
      }
      return new Response('not found', { status: 404 });
    }) as unknown as typeof fetch;

    await remountWithFetch(collidingFetch);

    const harnessSelect = container.querySelector(
      'select[aria-label="Harness"]',
    ) as HTMLSelectElement;
    await act(async () => {
      selectValue(harnessSelect, 'pi');
    });
    await flushAsync();

    const providerSelect = container.querySelector(
      'select[aria-label="Provider"]',
    ) as HTMLSelectElement;
    await act(async () => {
      selectValue(providerSelect, 'alpha');
    });

    const modelSelect = container.querySelector('select[aria-label="Model"]') as HTMLSelectElement;
    // The colliding modelId is provider-qualified in the option value, never a
    // bare id, so the two providers' 'gpt-x' rows are uniquely identifiable.
    const optionValues = [...modelSelect.options].map((o) => o.value).filter((v) => v !== '');
    expect(optionValues).toEqual(['alpha:gpt-x']);

    await act(async () => {
      selectValue(modelSelect, 'alpha:gpt-x');
    });
    await flushAsync();

    // The controlled select highlights the alpha row via the composite identity.
    expect(modelSelect.value).toBe('alpha:gpt-x');

    const textarea = container.querySelector(
      'textarea[aria-label="Message"]',
    ) as HTMLTextAreaElement;
    await act(async () => {
      setValue(textarea, 'ping');
    });
    await act(async () => {
      textarea.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
      );
    });

    // The persisted/sent tuple resolves to provider alpha — NOT beta — even
    // though the bare modelId 'gpt-x' exists under both providers.
    expect(fake.emitted).toContainEqual({
      event: 'message',
      payload: {
        conversationId: undefined,
        content: 'ping',
        provider: 'alpha',
        modelId: 'gpt-x',
      },
    });
  });

  it('removes socket handlers and tears down the socket on unmount, with no network calls', async () => {
    expect(fake.listeners.size).toBeGreaterThan(0);

    await act(async () => {
      root?.unmount();
    });
    root = null;

    for (const [, handlers] of fake.listeners) {
      expect(handlers.size).toBe(0);
    }
    expect(destroySocketMock).toHaveBeenCalledOnce();
  });
});
