// @ts-nocheck
/**
 * Chat harness — scroll behaviour + markdown rendering.
 *
 * Two related properties on the same chat surface:
 *
 *   1. Scroll: the message column is anchored to the bottom by the
 *      `useStickToBottom` hook (`app/src/hooks/useStickToBottom.ts`).
 *      After several messages, the container's `scrollTop` must sit
 *      within a small margin of `scrollHeight - clientHeight`.
 *      When the user manually scrolls UP, the auto-stick releases
 *      (so we don't yank them away from the message they're reading).
 *
 *   2. Markdown rendering: `BubbleMarkdown` (in
 *      `app/src/pages/conversations/components/AgentMessageBubble.tsx`)
 *      runs assistant content through `Markdown`. Bold, code blocks
 *      and links must produce the right DOM tags (`<strong>`, `<pre>`,
 *      `<code>`, `<a>`).
 *
 * We script the mock LLM to reply with a markdown blob containing all
 * three constructs at once, and use the same exchange to fill the
 * thread for the scroll asserts.
 */
import { waitForApp } from '../helpers/app-helpers';
import {
  chatMounted,
  clickByTitle,
  clickSend,
  typeIntoComposer,
  waitForSocketConnected,
} from '../helpers/chat-harness';
import { textExists } from '../helpers/element-helpers';
import { resetApp } from '../helpers/reset-app';
import { navigateViaHash } from '../helpers/shared-flows';
import { setMockBehavior, startMockServer, stopMockServer } from '../mock-server';

const USER_ID = 'e2e-chat-harness-scroll-render';

const CANARY_BOLD = 'BOLD-CANARY-22ff';
const CANARY_CODE = 'CODE-CANARY-93b1';
const LINK_URL = 'https://example.com/canary';

const REPLY_MARKDOWN = [
  `**${CANARY_BOLD}** is bold.`,
  '',
  '```',
  `${CANARY_CODE}`,
  'line 2',
  '```',
  '',
  `Visit [the docs](${LINK_URL}) for more.`,
].join('\n');

// Lots of message lines so the column actually has overflow.
const FILLER_LINES = Array.from(
  { length: 30 },
  (_, i) => `Filler line ${i + 1} — autogenerated to grow the scroll column.`
);

const STREAM_SCRIPT = [
  ...FILLER_LINES.map(line => ({ text: line + '\n', delayMs: 5 })),
  { text: '\n', delayMs: 5 },
  { text: REPLY_MARKDOWN, delayMs: 10 },
  { finish: 'stop' },
];

// The message column is the `<div ref={messagesContainerRef} className="flex-1 overflow-y-auto ...">`
// in Conversations.tsx. There's only one in /chat so a CSS predicate
// matching the unique `bg-[#f6f6f6]` class is enough.
async function scrollMetrics(): Promise<{
  scrollTop: number;
  scrollHeight: number;
  clientHeight: number;
}> {
  return (await browser.execute(() => {
    const el = document.querySelector(
      'div.flex-1.overflow-y-auto.bg-\\[\\#f6f6f6\\]'
    ) as HTMLElement | null;
    if (!el) return { scrollTop: 0, scrollHeight: 0, clientHeight: 0 };
    return {
      scrollTop: el.scrollTop,
      scrollHeight: el.scrollHeight,
      clientHeight: el.clientHeight,
    };
  })) as { scrollTop: number; scrollHeight: number; clientHeight: number };
}

async function scrollMessageColumn(top: number): Promise<void> {
  await browser.execute((y: number) => {
    const el = document.querySelector(
      'div.flex-1.overflow-y-auto.bg-\\[\\#f6f6f6\\]'
    ) as HTMLElement | null;
    if (el) el.scrollTo({ top: y, behavior: 'auto' });
  }, top);
}

describe('Chat harness — scroll + markdown render', () => {
  before(async function beforeSuite() {
    this.timeout(90_000);
    await startMockServer();
    await waitForApp();
    await resetApp(USER_ID);

    setMockBehavior('llmStreamScript', JSON.stringify(STREAM_SCRIPT));
    setMockBehavior('llmStreamChunkDelayMs', '5');
  });

  after(async () => {
    setMockBehavior('llmStreamScript', '');
    setMockBehavior('llmStreamChunkDelayMs', '');
    await stopMockServer();
  });

  // One `it` covers stream → markdown render → scroll-anchor → scroll-up
  // release because all four assertions are facts about the SAME chat
  // exchange. Splitting them into separate Mocha tests would make each
  // case rely on state produced by the previous one — a fragile shape
  // CodeRabbit flagged. Keeping the asserts together also keeps the
  // failure-mode obvious: if streaming dies, no later check executes.
  it('streams long markdown, renders it, auto-anchors to bottom, releases on scroll-up', async function () {
    this.timeout(90_000);
    await navigateViaHash('/chat');
    await browser.waitUntil(async () => await chatMounted(), {
      timeout: 15_000,
      timeoutMsg: 'Conversations did not mount',
    });
    expect(await clickByTitle('New thread', 8_000)).toBe(true);

    await typeIntoComposer('Reply with the markdown sample please.');
    const socketReady = await waitForSocketConnected(30_000);
    if (!socketReady) {
      console.warn(
        '[chat-harness-scroll-render] socket did not connect within 30 s — send may fail'
      );
    }
    expect(
      await browser.waitUntil(async () => await clickSend(), {
        timeout: 5_000,
        timeoutMsg: 'Send button never enabled',
      })
    ).toBe(true);

    // ── 1. Stream completes: both canaries arrive ──────────────────
    await browser.waitUntil(async () => await textExists(CANARY_BOLD), {
      timeout: 40_000,
      timeoutMsg: 'bold canary never landed',
    });
    await browser.waitUntil(async () => await textExists(CANARY_CODE), {
      timeout: 20_000,
      timeoutMsg: 'code canary never landed',
    });

    // ── 2. Markdown renders to the expected DOM tags ───────────────
    const tags = await browser.execute(
      (boldCanary: string, codeCanary: string, linkUrl: string) => {
        const strongs = Array.from(document.querySelectorAll('strong')).map(
          s => s.textContent ?? ''
        );
        const codes = Array.from(document.querySelectorAll('pre code, pre')).map(
          c => c.textContent ?? ''
        );
        const anchors = Array.from(document.querySelectorAll('a[href]')).map(a => ({
          href: (a as HTMLAnchorElement).getAttribute('href') ?? '',
          text: a.textContent ?? '',
        }));
        return {
          hasBold: strongs.some(t => t.includes(boldCanary)),
          hasCode: codes.some(t => t.includes(codeCanary)),
          hasLink: anchors.some(a => a.href === linkUrl),
        };
      },
      CANARY_BOLD,
      CANARY_CODE,
      LINK_URL
    );
    expect(tags.hasBold).toBe(true);
    expect(tags.hasCode).toBe(true);
    expect(tags.hasLink).toBe(true);

    // ── 3. Auto-scroll anchored to the bottom after the stream ─────
    // (within 40 px to absorb sub-pixel layout drift)
    const atBottom = await scrollMetrics();
    expect(atBottom.scrollHeight).toBeGreaterThan(atBottom.clientHeight);
    expect(atBottom.scrollHeight - (atBottom.scrollTop + atBottom.clientHeight)).toBeLessThan(40);

    // ── 4. Manual scroll-up releases the auto-stick ────────────────
    const targetTop = Math.max(0, atBottom.scrollTop - Math.floor(atBottom.clientHeight / 2));
    await scrollMessageColumn(targetTop);
    await browser.pause(500); // let the stick hook react
    const afterScrollUp = await scrollMetrics();
    expect(Math.abs(afterScrollUp.scrollTop - targetTop)).toBeLessThan(40);
    expect(
      afterScrollUp.scrollHeight - (afterScrollUp.scrollTop + afterScrollUp.clientHeight)
    ).toBeGreaterThan(50);
  });
});
