import assert from 'node:assert/strict';
import test from 'node:test';
import { parseHTML } from 'linkedom';
import {
  ChatGptProductStateError,
  ChatGptWebAdapter,
  HostUiChangedError,
  conversationIdFromPath,
  detectChatGptIssue,
  latestAssistantOutput,
  parseRateLimitReset,
} from '../src/index.js';

function installWindow(parsed: ReturnType<typeof parseHTML>): void {
  Object.assign(globalThis, {
    HTMLElement: parsed.window.HTMLElement,
    HTMLTextAreaElement: parsed.window.HTMLTextAreaElement,
    HTMLButtonElement: parsed.window.HTMLButtonElement,
    InputEvent: parsed.window.InputEvent ?? parsed.window.Event,
    MutationObserver: parsed.window.MutationObserver,
  });
}

function fixture(html: string, pathname = '/') {
  const parsed = parseHTML(html);
  installWindow(parsed);
  return new ChatGptWebAdapter({
    document: parsed.document as unknown as Document,
    location: { href: `https://chatgpt.com${pathname}`, pathname } as Location,
  });
}

async function flush(): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, 0));
}

test('conversation ID parser handles ordinary and project paths', () => {
  assert.equal(conversationIdFromPath('/c/abc-123'), 'abc-123');
  assert.equal(conversationIdFromPath('/g/g-project/c/child-id'), 'child-id');
  assert.equal(conversationIdFromPath('/'), null);
});

test('adapter reports ready new chat and idle existing chat', () => {
  const html = '<form><div id="prompt-textarea" role="textbox" contenteditable="true"></div><button data-testid="send-button"></button></form>';
  assert.equal(fixture(html).inspect().state, 'ready');
  assert.equal(fixture(html, '/c/c-1').inspect().state, 'idle');
});

test('adapter detects generation by semantic stop control', () => {
  const html = '<div id="prompt-textarea" contenteditable="true"></div><button data-testid="stop-button"></button>';
  assert.equal(fixture(html, '/c/c-1').inspect().state, 'generating');
});

test('submit writes composer and clicks semantic send control exactly once', async () => {
  const html = '<form><div id="prompt-textarea" role="textbox" contenteditable="true"></div><button data-testid="send-button"></button></form>';
  const parsed = parseHTML(html);
  installWindow(parsed);
  let clicks = 0;
  parsed.document.querySelector('button')?.addEventListener('click', () => { clicks += 1; });
  const adapter = new ChatGptWebAdapter({
    document: parsed.document as unknown as Document,
    location: { href: 'https://chatgpt.com/', pathname: '/' } as Location,
  });
  await adapter.submitPrompt('Attach worker W17');
  assert.equal(parsed.document.querySelector('#prompt-textarea')?.textContent, 'Attach worker W17');
  assert.equal(clicks, 1);
});

test('submit waits for ChatGPT to enable send after async composer state update', async () => {
  const html = '<form><div id="prompt-textarea" role="textbox" contenteditable="true"></div><button data-testid="send-button" disabled></button></form>';
  const parsed = parseHTML(html);
  Object.assign(globalThis, {
    HTMLElement: parsed.window.HTMLElement,
    HTMLTextAreaElement: parsed.window.HTMLTextAreaElement,
    HTMLButtonElement: parsed.window.HTMLButtonElement,
    InputEvent: parsed.window.InputEvent ?? parsed.window.Event,
    MutationObserver: parsed.window.MutationObserver,
  });
  const button = parsed.document.querySelector('button')!;
  let clicks = 0;
  button.addEventListener('click', () => { clicks += 1; });
  parsed.document.querySelector('#prompt-textarea')?.addEventListener('input', () => {
    queueMicrotask(() => button.removeAttribute('disabled'));
  });
  const adapter = new ChatGptWebAdapter({ document: parsed.document as unknown as Document, location: { href: 'https://chatgpt.com/', pathname: '/' } as Location });
  await adapter.submitPrompt('Async prompt');
  assert.equal(clicks, 1);
});

test('submit fails closed when known composer is absent', async () => {
  await assert.rejects(fixture('<main></main>').submitPrompt('work'), HostUiChangedError);
});

test('bounded wait resolves from MutationObserver when composer appears', async () => {
  const parsed = parseHTML('<main id="root"></main>');
  installWindow(parsed);
  const adapter = new ChatGptWebAdapter({
    document: parsed.document as unknown as Document,
    location: { href: 'https://chatgpt.com/', pathname: '/' } as Location,
  });
  const waiting = adapter.waitFor('composer-ready', 100);
  parsed.document.querySelector('#root')?.insertAdjacentHTML('beforeend', '<div id="prompt-textarea" contenteditable="true"></div>');
  const inspection = await waiting;
  assert.equal(inspection.composerReady, true);
});

test('bounded wait fails closed at deadline when UI never becomes ready', async () => {
  const adapter = fixture('<main></main>');
  await assert.rejects(adapter.waitFor('composer-ready', 5), HostUiChangedError);
});

test('continuous observer publishes coarse state transitions and coalesces DOM churn', async () => {
  const parsed = parseHTML('<main><form><div id="prompt-textarea" contenteditable="true"></div><button data-testid="send-button"></button></form></main>');
  installWindow(parsed);
  const adapter = new ChatGptWebAdapter({
    document: parsed.document as unknown as Document,
    location: { href: 'https://chatgpt.com/c/c-1', pathname: '/c/c-1' } as Location,
  });
  const states: string[] = [];
  const dispose = adapter.observe((inspection) => states.push(inspection.state), { reconcileIntervalMs: 30_000 });
  parsed.document.querySelector('main')?.setAttribute('data-noise', '1');
  parsed.document.querySelector('main')?.setAttribute('data-more-noise', '2');
  parsed.document.querySelector('form')?.insertAdjacentHTML('beforeend', '<button data-testid="stop-button"></button>');
  await new Promise((resolve) => setTimeout(resolve, 300));
  parsed.document.querySelector('[data-testid="stop-button"]')?.remove();
  await new Promise((resolve) => setTimeout(resolve, 300));
  dispose();
  assert.deepEqual(states, ['idle', 'generating', 'idle']);
});

test('delivery timeout is a dedicated recoverable execution-boundary classification', () => {
  const parsed = parseHTML('<main><div role="alert">Message delivery timed out. Please try again.</div></main>');
  installWindow(parsed);
  const issue = detectChatGptIssue(parsed.document as unknown as Document);
  assert.deepEqual(issue, {
    code: 'MESSAGE_DELIVERY_TIMEOUT',
    detail: 'Message delivery timed out. Please try again.',
    recoverable: true,
  });
});

test('delivery timeout never clicks Retry and permits a fresh continue submission through the composer', async () => {
  const parsed = parseHTML(`
    <main>
      <div role="alert">Message delivery timed out. Please try again.</div>
      <form>
        <div id="prompt-textarea" role="textbox" contenteditable="true"></div>
        <button type="button" data-testid="send-button">Send</button>
      </form>
      <button type="button" aria-label="Retry">Retry</button>
    </main>
  `);
  installWindow(parsed);
  let sends = 0;
  let retries = 0;
  parsed.document.querySelector('[data-testid="send-button"]')?.addEventListener('click', () => { sends += 1; });
  parsed.document.querySelector('[aria-label="Retry"]')?.addEventListener('click', () => { retries += 1; });
  const adapter = new ChatGptWebAdapter({
    document: parsed.document as unknown as Document,
    location: { href: 'https://chatgpt.com/c/c-1', pathname: '/c/c-1' } as Location,
  });
  const inspection = adapter.inspect();
  assert.equal(inspection.issue?.code, 'MESSAGE_DELIVERY_TIMEOUT');
  assert.equal(inspection.state, 'error');
  await adapter.submitPrompt('continue');
  assert.equal(parsed.document.querySelector('#prompt-textarea')?.textContent, 'continue');
  assert.equal(sends, 1);
  assert.equal(retries, 0);
});

test('repeated delivery timeouts still use fresh continue sends and never Retry', async () => {
  const parsed = parseHTML(`
    <div role="alert">Message delivery timed out. Please try again.</div>
    <form><div id="prompt-textarea" contenteditable="true"></div><button data-testid="send-button">Send</button></form>
    <button aria-label="Retry">Retry</button>
  `);
  installWindow(parsed);
  let sends = 0;
  let retries = 0;
  parsed.document.querySelector('[data-testid="send-button"]')?.addEventListener('click', () => { sends += 1; });
  parsed.document.querySelector('[aria-label="Retry"]')?.addEventListener('click', () => { retries += 1; });
  const adapter = new ChatGptWebAdapter({
    document: parsed.document as unknown as Document,
    location: { href: 'https://chatgpt.com/c/c-1', pathname: '/c/c-1' } as Location,
  });
  await adapter.submitPrompt('continue');
  await adapter.submitPrompt('continue');
  assert.equal(sends, 2);
  assert.equal(retries, 0);
});

test('upstream usage/restriction states are classified and block submission rather than being bypassed', async () => {
  const adapter = fixture(`
    <div role="alert">You have reached your usage limit. Your limit resets later.</div>
    <form><div id="prompt-textarea" contenteditable="true"></div><button data-testid="send-button"></button></form>
  `, '/c/c-1');
  assert.equal(adapter.inspect().issue?.code, 'USAGE_LIMIT');
  await assert.rejects(adapter.submitPrompt('continue'), ChatGptProductStateError);
});

test('arbitrary transcript text mentioning timeout is not treated as a product error', () => {
  const adapter = fixture(`
    <article>Message delivery timed out. Please try again.</article>
    <form><div id="prompt-textarea" contenteditable="true"></div><button data-testid="send-button"></button></form>
  `, '/c/c-1');
  assert.equal(adapter.inspect().issue, null);
  assert.equal(adapter.inspect().state, 'idle');
});


test('latest assistant output is bounded and only exposed when idle', () => {
  const long = 'x'.repeat(17_000) + '<continuation-state>\nstatus: working\n</continuation-state>';
  const encoded = long.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
  const parsed = parseHTML(`<main><div data-message-author-role="assistant">${encoded}</div><form><div id="prompt-textarea" contenteditable="true"></div><button data-testid="send-button"></button></form></main>`);
  installWindow(parsed);
  const extracted = latestAssistantOutput(parsed.document as unknown as Document);
  assert.equal(extracted.truncated, true);
  assert.equal(extracted.tail?.length, 16_384);
  assert.match(extracted.tail ?? '', /<continuation-state>/);
  const adapter = new ChatGptWebAdapter({
    document: parsed.document as unknown as Document,
    location: { href: 'https://chatgpt.com/c/c-1', pathname: '/c/c-1' } as Location,
  });
  assert.equal(adapter.inspect().assistantOutputTruncated, true);
  parsed.document.querySelector('form')?.insertAdjacentHTML('beforeend', '<button data-testid="stop-button"></button>');
  assert.equal(adapter.inspect().assistantOutputTail, null);
  assert.equal(adapter.inspect().assistantOutputTruncated, false);
});

test('latest assistant output ignores user messages and returns the last assistant turn', () => {
  const parsed = parseHTML(`
    <div data-message-author-role="assistant">older assistant</div>
    <div data-message-author-role="user">do not publish this as assistant output</div>
    <div data-message-author-role="assistant">final assistant block</div>
  `);
  installWindow(parsed);
  assert.deepEqual(latestAssistantOutput(parsed.document as unknown as Document), { tail: 'final assistant block', truncated: false });
});


test('mid-turn steering requires active generation and submits through ordinary send control', async () => {
  const html = '<form><div id="prompt-textarea" role="textbox" contenteditable="true"></div><button data-testid="stop-button"></button><button data-testid="send-button"></button></form>';
  const parsed = parseHTML(html);
  Object.assign(globalThis, {
    HTMLElement: parsed.window.HTMLElement,
    HTMLTextAreaElement: parsed.window.HTMLTextAreaElement,
    HTMLButtonElement: parsed.window.HTMLButtonElement,
    InputEvent: parsed.window.InputEvent ?? parsed.window.Event,
    MutationObserver: parsed.window.MutationObserver,
  });
  const composer = parsed.document.querySelector('#prompt-textarea')!;
  const send = parsed.document.querySelector('[data-testid="send-button"]')!;
  const stop = parsed.document.querySelector('[data-testid="stop-button"]')!;
  let clicks = 0;
  let stopClicks = 0;
  stop.addEventListener('click', () => { stopClicks += 1; });
  send.addEventListener('click', () => {
    clicks += 1;
    composer.textContent = '';
  });
  const adapter = new ChatGptWebAdapter({ document: parsed.document as unknown as Document, location: { href: 'https://chatgpt.com/c/c-1', pathname: '/c/c-1' } as Location });
  await adapter.submitSteering('Report to root, then continue.');
  assert.equal(clicks, 1);
  assert.equal(stopClicks, 0);
});

test('mid-turn steering fails closed instead of stopping an idle conversation', async () => {
  const html = '<form><div id="prompt-textarea" role="textbox" contenteditable="true"></div><button data-testid="send-button"></button></form>';
  await assert.rejects(fixture(html, '/c/c-1').submitSteering('steer'), /not generating/);
});

test('ordinary prompt submission still refuses active generation', async () => {
  const html = '<form><div id="prompt-textarea" role="textbox" contenteditable="true"></div><button data-testid="stop-button"></button><button data-testid="send-button"></button></form>';
  await assert.rejects(fixture(html, '/c/c-1').submitPrompt('ordinary'), /must wait for idle/);
});


test('rate-limit reset parser follows the next named local clock time', () => {
  const now = new Date(2026, 7, 21, 14, 0, 0, 0);
  const parsed = parseRateLimitReset("You've hit your usage limit. Try again at 3:45 PM.", now);
  assert.ok(parsed);
  const reset = new Date(parsed);
  assert.equal(reset.getHours(), 15);
  assert.equal(reset.getMinutes(), 45);
  assert.equal(reset.getDate(), now.getDate());
});

test('composer-adjacent Ask-GPT-style usage cap is classified with retry metadata', async () => {
  const adapter = fixture(`
    <section>
      <p>You've hit your usage limit. Try again at 3:45 PM.</p>
      <form><div id="prompt-textarea" contenteditable="true"></div><button data-testid="send-button"></button></form>
    </section>
  `, '/c/c-1');
  const issue = adapter.inspect().issue;
  assert.equal(issue?.code, 'USAGE_LIMIT');
  assert.ok(issue?.retryAt);
  await assert.rejects(adapter.waitFor('composer-ready', 10), (error: unknown) => {
    assert.ok(error instanceof ChatGptProductStateError);
    assert.equal(error.code, 'USAGE_LIMIT');
    assert.equal(error.retryAt, issue?.retryAt);
    return true;
  });
});

test('rate-limit detector ignores matching transcript text', () => {
  const adapter = fixture(`
    <article data-message-author-role="assistant">You've hit your usage limit. Try again at 3:45 PM.</article>
    <form><div id="prompt-textarea" contenteditable="true"></div><button data-testid="send-button"></button></form>
  `, '/c/c-1');
  assert.equal(adapter.inspect().issue, null);
});
