import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { decodeAnchor, encodeAnchor } from '../../lib/comment-anchor.js';
/**
 * Build a Yjs doc with a TipTap-style XmlFragment containing one
 * paragraph element whose text is the supplied string. Returns both the
 * doc and the `Y.XmlText` leaf so tests can anchor against the right
 * type (`createRelativePositionFromTypeIndex` indexes characters when
 * called against an `XmlText`/`Text`, and child slots when called
 * against the `XmlFragment` / element).
 */
function makeDoc(text) {
    const doc = new Y.Doc();
    const fragment = doc.getXmlFragment('default');
    const paragraph = new Y.XmlElement('paragraph');
    const yText = new Y.XmlText(text);
    paragraph.insert(0, [yText]);
    fragment.insert(0, [paragraph]);
    return { doc, yText };
}
describe('encodeAnchor', () => {
    it('produces non-empty base64 strings for a valid selection', () => {
        const { doc, yText } = makeDoc('Hello world');
        const anchor = encodeAnchor({ doc, yType: yText, from: 0, to: 5 });
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        expect(anchor.from.length).toBeGreaterThan(0);
        expect(anchor.to.length).toBeGreaterThan(0);
        expect(anchor.from).not.toBe(anchor.to);
    });
    it('returns null for an empty range', () => {
        const { doc, yText } = makeDoc('Hello world');
        expect(encodeAnchor({ doc, yType: yText, from: 3, to: 3 })).toBeNull();
    });
    it('returns null for an inverted range', () => {
        const { doc, yText } = makeDoc('Hello world');
        expect(encodeAnchor({ doc, yType: yText, from: 5, to: 1 })).toBeNull();
    });
    it('returns null when from < 0', () => {
        const { doc, yText } = makeDoc('Hello world');
        expect(encodeAnchor({ doc, yType: yText, from: -1, to: 5 })).toBeNull();
    });
    it('returns null when to > yType.length', () => {
        const { doc, yText } = makeDoc('Hello world');
        expect(encodeAnchor({ doc, yType: yText, from: 0, to: 999 })).toBeNull();
    });
    it('produces stable output for the same range called twice', () => {
        const { doc, yText } = makeDoc('Hello world');
        const a1 = encodeAnchor({ doc, yType: yText, from: 0, to: 5 });
        const a2 = encodeAnchor({ doc, yType: yText, from: 0, to: 5 });
        expect(a1).toEqual(a2);
    });
});
describe('decodeAnchor — round trip', () => {
    it('round-trips the original positions on an unchanged doc', () => {
        const { doc, yText } = makeDoc('Hello world');
        const anchor = encodeAnchor({ doc, yType: yText, from: 6, to: 11 });
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        const resolved = decodeAnchor({ doc, anchor });
        expect(resolved).not.toBeNull();
        if (!resolved)
            return;
        expect(resolved.from).toBe(6);
        expect(resolved.to).toBe(11);
        expect(resolved.yType).toBe(yText);
    });
    it('returns null when the anchor blobs are not valid base64', () => {
        const { doc } = makeDoc('Hello world');
        expect(decodeAnchor({
            doc,
            anchor: { from: 'not valid !@#$', to: 'also not !@#$' },
        })).toBeNull();
    });
});
describe('decodeAnchor — survives concurrent edits', () => {
    it('shifts the anchor right when text is inserted before it', () => {
        const { doc, yText } = makeDoc('Hello world');
        // Anchor "world" (chars 6..11).
        const anchor = encodeAnchor({ doc, yType: yText, from: 6, to: 11 });
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        // Prepend "Hey, " — five characters — to the text.
        yText.insert(0, 'Hey, ');
        const resolved = decodeAnchor({ doc, anchor });
        expect(resolved).not.toBeNull();
        if (!resolved)
            return;
        expect(yText.toString().slice(resolved.from, resolved.to)).toBe('world');
    });
    it('still resolves when text is appended after the anchor', () => {
        const { doc, yText } = makeDoc('Hello world');
        const anchor = encodeAnchor({ doc, yType: yText, from: 0, to: 5 });
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        yText.insert(yText.length, '!');
        const resolved = decodeAnchor({ doc, anchor });
        expect(resolved).not.toBeNull();
        if (!resolved)
            return;
        expect(yText.toString().slice(resolved.from, resolved.to)).toBe('Hello');
    });
    it('detaches (returns null) when the anchored text is deleted', () => {
        const { doc, yText } = makeDoc('Hello world');
        const anchor = encodeAnchor({ doc, yType: yText, from: 6, to: 11 });
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        // Delete "world" — five chars starting at index 6.
        yText.delete(6, 5);
        const resolved = decodeAnchor({ doc, anchor });
        // After deletion both ends collapse; we treat `from >= to` as detached
        // so the panel can flag the comment as "anchor lost".
        expect(resolved).toBeNull();
    });
    it('survives a sequence of inserts on both sides', () => {
        const { doc, yText } = makeDoc('alpha beta gamma');
        const anchor = encodeAnchor({ doc, yType: yText, from: 6, to: 10 }); // "beta"
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        yText.insert(0, '>> ');
        yText.insert(yText.length, ' <<');
        yText.insert(0, '!');
        const resolved = decodeAnchor({ doc, anchor });
        expect(resolved).not.toBeNull();
        if (!resolved)
            return;
        expect(yText.toString().slice(resolved.from, resolved.to)).toBe('beta');
    });
});
describe('decodeAnchor — independent docs', () => {
    it('survives transport between two clones (the multi-user scenario)', () => {
        const a = makeDoc('Hello world');
        const anchor = encodeAnchor({ doc: a.doc, yType: a.yText, from: 6, to: 11 });
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        // Sync a fresh clone (the other client's doc) from author A's state.
        const docB = new Y.Doc();
        Y.applyUpdate(docB, Y.encodeStateAsUpdate(a.doc));
        const resolved = decodeAnchor({ doc: docB, anchor });
        expect(resolved).not.toBeNull();
        if (!resolved)
            return;
        expect(resolved.from).toBe(6);
        expect(resolved.to).toBe(11);
    });
    it('still resolves after the clone makes its own edit', () => {
        const a = makeDoc('Hello world');
        const anchor = encodeAnchor({ doc: a.doc, yType: a.yText, from: 6, to: 11 });
        expect(anchor).not.toBeNull();
        if (!anchor)
            return;
        const docB = new Y.Doc();
        Y.applyUpdate(docB, Y.encodeStateAsUpdate(a.doc));
        // Look up B's mirror of the same text node and edit it.
        const fragmentB = docB.getXmlFragment('default');
        const paragraphB = fragmentB.firstChild;
        const yTextB = paragraphB.firstChild;
        yTextB.insert(0, 'Hey, ');
        const resolved = decodeAnchor({ doc: docB, anchor });
        expect(resolved).not.toBeNull();
        if (!resolved)
            return;
        expect(yTextB.toString().slice(resolved.from, resolved.to)).toBe('world');
    });
});
//# sourceMappingURL=comment-anchor.test.js.map