import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { CollaborationReviewStore } from './collaboration-review';
import { COLLABORATION_REVIEW_TRANSACTION_META_KEY } from './collaboration-capabilities';
import type { CollaborationUser } from './index';

const host: CollaborationUser = {
  id: 'anon:test-host',
  username: 'Host',
  name: 'Host',
  color: '#ef4444',
  colorLight: '#ef444433',
};
const guest: CollaborationUser = {
  id: 'anon:test-guest',
  username: 'Guest',
  name: 'Guest',
  color: '#3b82f6',
  colorLight: '#3b82f633',
};

function project(initial = '') {
  const doc = new Y.Doc();
  const files = doc.getMap<Y.Text>('files');
  const text = new Y.Text();
  files.set('main.tex', text);
  if (initial) text.insert(0, initial);
  return { doc, files, text };
}

function sync(source: Y.Doc, target: Y.Doc): void {
  Y.applyUpdate(target, Y.encodeStateAsUpdate(source));
}

describe('TX7.2 collaborative review metadata', () => {
  it('keeps a selected comment attached to the selected text at both boundaries', () => {
    const { doc, files, text } = project('Introduction');
    const review = new CollaborationReviewStore(doc, files, () => host);
    const comment = review.createComment({ filePath: 'main.tex', from: 0, to: 'Introduction'.length, body: 'Clarify this.' });

    text.insert(0, 'New ');
    let current = review.getSnapshot().comments.find((item) => item.id === comment.id)!;
    expect(text.toString().slice(current.from, current.to)).toBe('Introduction');

    text.insert(current.to!, ' tail');
    current = review.getSnapshot().comments.find((item) => item.id === comment.id)!;
    expect(text.toString().slice(current.from, current.to)).toBe('Introduction');
    review.destroy();
  });

  it('retains a comment thread as anchor-removed when its quoted text is deleted', () => {
    const { doc, files, text } = project('Introduction follows.');
    const review = new CollaborationReviewStore(doc, files, () => host);
    const comment = review.createComment({ filePath: 'main.tex', from: 0, to: 'Introduction'.length, body: 'Keep history.' });
    text.delete(0, 'Introduction'.length);
    const current = review.getSnapshot().comments.find((item) => item.id === comment.id)!;
    expect(current.anchorRemoved).toBe(true);
    expect(current.quotedText).toBe('Introduction');
    expect(current.body).toBe('Keep history.');
    review.destroy();
  });

  it('merges simultaneous replies and a concurrent resolve without overwriting either reply', () => {
    const a = project('Introduction');
    const reviewA = new CollaborationReviewStore(a.doc, a.files, () => host);
    const comment = reviewA.createComment({ filePath: 'main.tex', from: 0, to: 12, body: 'Clarify.' });

    const bDoc = new Y.Doc();
    sync(a.doc, bDoc);
    const bFiles = bDoc.getMap<Y.Text>('files');
    const reviewB = new CollaborationReviewStore(bDoc, bFiles, () => guest);

    expect(reviewA.reply(comment.id, 'Host follow-up')).toBe(true);
    expect(reviewB.reply(comment.id, 'Guest reply')).toBe(true);
    expect(reviewA.setCommentResolved(comment.id, true)).toBe(true);

    sync(a.doc, bDoc);
    sync(bDoc, a.doc);
    sync(a.doc, bDoc);

    for (const snapshot of [reviewA.getSnapshot(), reviewB.getSnapshot()]) {
      const current = snapshot.comments.find((item) => item.id === comment.id)!;
      expect(current.status).toBe('resolved');
      expect(current.replies.map((reply) => reply.body).sort()).toEqual(['Guest reply', 'Host follow-up']);
    }
    reviewA.destroy();
    reviewB.destroy();
  });

  it('coalesces typing over a selection into one first-class replacement', () => {
    const initial = 'This study investigates X.';
    const { doc, files, text } = project(initial);
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    const from = initial.indexOf('investigates');

    doc.transact(() => {
      text.delete(from, 'investigates'.length);
      text.insert(from, 'e');
    }, 'interactive-collaboration-edit');
    for (const char of 'xamines') {
      doc.transact(() => text.insert(text.toString().indexOf(' X.'), char), 'interactive-collaboration-edit');
    }

    const changes = review.getSnapshot().changes;
    expect(text.toString()).toBe('This study examines X.');
    expect(changes).toHaveLength(1);
    expect(changes[0]).toMatchObject({ type: 'replace', originalText: 'investigates', proposedText: 'examines', state: 'pending' });
    review.destroy();
  });

  it('accepts a replacement without changing Y.Text and rejects another replacement atomically', () => {
    const initial = 'This study investigates X.';
    const { doc, files, text } = project(initial);
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    const from = initial.indexOf('investigates');

    doc.transact(() => {
      text.delete(from, 'investigates'.length);
      text.insert(from, 'examines');
    }, 'interactive-collaboration-edit');
    const first = review.getSnapshot().changes[0]!;
    expect(first).toMatchObject({ type: 'replace', originalText: 'investigates', proposedText: 'examines', state: 'pending' });
    const acceptedText = text.toString();
    expect(review.acceptChange(first.id)).toBe(true);
    expect(text.toString()).toBe(acceptedText);
    expect(review.getSnapshot().changes.find((item) => item.id === first.id)?.state).toBe('accepted');

    review.stopCoalescing('main.tex');
    const current = text.toString();
    const secondFrom = current.indexOf('examines');
    doc.transact(() => {
      text.delete(secondFrom, 'examines'.length);
      text.insert(secondFrom, 'reviews');
    }, 'interactive-collaboration-edit');
    const second = review.getSnapshot().changes.find((item) => item.state === 'pending')!;
    expect(second).toMatchObject({ type: 'replace', originalText: 'examines', proposedText: 'reviews' });
    expect(review.rejectChange(second.id)).toBe(true);
    expect(text.toString()).toBe(acceptedText);
    expect(review.getSnapshot().changes.find((item) => item.id === second.id)?.state).toBe('rejected');
    review.destroy();
  });

  it('shares deferred AI proposals without mutating Y.Text until Accept', () => {
    const initial = 'The method may offer improvements.';
    const a = project(initial);
    const reviewA = new CollaborationReviewStore(a.doc, a.files, () => host);
    const from = initial.indexOf('may offer improvements');
    const proposal = reviewA.proposeDeferredChange({
      filePath: 'main.tex',
      from,
      to: from + 'may offer improvements'.length,
      originalText: 'may offer improvements',
      proposedText: 'demonstrates consistent gains',
      author: { id: 'interactive-ai-suggest', displayName: 'Learnitive AI' },
      source: 'ai',
      semanticKind: 'prose',
      operationId: 'ai-operation-1',
      metadata: { automatic: true, category: 'writing', confidence: 0.91, provider: 'local', model: 'test-model' },
    });

    expect(a.text.toString()).toBe(initial);
    expect(proposal).toMatchObject({
      state: 'pending',
      source: 'ai',
      applicationMode: 'deferred',
      originalText: 'may offer improvements',
      proposedText: 'demonstrates consistent gains',
      metadata: { automatic: true, category: 'writing', confidence: 0.91, provider: 'local', model: 'test-model' },
    });

    const bDoc = new Y.Doc();
    sync(a.doc, bDoc);
    const bFiles = bDoc.getMap<Y.Text>('files');
    const reviewB = new CollaborationReviewStore(bDoc, bFiles, () => guest);
    expect(bFiles.get('main.tex')?.toString()).toBe(initial);
    expect(reviewB.getSnapshot().changes.find((item) => item.id === proposal.id)).toMatchObject({ applicationMode: 'deferred', state: 'pending' });

    expect(reviewB.acceptChange(proposal.id)).toBe(true);
    sync(bDoc, a.doc);
    expect(a.text.toString()).toBe('The method demonstrates consistent gains.');
    expect(reviewA.getSnapshot().changes.find((item) => item.id === proposal.id)?.state).toBe('accepted');
    reviewA.destroy();
    reviewB.destroy();
  });

  it('rejects a deferred AI proposal without changing collaborative source', () => {
    const initial = 'Results are very good.';
    const { doc, files, text } = project(initial);
    const review = new CollaborationReviewStore(doc, files, () => host);
    const from = initial.indexOf('very good');
    const proposal = review.proposeDeferredChange({
      filePath: 'main.tex',
      from,
      to: from + 'very good'.length,
      originalText: 'very good',
      proposedText: 'statistically significant',
      author: { id: 'interactive-ai-suggest', displayName: 'Learnitive AI' },
      source: 'ai',
    });
    expect(review.rejectChange(proposal.id)).toBe(true);
    expect(text.toString()).toBe(initial);
    expect(review.getSnapshot().changes.find((item) => item.id === proposal.id)?.state).toBe('rejected');
    review.destroy();
  });

  it('marks a deferred AI proposal conflicted instead of applying across newer edits', () => {
    const initial = 'The model improves accuracy.';
    const { doc, files, text } = project(initial);
    const review = new CollaborationReviewStore(doc, files, () => host);
    const from = initial.indexOf('improves');
    const proposal = review.proposeDeferredChange({
      filePath: 'main.tex',
      from,
      to: from + 'improves'.length,
      originalText: 'improves',
      proposedText: 'substantially improves',
      author: { id: 'interactive-ai-suggest', displayName: 'Learnitive AI' },
      source: 'ai',
    });

    text.delete(from, 'improves'.length);
    text.insert(from, 'changes');
    expect(review.acceptChange(proposal.id)).toBe(false);
    expect(text.toString()).toBe('The model changes accuracy.');
    expect(review.getSnapshot().changes.find((item) => item.id === proposal.id)?.state).toBe('conflicted');
    review.destroy();
  });

  it('coalesces contiguous Backspace deletions into one pending deletion', () => {
    const { doc, files, text } = project('Alpha beta');
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    const end = text.toString().indexOf(' beta');

    for (let index = 0; index < 'Alpha'.length; index += 1) {
      doc.transact(() => text.delete(end - index - 1, 1), 'interactive-collaboration-edit');
    }

    const pending = review.getSnapshot().changes.filter((item) => item.state === 'pending');
    expect(text.toString()).toBe(' beta');
    expect(pending).toHaveLength(1);
    expect(pending[0]).toMatchObject({ type: 'delete', originalText: 'Alpha', proposedText: '' });
    review.destroy();
  });

  it('does not create a second local change when applying a remote tracked edit', () => {
    const a = project('Base');
    const reviewA = new CollaborationReviewStore(a.doc, a.files, () => host);
    reviewA.setTrackChangesEnabled(true);

    const bDoc = new Y.Doc();
    sync(a.doc, bDoc);
    const bFiles = bDoc.getMap<Y.Text>('files');
    const reviewB = new CollaborationReviewStore(bDoc, bFiles, () => guest);
    reviewB.setTrackChangesEnabled(true);

    a.doc.transact(() => a.text.insert(a.text.length, ' host'), 'interactive-collaboration-edit');
    sync(a.doc, bDoc);

    const changes = reviewB.getSnapshot().changes;
    expect(changes).toHaveLength(1);
    expect(changes[0]?.author.id).toBe(host.id);
    expect(changes[0]?.proposedText).toBe(' host');
    reviewA.destroy();
    reviewB.destroy();
  });

  it('marks deletion rejection conflicted when nearby text makes the zero-width location ambiguous', () => {
    const { doc, files, text } = project('Alpha very beta');
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    const from = text.toString().indexOf('very ');
    doc.transact(() => text.delete(from, 'very '.length), 'interactive-collaboration-edit');
    const change = review.getSnapshot().changes[0]!;
    expect(change.type).toBe('delete');

    review.setTrackChangesEnabled(false);
    text.insert(from, 'NEW ');
    review.setTrackChangesEnabled(true);

    expect(review.rejectChange(change.id)).toBe(false);
    expect(review.getSnapshot().changes.find((item) => item.id === change.id)?.state).toBe('conflicted');
    expect(text.toString()).toContain('NEW beta');
    review.destroy();
  });

  it('removes explicitly associated pending review metadata on Undo and restores it on Redo', () => {
    const { doc, files, text } = project('Base');
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    const undo = new Y.UndoManager(text, { captureTimeout: 1750 });
    undo.trackedOrigins.add('interactive-collaboration-edit');

    doc.transact(() => text.insert(text.length, ' tracked'), 'interactive-collaboration-edit');
    expect(review.getSnapshot().changes).toHaveLength(1);
    const ids = review.consumeUndoChangeIds('main.tex');
    const snapshot = review.captureUndoRecords('main.tex', ids);
    expect(snapshot.records).toHaveLength(1);
    undo.undo();
    review.removeUndoRecords('main.tex', snapshot);
    expect(text.toString()).toBe('Base');
    expect(review.getSnapshot().changes).toHaveLength(0);

    undo.redo();
    review.restoreUndoRecords('main.tex', snapshot);
    expect(text.toString()).toBe('Base tracked');
    expect(review.getSnapshot().changes).toHaveLength(1);
    expect(review.getSnapshot().changes[0]?.state).toBe('pending');
    review.destroy();
    undo.destroy();
  });

  it('captures only the tracked change IDs explicitly associated with one undo unit', () => {
    const { doc, files, text } = project('Base');
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);

    doc.transact(() => text.insert(text.length, ' one'), 'interactive-collaboration-edit');
    const firstIds = review.consumeUndoChangeIds('main.tex');
    review.stopCoalescing('main.tex');
    doc.transact(() => text.insert(text.length, ' two'), 'interactive-collaboration-edit');
    const secondIds = review.consumeUndoChangeIds('main.tex');

    expect(firstIds).toHaveLength(1);
    expect(secondIds).toHaveLength(1);
    expect(secondIds[0]).not.toBe(firstIds[0]);
    expect(review.captureUndoRecords('main.tex', firstIds).records.map((record) => record.id)).toEqual(firstIds);
    expect(review.captureUndoRecords('main.tex', secondIds).records.map((record) => record.id)).toEqual(secondIds);
    review.destroy();
  });

  it('rejects review anchors whose encoded Y.Text does not match the declared filePath', () => {
    const { doc, files } = project('Main text');
    const refs = new Y.Text();
    refs.insert(0, 'Refs text');
    files.set('refs.bib', refs);
    const review = new CollaborationReviewStore(doc, files, () => host);
    const comment = review.createComment({ filePath: 'main.tex', from: 0, to: 4, body: 'Main anchor' });
    review.comments.get(comment.id)!.set('filePath', 'refs.bib');
    expect(review.getSnapshot().comments.find((item) => item.id === comment.id)).toBeUndefined();
    review.destroy();
  });

  it('preserves semantic kind and operation id from programmatic Yjs transaction metadata', () => {
    const { doc, files, text } = project('Base');
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    doc.transact((transaction) => {
      transaction.meta.set(COLLABORATION_REVIEW_TRANSACTION_META_KEY, { source: 'ai', semanticKind: 'equation-rewrite', operationId: 'operation-1' });
      text.insert(text.length, ' updated');
    }, 'interactive-collaboration-edit:ai');
    expect(review.getSnapshot().changes[0]).toMatchObject({ source: 'ai', semanticKind: 'equation-rewrite', operationId: 'operation-1' });
    review.destroy();
  });

  it('coalesces multiple shared-map updates from one logical review mutation into one subscriber refresh', async () => {
    const { doc, files, text } = project('Base');
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    let notifications = 0;
    const cleanup = review.subscribe(() => { notifications += 1; });
    doc.transact(() => text.insert(text.length, ' a'), 'interactive-collaboration-edit');
    doc.transact(() => text.insert(text.length, 'b'), 'interactive-collaboration-edit');
    await Promise.resolve();
    expect(notifications).toBe(1);
    cleanup();
    review.destroy();
  });

  it('keeps a deletion reject safe after a distant edit but conflicts after a nearby edit', () => {
    const { doc, files, text } = project('Alpha very beta and distant tail');
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    const from = text.toString().indexOf('very ');
    doc.transact(() => text.delete(from, 'very '.length), 'interactive-collaboration-edit');
    const first = review.getSnapshot().changes[0]!;
    review.setTrackChangesEnabled(false);
    text.insert(text.length, ' remote');
    review.setTrackChangesEnabled(true);
    expect(review.rejectChange(first.id)).toBe(true);
    expect(text.toString()).toContain('Alpha very beta');

    review.stopCoalescing('main.tex');
    const secondFrom = text.toString().indexOf('very ');
    doc.transact(() => text.delete(secondFrom, 'very '.length), 'interactive-collaboration-edit');
    const second = review.getSnapshot().changes.find((item) => item.state === 'pending')!;
    review.setTrackChangesEnabled(false);
    text.insert(secondFrom, 'near ');
    review.setTrackChangesEnabled(true);
    expect(review.rejectChange(second.id)).toBe(false);
    expect(review.getSnapshot().changes.find((item) => item.id === second.id)?.state).toBe('conflicted');
    review.destroy();
  });

  it('rejects oversized replies before they can inflate the shared review document', () => {
    const { doc, files } = project('Base');
    const review = new CollaborationReviewStore(doc, files, () => host);
    const comment = review.createComment({ filePath: 'main.tex', from: 0, to: 4, body: 'Review this.' });
    expect(review.reply(comment.id, 'x'.repeat(8_001))).toBe(false);
    expect(review.getSnapshot().comments[0]?.replies).toHaveLength(0);
    review.destroy();
  });

  it('keeps 500 tracked changes readable within the bounded review snapshot contract', () => {
    const { doc, files, text } = project('x'.repeat(1200));
    const review = new CollaborationReviewStore(doc, files, () => host);
    review.setTrackChangesEnabled(true);
    for (let index = 0; index < 500; index += 1) {
      review.stopCoalescing('main.tex');
      doc.transact(() => text.insert(index * 2, 'a'), 'interactive-collaboration-edit');
    }
    const started = performance.now();
    const snapshot = review.getSnapshot();
    const elapsed = performance.now() - started;
    expect(snapshot.changes).toHaveLength(500);
    expect(Number.isFinite(elapsed)).toBe(true);
    review.destroy();
  });

});
