import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test, type Browser, type BrowserContext, type Page } from '@playwright/test';
import {
  closeLearnitiveSignalingServer,
  createLearnitiveSignalingServer,
} from '../../../../server/signaling.mjs';
import { createBrowserWorkspace } from './common';

const fixture = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  '../fixtures/interactive-collaboration-full-project.zip',
);

function relayInitScript(relayBase: string) {
  const NativeWebSocket = window.WebSocket;
  class RoutedWebSocket extends NativeWebSocket {
    constructor(url: string | URL, protocols?: string | string[]) {
      const original = new URL(String(url), window.location.href);
      let routedUrl: string | URL = url;
      if (original.pathname === '/collab-live' || original.pathname.startsWith('/collab-live/')) {
        const relay = new URL(relayBase);
        relay.pathname = original.pathname;
        relay.search = original.search;
        routedUrl = relay.toString();
      }
      if (protocols === undefined) super(routedUrl);
      else super(routedUrl, protocols);
    }
  }
  Object.defineProperty(window, 'WebSocket', { configurable: true, writable: true, value: RoutedWebSocket });
}

async function createGuest(browser: Browser, baseURL: string | undefined, relayBase: string): Promise<{ context: BrowserContext; page: Page }> {
  const context = await browser.newContext({ baseURL });
  const page = await context.newPage();
  await page.addInitScript(relayInitScript, relayBase);
  await page.goto('/');
  await page.evaluate(() => {
    localStorage.setItem('interactive-collaboration-user-id', 'anon:e2e-guest');
    localStorage.setItem('interactive-collaboration-name', 'Guest');
    localStorage.setItem('interactive-collaboration-color', '#2f80ed');
  });
  return { context, page };
}

async function sourceText(page: Page): Promise<string> {
  return page.getByLabel('LaTeX source editor').innerText();
}

async function expectSourcesConverged(a: Page, b: Page): Promise<void> {
  await expect.poll(async () => sourceText(a)).toBe(await sourceText(b));
}

async function selectCodeMirrorText(page: Page, needle: string): Promise<void> {
  await page.locator('.cm-content').evaluate((element, text) => {
    const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
    let node = walker.nextNode() as Text | null;
    while (node) {
      const index = node.data.indexOf(text);
      if (index >= 0) {
        const range = document.createRange();
        range.setStart(node, index);
        range.setEnd(node, index + text.length);
        const selection = window.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
        element.dispatchEvent(new Event('selectionchange', { bubbles: true }));
        return;
      }
      node = walker.nextNode() as Text | null;
    }
    throw new Error(`Unable to select ${text}`);
  }, needle);
}

async function startReviewRelay(): Promise<{ server: ReturnType<typeof createLearnitiveSignalingServer>; relayBase: string }> {
  const server = createLearnitiveSignalingServer({ allowUnsignedCollaborationHosts: true });
  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      server.off('error', reject);
      resolve();
    });
  });
  const address = server.address();
  if (!address || typeof address === 'string') {
    closeLearnitiveSignalingServer(server);
    throw new Error('Unable to resolve the local Learnitive collaboration review test port.');
  }
  return { server, relayBase: `ws://127.0.0.1:${address.port}` };
}

type CollaborationDebugWindow = Window & typeof globalThis & {
  services?: {
    core: {
      projectCollaboration: {
        getCollaborativeText: (wsName: string, path: string) => string | undefined;
        applyTextEdits: (wsName: string, path: string, edits: readonly { from: number; to: number; text: string }[], options?: { reviewSource?: string; trackReview?: boolean; semanticKind?: string; operationId?: string }) => boolean;
        setReviewTrackChangesEnabled: (wsName: string, enabled: boolean) => void;
        createSharedReviewComment: (wsName: string, input: { filePath?: string; from?: number; to?: number; body: string; scope?: string; quotedText?: string }) => { id: string } | undefined;
        replySharedReviewComment: (wsName: string, id: string, body: string) => boolean;
        setSharedReviewCommentResolved: (wsName: string, id: string, resolved: boolean) => boolean;
        getSharedReviewSnapshot: (wsName: string) => {
          comments: Array<{ id: string; body: string; status: string; replies: Array<{ body: string }> }>;
          changes: Array<{ id: string; type: string; originalText: string; proposedText: string; state: string; author: { name: string } }>;
        } | undefined;
        undoCollaborativeText: (wsName: string, path: string) => boolean;
        redoCollaborativeText: (wsName: string, path: string) => boolean;
      };
      latexSource: {
        getSnapshot: (wsPath: string) => { text: string } | undefined;
      };
      projectStorage: {
        getProvider: (wsName: string) => Promise<{
          listEntries: () => Promise<Array<{ kind: 'file' | 'directory'; path: string }>>;
          readFile: (path: string) => Promise<Uint8Array | undefined>;
        }>;
      };
    };
  };
};

async function getDebugYText(page: Page, wsName: string, projectPath = 'main.tex'): Promise<string> {
  return page.evaluate(({ wsName, projectPath }) => {
    const debugWindow = window as CollaborationDebugWindow;
    return debugWindow.services?.core.projectCollaboration.getCollaborativeText(wsName, projectPath) ?? '';
  }, { wsName, projectPath });
}

async function getDebugMirror(page: Page, wsName: string, projectPath = 'main.tex'): Promise<string> {
  return page.evaluate(({ wsName, projectPath }) => {
    const debugWindow = window as CollaborationDebugWindow;
    return debugWindow.services?.core.latexSource.getSnapshot(`${wsName}:${projectPath}`)?.text ?? '';
  }, { wsName, projectPath });
}

async function applyDeterministicYjsEdits(
  page: Page,
  wsName: string,
  owner: 'H' | 'G',
  start: number,
  count: number,
): Promise<void> {
  await page.evaluate(({ wsName, owner, start, count }) => {
    const debugWindow = window as CollaborationDebugWindow;
    const collaboration = debugWindow.services?.core.projectCollaboration;
    if (!collaboration) throw new Error('Debug collaboration service is unavailable.');
    for (let offset = 0; offset < count; offset += 1) {
      const sequence = start + offset;
      const current = collaboration.getCollaborativeText(wsName, 'main.tex');
      if (current === undefined) throw new Error('main.tex is not collaboratively bound.');
      const marker = `${owner}${String(sequence).padStart(5, '0')}\n`;
      const tracked = sequence % 20 === 0;
      collaboration.setReviewTrackChangesEnabled(wsName, tracked);
      if (!collaboration.applyTextEdits(
        wsName,
        'main.tex',
        [{ from: current.length, to: current.length, text: marker }],
        tracked ? { reviewSource: 'toolbar', semanticKind: 'bf35-soak-edit', operationId: `soak-${owner}-${sequence}` } : { reviewSource: 'typing' },
      )) {
        throw new Error(`TX7 edit ${sequence} was rejected.`);
      }
      if (sequence % 100 === 0) {
        const after = collaboration.getCollaborativeText(wsName, 'main.tex');
        if (after === undefined) throw new Error('main.tex disappeared during BF35 review soak.');
        const comment = collaboration.createSharedReviewComment(wsName, {
          filePath: 'main.tex',
          from: after.length - marker.length,
          to: after.length,
          body: `Soak review ${sequence}`,
          quotedText: marker.trim(),
        });
        if (!comment) throw new Error(`BF35 soak comment ${sequence} was not created.`);
        if (sequence % 200 === 0) {
          if (!collaboration.replySharedReviewComment(wsName, comment.id, `Reviewed ${sequence}`)) throw new Error(`BF35 soak reply ${sequence} failed.`);
          if (!collaboration.setSharedReviewCommentResolved(wsName, comment.id, true)) throw new Error(`BF35 soak resolve ${sequence} failed.`);
        }
      }
    }
  }, { wsName, owner, start, count });
}

test('BF33/TX7.1 preserves one Source Yjs binding with exact named remote cursor placement and hardened lifecycle', async ({ page, browser, baseURL }) => {
  const collaborationServer = createLearnitiveSignalingServer({ allowUnsignedCollaborationHosts: true });
  await new Promise<void>((resolve, reject) => {
    collaborationServer.once('error', reject);
    collaborationServer.listen(0, '127.0.0.1', () => {
      collaborationServer.off('error', reject);
      resolve();
    });
  });
  const address = collaborationServer.address();
  if (!address || typeof address === 'string') {
    closeLearnitiveSignalingServer(collaborationServer);
    throw new Error('Unable to resolve the local Learnitive collaboration test port.');
  }
  const relayBase = `ws://127.0.0.1:${address.port}`;
  await page.addInitScript(relayInitScript, relayBase);
  const guest = await createGuest(browser, baseURL, relayBase);

  try {
    const stamp = Date.now();
    const hostWorkspace = `collab-tx7-${stamp}`;
    await page.goto('/projects');
    await page.evaluate(() => {
      localStorage.setItem('interactive-collaboration-user-id', 'anon:e2e-host');
      localStorage.setItem('interactive-collaboration-name', 'Host');
      localStorage.setItem('interactive-collaboration-color', '#eb5757');
    });
    await createBrowserWorkspace(page, { workspaceName: hostWorkspace });
    await page.getByLabel('Import Project ZIP').setInputFiles(fixture);
    await page.getByRole('link', { name: /^main\.tex$/ }).click();

    await page.getByRole('button', { name: 'Collaborate' }).click();
    const hostDialog = page.getByRole('dialog');
    await hostDialog.getByLabel('Your name').fill('Host');
    await hostDialog.getByRole('button', { name: 'Start collaboration' }).click();
    await expect(hostDialog.getByText(/^live$/i)).toBeVisible();
    const projectLink = await hostDialog.locator('input[readonly]').inputValue();
    expect(projectLink).toMatch(/\/join\/[0-9a-f-]+#[0-9a-f]{64}$/iu);
    await expect(hostDialog.getByLabel('Copy project link')).toBeVisible();
    await expect(hostDialog.getByAltText('QR code for the collaboration project link')).toBeVisible();
    await page.keyboard.press('Escape');

    // TX7 is intentionally Source-only and freezes the project structure.
    const visualButton = page.getByRole('button', { name: /^Visual$/ });
    await expect(visualButton).toBeDisabled();
    await expect(visualButton).toHaveAttribute('title', 'Unavailable during live collaboration');
    await expect(page.getByText('Visual editing is unavailable during live collaboration.')).toHaveCount(0);
    await expect(page.getByLabel('New File')).toBeDisabled();
    await expect(page.getByLabel('New Folder')).toBeDisabled();

    // Link-driven joining must verify a live host before allocating the browser project.
    await guest.page.goto(projectLink);
    await expect(guest.page).toHaveURL(new RegExp('/join/[0-9a-f-]+$', 'iu')); // raw #secret scrubbed immediately
    await expect(guest.page.getByText('Joining project…')).toBeVisible();
    await expect(guest.page.getByText(/Verifying the live host before creating a browser project/i)).toBeVisible();
    await expect(guest.page).toHaveURL(new RegExp(`ws#route=editor&wsPath=${encodeURIComponent(`${hostWorkspace}:main.tex`)}$`));
    await expect(guest.page.getByLabel('LaTeX source editor')).toBeVisible();

    // Initial project snapshot includes non-collaborative data/binary assets, but the tree is locked afterward.
    const guestExplorer = guest.page.getByTestId('interactive-file-explorer');
    for (const folder of ['analysis', 'data', 'figures', 'notes', 'references']) {
      const item = guestExplorer.getByRole('treeitem', { name: new RegExp(`^${folder}$`) });
      await expect(item).toBeVisible();
      if ((await item.getAttribute('aria-expanded')) !== 'true') await item.click();
    }
    for (const file of ['script.py', 'table.csv', 'payload.bin', 'collab.png', 'readme.md', 'sample.pdf']) {
      await expect(guestExplorer.getByRole('treeitem', { name: new RegExp(file.replace('.', '\\.')) })).toBeVisible();
    }
    await expect(guest.page.getByLabel('New File')).toBeDisabled();
    await expect(guest.page.getByRole('button', { name: /^Visual$/ })).toBeDisabled();

    const hostSource = page.getByLabel('LaTeX source editor');
    const guestSource = guest.page.getByLabel('LaTeX source editor');

    // Exact regression from the reported stale-cursor failure.
    const screenshotRegression = [
      '\\documentclass{article}',
      '\\begin{document}',
      '\\section{Introduction}',
      'Start writing herfge.',
      '\\end{document}',
      '',
    ].join('\n');
    await hostSource.fill(screenshotRegression);
    await expect(guestSource).toContainText('Start writing herfge.');
    await hostSource.click();
    await page.keyboard.press('Control+Home');
    await page.keyboard.press('ArrowDown');
    await page.keyboard.press('ArrowDown');
    await page.keyboard.press('ArrowDown');
    await page.keyboard.press('End');
    await page.keyboard.type(' I am user.');
    await expect(guestSource).toContainText('Start writing herfge. I am user.');
    const hostCursor = guest.page.locator('.cm-ySelectionCaret').filter({ has: guest.page.locator('.cm-ySelectionInfo', { hasText: 'Host' }) }).first();
    await expect(hostCursor).toBeVisible();
    await expect(hostCursor.locator('.cm-ySelectionInfo')).toHaveText('Host');
    await expect.poll(async () => hostCursor.evaluate((node) => node.closest('.cm-line')?.textContent ?? '')).toContain('Start writing herfge. I am user.');

    // Compilation remains browser-local and must consume the converged Y.Text mirror while the peer stays connected.
    await page.getByRole('button', { name: 'Compile', exact: true }).click();
    const compileStatus = page.getByTestId('interactive-log-status-dot');
    await expect(compileStatus).toHaveAttribute('data-compile-status', 'success', { timeout: 180_000 });
    await page.getByRole('button', { name: 'Close PDF and return to editor' }).click();
    await expect(guestSource).toContainText('Start writing herfge. I am user.');

    // Remote insertion before Guest's logical caret must not strand the caret on an old absolute offset.
    await hostSource.fill('Alpha line\nTARGET-LINE\n');
    await expect(guestSource).toContainText('TARGET-LINE');
    await guestSource.click();
    await guest.page.keyboard.press('Control+End');
    await guest.page.keyboard.press('Home');
    await expect(page.locator('.cm-ySelectionInfo').filter({ hasText: 'Guest' }).first()).toBeVisible();
    await hostSource.click();
    await page.keyboard.press('Control+Home');
    await page.keyboard.type('REMOTE-ONE\nREMOTE-TWO\n');
    await guest.page.keyboard.type('GUEST-');
    await expect(hostSource).toContainText('GUEST-TARGET-LINE');
    await expect(hostSource).toContainText('REMOTE-ONE\nREMOTE-TWO\nAlpha line');

    // Collaborative undo is local-user scoped: Guest undo must preserve Host's later edit.
    await hostSource.fill('BASE\n');
    await guestSource.click();
    await guest.page.keyboard.press('Control+End');
    await guest.page.keyboard.type('GUEST-LOCAL\n');
    await expect(hostSource).toContainText('GUEST-LOCAL');
    await hostSource.click();
    await page.keyboard.press('Control+Home');
    await page.keyboard.type('HOST-REMOTE-KEPT\n');
    await expect(guestSource).toContainText('HOST-REMOTE-KEPT');
    await guestSource.click();
    await guest.page.keyboard.press(process.platform === 'darwin' ? 'Meta+z' : 'Control+z');
    await expect(hostSource).not.toContainText('GUEST-LOCAL');
    await expect(hostSource).toContainText('HOST-REMOTE-KEPT');

    // Representative Source mutation matrix: newline, paste, delete and concurrent typing.
    await hostSource.fill('A middle Z\nsecond line\n');
    await guestSource.click();
    await guest.page.keyboard.press('Control+Home');
    await guest.page.keyboard.press('End');
    await guest.page.keyboard.press('Enter');
    await guest.page.keyboard.insertText('guest paste line 1\nguest paste line 2');
    await expect(hostSource).toContainText('guest paste line 2');
    await hostSource.click();
    await page.keyboard.press('Control+End');
    await page.keyboard.type('HOST-END');
    await guestSource.click();
    await guest.page.keyboard.press('Control+Home');
    await guest.page.keyboard.type('GUEST-BEGIN-');
    await expect(hostSource).toContainText('GUEST-BEGIN-');
    await expect(guestSource).toContainText('HOST-END');
    await expectSourcesConverged(page, guest.page);

    // Offline Yjs editing must converge on reconnect without a custom merge algorithm.
    await guest.context.setOffline(true);
    await guestSource.click();
    await guest.page.keyboard.press('Control+End');
    await guest.page.keyboard.type('\nGUEST-OFFLINE');
    await hostSource.click();
    await page.keyboard.press('Control+End');
    await page.keyboard.type('\nHOST-WHILE-GUEST-OFFLINE');
    await guest.context.setOffline(false);
    await expect(guestSource).toContainText('HOST-WHILE-GUEST-OFFLINE', { timeout: 15_000 });
    await expect(hostSource).toContainText('GUEST-OFFLINE', { timeout: 15_000 });
    await expectSourcesConverged(page, guest.page);

    // BF33: a Host page refresh reclaims the same Host session instead of returning as a Guest.
    await page.reload();
    await expect(page.getByLabel('LaTeX source editor')).toBeVisible();
    await expect.poll(async () => page.getByRole('button', { name: 'Collaborate' }).isVisible()).toBe(true);
    await page.getByRole('button', { name: 'Collaborate' }).click();
    await expect(page.getByRole('dialog').getByText('You are hosting')).toBeVisible({ timeout: 15_000 });
    await page.keyboard.press('Escape');
    const refreshedHostSource = page.getByLabel('LaTeX source editor');
    await guest.page.getByLabel('LaTeX source editor').click();
    await guest.page.keyboard.press('Control+End');
    await guest.page.keyboard.type('\nGUEST-AFTER-HOST-REFRESH');
    await expect(refreshedHostSource).toContainText('GUEST-AFTER-HOST-REFRESH', { timeout: 15_000 });

    // BF33: leaving, editing the same path locally, then rejoining must preserve the divergent local file.
    await guest.page.getByRole('button', { name: 'Collaborate' }).click();
    await guest.page.getByRole('dialog').getByRole('button', { name: 'Leave collaboration' }).click();
    await expect(guest.page.getByRole('button', { name: /^Visual$/ })).toBeEnabled();
    const localGuestSource = guest.page.getByLabel('LaTeX source editor');
    await localGuestSource.fill('LOCAL-DIVERGENT-MAIN\n');
    await localGuestSource.press(process.platform === 'darwin' ? 'Meta+s' : 'Control+s');
    await refreshedHostSource.fill('HOST-CANONICAL-AFTER-DIVERGENCE\n');
    await expect(refreshedHostSource).toContainText('HOST-CANONICAL-AFTER-DIVERGENCE');
    await guest.page.goto(projectLink);
    await expect(guest.page.getByLabel('LaTeX source editor')).toContainText('HOST-CANONICAL-AFTER-DIVERGENCE', { timeout: 15_000 });
    const recovery = await guest.page.evaluate(async ({ workspaceName }) => {
      const debugWindow = window as CollaborationDebugWindow;
      const provider = await debugWindow.services?.core.projectStorage.getProvider(workspaceName);
      if (!provider) return undefined;
      const entries = await provider.listEntries();
      const recoveryEntry = entries.find((entry) => entry.kind === 'file' && entry.path.startsWith('.interactive-local-recovery/') && entry.path.endsWith('/main.tex'));
      if (!recoveryEntry) return undefined;
      const bytes = await provider.readFile(recoveryEntry.path);
      return bytes ? new TextDecoder().decode(bytes) : undefined;
    }, { workspaceName: hostWorkspace });
    expect(recovery).toContain('LOCAL-DIVERGENT-MAIN');

    // Reopening the same Project link reuses the browser workspace instead of creating Paper (2), Paper (3), ...
    await guest.page.goto('/projects');
    await guest.page.goto(projectLink);
    await expect(guest.page).toHaveURL(new RegExp(`ws#route=editor&wsPath=${encodeURIComponent(`${hostWorkspace}:main.tex`)}$`));
    await guest.page.goto('/projects');
    await expect(guest.page.getByText(hostWorkspace, { exact: true })).toHaveCount(1);

    // Host Stop explicitly tombstones the live session. The joined browser project remains locally usable.
    await page.getByRole('button', { name: 'Collaborate' }).click();
    await page.getByRole('dialog').getByRole('button', { name: 'Stop collaboration' }).click();
    await expect(page.getByRole('button', { name: /^Visual$/ })).toBeEnabled();
    await guest.page.goto(projectLink);
    await expect(guest.page.getByText('Unable to join project')).toBeVisible();
    await expect(guest.page.getByText(/session has ended|host is currently offline/i)).toBeVisible();
  } finally {
    await guest.context.close().catch(() => undefined);
    await new Promise<void>((resolve) => closeLearnitiveSignalingServer(collaborationServer, () => resolve()));
  }
});

test('BF34/TX7.2 shares comments, replies and resolved state across live collaborators and reconnects', async ({ page, browser, baseURL }) => {
  const { server, relayBase } = await startReviewRelay();
  await page.addInitScript(relayInitScript, relayBase);
  const guest = await createGuest(browser, baseURL, relayBase);

  try {
    const workspaceName = `collab-review-comments-${Date.now()}`;
    await page.goto('/projects?debug=true');
    await page.evaluate(() => {
      localStorage.setItem('interactive-collaboration-user-id', 'anon:e2e-alice');
      localStorage.setItem('interactive-collaboration-name', 'Alice');
      localStorage.setItem('interactive-collaboration-color', '#eb5757');
    });
    await guest.page.evaluate(() => {
      localStorage.setItem('interactive-collaboration-user-id', 'anon:e2e-bob');
      localStorage.setItem('interactive-collaboration-name', 'Bob');
      localStorage.setItem('interactive-collaboration-color', '#2f80ed');
    });
    await createBrowserWorkspace(page, { workspaceName });
    const source = page.getByLabel('LaTeX source editor');
    const manuscript = [
      '\\documentclass{article}',
      '\\begin{document}',
      '\\section{Introduction}',
      'This study investigates X.',
      '\\end{document}',
      '',
    ].join('\n');
    await source.fill(manuscript);

    await page.getByRole('button', { name: 'Collaborate' }).click();
    const hostDialog = page.getByRole('dialog');
    await hostDialog.getByLabel('Your name').fill('Alice');
    await hostDialog.getByRole('button', { name: 'Start collaboration' }).click();
    await expect(hostDialog.getByText(/^live$/i)).toBeVisible();
    const projectLink = await hostDialog.locator('input[readonly]').inputValue();
    await page.keyboard.press('Escape');

    await guest.page.goto(projectLink.replace('#', '?debug=true#'));
    await expect(guest.page.getByLabel('LaTeX source editor')).toBeVisible();
    const guestSidebarToggle = guest.page.getByTestId('review-toggle');
    await guestSidebarToggle.click();
    const guestSidebar = guest.page.getByTestId('review-sidebar');
    await expect(guestSidebar).toBeVisible();

    await selectCodeMirrorText(page, 'Introduction');
    const manuscriptActions = page.getByTestId('paper-memory-manuscript-actions');
    await manuscriptActions.getByRole('button', { name: 'Comment', exact: true }).click();
    const commentDialog = page.getByRole('dialog', { name: 'Add comment' });
    await commentDialog.getByPlaceholder('Write a review comment…').fill('Please clarify this section.');
    await commentDialog.getByRole('button', { name: 'Add comment' }).click();

    await expect(guestSidebar).toContainText('Please clarify this section.');
    await expect(guestSidebar).toContainText('Alice');
    const guestThread = guestSidebar.locator('[data-review-thread]').filter({ hasText: 'Please clarify this section.' });
    await guestThread.getByRole('button', { name: 'Reply', exact: true }).click();
    await guestThread.getByPlaceholder('Reply…').fill('Updated.');
    await guestThread.getByRole('button', { name: 'Reply', exact: true }).click();

    const hostSidebar = page.getByTestId('review-sidebar');
    await expect(hostSidebar).toContainText('Updated.');
    await expect(hostSidebar).toContainText('Bob');
    await guestThread.getByRole('button', { name: 'Resolve', exact: true }).click();
    await hostSidebar.getByRole('button', { name: 'Resolved', exact: true }).click();
    await expect(hostSidebar).toContainText('Please clarify this section.');
    await expect(hostSidebar.getByText('resolved', { exact: true })).toBeVisible();

    await expect.poll(() => page.evaluate(({ workspaceName }) => {
      const snapshot = (window as CollaborationDebugWindow).services?.core.projectCollaboration.getSharedReviewSnapshot(workspaceName);
      const comment = snapshot?.comments.find((item) => item.body === 'Please clarify this section.');
      return comment ? { status: comment.status, replies: comment.replies.map((reply) => reply.body).sort() } : undefined;
    }, { workspaceName })).toEqual({ status: 'resolved', replies: ['Updated.'] });

    // Refresh both participants to exercise same-room Yjs/y-indexeddb review restoration.
    await page.reload();
    await expect(page.getByLabel('LaTeX source editor')).toBeVisible();
    await guest.page.reload();
    await expect(guest.page.getByLabel('LaTeX source editor')).toBeVisible();
    await page.getByTestId('review-toggle').click();
    const reopened = page.getByTestId('review-sidebar');
    await reopened.getByRole('button', { name: 'Resolved', exact: true }).click();
    await expect(reopened).toContainText('Please clarify this section.');
    await expect(reopened).toContainText('Updated.');

    const cleanSource = await page.getByLabel('LaTeX source editor').inputValue();
    expect(cleanSource).not.toContain('LEARNITIVE-REVIEW');
    expect(cleanSource).not.toContain('<###');
  } finally {
    await guest.context.close().catch(() => undefined);
    await new Promise<void>((resolve) => closeLearnitiveSignalingServer(server, () => resolve()));
  }
});

test('BF34/TX7.2 supervisor replacement is one shared tracked change and Reject restores both replicas', async ({ page, browser, baseURL }) => {
  const { server, relayBase } = await startReviewRelay();
  await page.addInitScript(relayInitScript, relayBase);
  const guest = await createGuest(browser, baseURL, relayBase);

  try {
    const workspaceName = `collab-review-track-${Date.now()}`;
    await page.goto('/projects?debug=true');
    await page.evaluate(() => {
      localStorage.setItem('interactive-collaboration-user-id', 'anon:e2e-alice-track');
      localStorage.setItem('interactive-collaboration-name', 'Alice');
      localStorage.setItem('interactive-collaboration-color', '#eb5757');
    });
    await guest.page.evaluate(() => {
      localStorage.setItem('interactive-collaboration-user-id', 'anon:e2e-bob-track');
      localStorage.setItem('interactive-collaboration-name', 'Bob');
      localStorage.setItem('interactive-collaboration-color', '#2f80ed');
    });
    await createBrowserWorkspace(page, { workspaceName });
    const hostSource = page.getByLabel('LaTeX source editor');
    const manuscript = [
      '\\documentclass{article}',
      '\\begin{document}',
      '\\section{Introduction}',
      '',
      'This study investigates X.',
      '\\end{document}',
      '',
    ].join('\n');
    await hostSource.fill(manuscript);

    await page.getByRole('button', { name: 'Collaborate' }).click();
    const hostDialog = page.getByRole('dialog');
    await hostDialog.getByLabel('Your name').fill('Alice');
    await hostDialog.getByRole('button', { name: 'Start collaboration' }).click();
    await expect(hostDialog.getByText(/^live$/i)).toBeVisible();
    const projectLink = await hostDialog.locator('input[readonly]').inputValue();
    await page.keyboard.press('Escape');

    await guest.page.goto(projectLink.replace('#', '?debug=true#'));
    const guestSource = guest.page.getByLabel('LaTeX source editor');
    await expect(guestSource).toContainText('This study investigates X.');
    await guest.page.getByTestId('review-toggle').click();
    const guestSidebar = guest.page.getByTestId('review-sidebar');

    const track = page.getByTestId('track-changes-toggle');
    await track.click();
    await expect(track).toHaveAttribute('aria-pressed', 'true');
    await selectCodeMirrorText(page, 'investigates');
    await page.keyboard.type('examines');

    await expect(guestSource).toContainText('This study examines X.');
    await expect(guestSidebar.getByTestId('review-change-replace')).toHaveCount(1);
    await expect(guestSidebar.getByTestId('review-change-insert')).toHaveCount(0);
    await expect(guestSidebar).toContainText('Alice');
    await expect(guestSidebar).toContainText('investigates');
    await expect(guestSidebar).toContainText('examines');

    await expect.poll(() => guest.page.evaluate(({ workspaceName }) => {
      const changes = (window as CollaborationDebugWindow).services?.core.projectCollaboration.getSharedReviewSnapshot(workspaceName)?.changes ?? [];
      return changes.map((change) => ({ type: change.type, originalText: change.originalText, proposedText: change.proposedText, state: change.state }));
    }, { workspaceName })).toEqual([{ type: 'replace', originalText: 'investigates', proposedText: 'examines', state: 'pending' }]);

    await guestSidebar.getByRole('button', { name: 'Reject', exact: true }).click();
    await expect(hostSource).toContainText('This study investigates X.');
    await expect(guestSource).toContainText('This study investigates X.');
    await expectSourcesConverged(page, guest.page);
    await expect.poll(() => guest.page.evaluate(({ workspaceName }) => {
      return (window as CollaborationDebugWindow).services?.core.projectCollaboration.getSharedReviewSnapshot(workspaceName)?.changes[0]?.state;
    }, { workspaceName })).toBe('rejected');

    expect(await hostSource.inputValue()).not.toContain('LEARNITIVE-REVIEW');
  } finally {
    await guest.context.close().catch(() => undefined);
    await new Promise<void>((resolve) => closeLearnitiveSignalingServer(server, () => resolve()));
  }
});

// Formal release soak. It is intentionally opt-in so ordinary local development
// does not spend 30 minutes here; release certification sets LEARNITIVE_TX7_SOAK=1.
test('BF35/TX7.2.1 10,000-edit reconnect soak adds 500 tracked changes and 200 comment/reply/resolve operations while replicas converge', async ({ page, browser, baseURL }) => {
  test.skip(process.env.LEARNITIVE_TX7_SOAK !== '1' && process.env.LEARNITIVE_RELEASE_QUALITY !== '1', 'Set LEARNITIVE_TX7_SOAK=1 or run formal release quality for the BF35 collaboration/review soak.');
  test.setTimeout(35 * 60 * 1000);

  const collaborationServer = createLearnitiveSignalingServer({ allowUnsignedCollaborationHosts: true });
  await new Promise<void>((resolve, reject) => {
    collaborationServer.once('error', reject);
    collaborationServer.listen(0, '127.0.0.1', () => {
      collaborationServer.off('error', reject);
      resolve();
    });
  });
  const address = collaborationServer.address();
  if (!address || typeof address === 'string') {
    closeLearnitiveSignalingServer(collaborationServer);
    throw new Error('Unable to resolve the local TX7 soak relay port.');
  }
  const relayBase = `ws://127.0.0.1:${address.port}`;
  await page.addInitScript(relayInitScript, relayBase);
  const guest = await createGuest(browser, baseURL, relayBase);

  try {
    const workspaceName = `collab-tx7-soak-${Date.now()}`;
    await page.goto('/projects?debug=true');
    await page.evaluate(() => {
      localStorage.setItem('interactive-collaboration-user-id', 'anon:e2e-soak-host');
      localStorage.setItem('interactive-collaboration-name', 'Soak Host');
      localStorage.setItem('interactive-collaboration-color', '#eb5757');
    });
    await createBrowserWorkspace(page, { workspaceName });
    await page.getByLabel('Import Project ZIP').setInputFiles(fixture);
    await page.getByRole('link', { name: /^main\.tex$/ }).click();
    await page.getByRole('button', { name: 'Collaborate' }).click();
    const hostDialog = page.getByRole('dialog');
    await hostDialog.getByLabel('Your name').fill('Soak Host');
    await hostDialog.getByRole('button', { name: 'Start collaboration' }).click();
    await expect(hostDialog.getByText(/^live$/i)).toBeVisible();
    const projectLink = await hostDialog.locator('input[readonly]').inputValue();
    await page.keyboard.press('Escape');

    const debugJoinLink = projectLink.replace('#', '?debug=true#');
    await guest.page.goto(debugJoinLink);
    await expect(guest.page.getByLabel('LaTeX source editor')).toBeVisible();
    await expect.poll(() => page.evaluate(() => Boolean((window as CollaborationDebugWindow).services))).toBe(true);
    await expect.poll(() => guest.page.evaluate(() => Boolean((window as CollaborationDebugWindow).services))).toBe(true);

    const hostSource = page.getByLabel('LaTeX source editor');
    const guestSource = guest.page.getByLabel('LaTeX source editor');
    await hostSource.fill('\\documentclass{article}\n\\begin{document}\nSOAK-START\n\\end{document}\n');
    await expect(guestSource).toContainText('SOAK-START');

    const batchSize = 250;
    const batchCount = 10_000 / batchSize;
    for (let batch = 0; batch < batchCount; batch += 1) {
      // Twice during the soak the guest edits offline while the host continues.
      if (batch === 9 || batch === 29) await guest.context.setOffline(true);

      const owner = batch % 2 === 0 ? 'H' : 'G';
      const ownerPage = owner === 'H' ? page : guest.page;
      await applyDeterministicYjsEdits(ownerPage, workspaceName, owner, batch * batchSize, batchSize);

      // Exercise local collaborative history without changing the final batch result.
      if (batch % 4 === 3) {
        await ownerPage.evaluate(({ workspaceName }) => {
          const collaboration = (window as CollaborationDebugWindow).services?.core.projectCollaboration;
          if (!collaboration?.undoCollaborativeText(workspaceName, 'main.tex')) throw new Error('TX7 soak undo was unavailable.');
          if (!collaboration.redoCollaborativeText(workspaceName, 'main.tex')) throw new Error('TX7 soak redo was unavailable.');
        }, { workspaceName });
      }

      if (batch === 10 || batch === 30) await guest.context.setOffline(false);

      // Every 1,000 edits switch out to a snapshot code file and back to the shared Source file.
      if ((batch + 1) % 4 === 0) {
        const explorer = guest.page.getByTestId('interactive-file-explorer');
        const analysis = explorer.getByRole('treeitem', { name: /^analysis$/ });
        if ((await analysis.getAttribute('aria-expanded')) !== 'true') await analysis.click();
        await explorer.getByRole('treeitem', { name: /^script\.py$/ }).click();
        await explorer.getByRole('treeitem', { name: /^main\.tex$/ }).click();
        await expect(guest.page.getByLabel('LaTeX source editor')).toBeVisible();
      }

      // Checkpoints compare both Y.Text replicas and their one-way LatexSource mirrors.
      if ((batch + 1) % 2 === 0 && batch !== 9 && batch !== 29) {
        await expect.poll(() => getDebugYText(guest.page, workspaceName), { timeout: 15_000 }).toBe(await getDebugYText(page, workspaceName));
        await expect.poll(() => getDebugMirror(page, workspaceName), { timeout: 15_000 }).toBe(await getDebugYText(page, workspaceName));
        await expect.poll(() => getDebugMirror(guest.page, workspaceName), { timeout: 15_000 }).toBe(await getDebugYText(guest.page, workspaceName));
      }
    }

    await guest.context.setOffline(false);
    const finalHostYText = await getDebugYText(page, workspaceName);
    await expect.poll(() => getDebugYText(guest.page, workspaceName), { timeout: 30_000 }).toBe(finalHostYText);
    await expect.poll(() => getDebugMirror(page, workspaceName), { timeout: 15_000 }).toBe(finalHostYText);
    await expect.poll(() => getDebugMirror(guest.page, workspaceName), { timeout: 15_000 }).toBe(finalHostYText);

    // CodeMirror must render the converged tail, not a stale local snapshot.
    await hostSource.click();
    await page.keyboard.press('Control+End');
    await guestSource.click();
    await guest.page.keyboard.press('Control+End');
    await expect(hostSource).toContainText('G09999');
    await expect(guestSource).toContainText('G09999');

    // BF35/TX7.2.1 review-soak invariants: 10,000 text edits include exactly
    // 500 independently tracked programmatic changes plus 100 comments, 50
    // replies and 50 resolves (200 review operations total).
    const expectedReviewSummary = { changes: 500, comments: 100, replies: 50, resolved: 50 };
    const summarizeReview = (snapshot: ReturnType<NonNullable<CollaborationDebugWindow['services']>['core']['projectCollaboration']['getSharedReviewSnapshot']>) => ({
      changes: snapshot?.changes.length ?? -1,
      comments: snapshot?.comments.length ?? -1,
      replies: snapshot?.comments.reduce((total, comment) => total + comment.replies.length, 0) ?? -1,
      resolved: snapshot?.comments.filter((comment) => comment.status === 'resolved').length ?? -1,
    });
    await expect.poll(() => page.evaluate(({ workspaceName }) => {
      const snapshot = (window as CollaborationDebugWindow).services?.core.projectCollaboration.getSharedReviewSnapshot(workspaceName);
      return {
        changes: snapshot?.changes.length ?? -1,
        comments: snapshot?.comments.length ?? -1,
        replies: snapshot?.comments.reduce((total, comment) => total + comment.replies.length, 0) ?? -1,
        resolved: snapshot?.comments.filter((comment) => comment.status === 'resolved').length ?? -1,
      };
    }, { workspaceName }), { timeout: 30_000 }).toEqual(expectedReviewSummary);
    const hostReview = await page.evaluate(({ workspaceName }) => (window as CollaborationDebugWindow).services?.core.projectCollaboration.getSharedReviewSnapshot(workspaceName), { workspaceName });
    await expect.poll(() => guest.page.evaluate(({ workspaceName }) => {
      const snapshot = (window as CollaborationDebugWindow).services?.core.projectCollaboration.getSharedReviewSnapshot(workspaceName);
      return {
        changes: snapshot?.changes.length ?? -1,
        comments: snapshot?.comments.length ?? -1,
        replies: snapshot?.comments.reduce((total, comment) => total + comment.replies.length, 0) ?? -1,
        resolved: snapshot?.comments.filter((comment) => comment.status === 'resolved').length ?? -1,
      };
    }, { workspaceName }), { timeout: 30_000 }).toEqual(summarizeReview(hostReview));
  } finally {
    await guest.context.close().catch(() => undefined);
    await new Promise<void>((resolve) => closeLearnitiveSignalingServer(collaborationServer, () => resolve()));
  }
});
