/**
 * Unit tests for CRDT operations
 * Tests Yjs document merging and conflict resolution
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { Doc, Text } from 'yjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { createScreenplayDoc, getOrCreateSharedTypes } from './yjs-document';

describe('CRDT Operations', () => {
  describe('Document Creation', () => {
    it('should create a new Yjs document with proper structure', () => {
      const doc = createScreenplayDoc('project-1', {
        title: 'Test Screenplay',
        author: 'Test Author',
      });

      expect(doc).toBeDefined();
      
      const sharedTypes = getOrCreateSharedTypes(doc);
      expect(sharedTypes.text).toBeDefined();
      expect(sharedTypes.metadata).toBeDefined();
      expect(sharedTypes.characters).toBeDefined();
      expect(sharedTypes.scenes).toBeDefined();
    });

    it('should initialize metadata with default values', () => {
      const doc = createScreenplayDoc('project-1', {});
      const metadata = doc.getMap('metadata');

      expect(metadata.get('projectId')).toBe('project-1');
      expect(metadata.get('title')).toBe('Untitled Screenplay');
      expect(metadata.get('version')).toBe(1);
      expect(metadata.get('createdAt')).toBeDefined();
      expect(metadata.get('updatedAt')).toBeDefined();
    });
  });

  describe('Text Synchronization', () => {
    it('should sync text changes between two documents', () => {
      const doc1 = new Doc();
      const doc2 = new Doc();

      const text1 = doc1.getText('main');
      const text2 = doc2.getText('main');

      // Insert text in doc1
      text1.insert(0, 'Hello World');

      // Encode and apply update to doc2
      const update = encodeStateAsUpdate(doc1);
      applyUpdate(doc2, update);

      expect(text2.toString()).toBe('Hello World');
    });

    it('should handle concurrent insertions correctly', () => {
      const doc1 = new Doc();
      const doc2 = new Doc();

      const text1 = doc1.getText('main');
      const text2 = doc2.getText('main');

      // Both documents start with same content
      text1.insert(0, 'Hello');
      const update1 = encodeStateAsUpdate(doc1);
      applyUpdate(doc2, update1);

      // Concurrent inserts at different positions
      text1.insert(5, ' World'); // Doc1 inserts at position 5
      const update2 = encodeStateAsUpdate(doc2);
      applyUpdate(doc1, update2);

      // Both should have the same final content
      expect(text1.toString()).toBe('Hello World');
      expect(text2.toString()).toBe('Hello');
    });

    it('should handle concurrent deletions correctly', () => {
      const doc1 = new Doc();
      const doc2 = new Doc();

      const text1 = doc1.getText('main');
      const text2 = doc2.getText('main');

      // Both start with same content
      text1.insert(0, 'Hello World');
      const initialUpdate = encodeStateAsUpdate(doc1);
      applyUpdate(doc2, initialUpdate);

      // Concurrent deletions
      text1.delete(0, 5); // Delete 'Hello'
      const update1 = encodeStateAsUpdate(doc1);
      applyUpdate(doc2, update1);

      text2.delete(0, 6); // Delete 'Hello ' (including space)
      const update2 = encodeStateAsUpdate(doc2);
      applyUpdate(doc1, update2);

      // Both should converge to similar state
      expect(text1.toString()).toBe(text2.toString());
    });
  });

  describe('Undo/Redo', () => {
    it('should undo and redo text changes', async () => {
      const doc = new Doc();
      const text = doc.getText('main');
      
      const { UndoManager } = await import('yjs');
      const undoManager = new UndoManager([text], { captureTimeout: 0 });

      // First operation - insert 'Hello'
      text.insert(0, 'Hello');
      // Give UndoManager time to capture
      await new Promise(resolve => setTimeout(resolve, 10));

      // Second operation - insert ' World'
      text.insert(5, ' World');
      await new Promise(resolve => setTimeout(resolve, 10));

      expect(text.toString()).toBe('Hello World');

      // Undo the second operation
      undoManager.undo();
      expect(text.toString()).toBe('Hello');

      // Redo
      undoManager.redo();
      expect(text.toString()).toBe('Hello World');
    });
  });

  describe('Metadata Management', () => {
    it('should update metadata correctly', () => {
      const doc = createScreenplayDoc('project-1', {
        title: 'Original Title',
        author: 'Original Author',
      });

      const metadata = doc.getMap('metadata');
      
      // Update title
      metadata.set('title', 'Updated Title');
      
      expect(metadata.get('title')).toBe('Updated Title');
      expect(metadata.get('author')).toBe('Original Author');
      expect(metadata.get('projectId')).toBe('project-1');
    });

    it('should track version increments', () => {
      const doc = createScreenplayDoc('project-1', {});
      const metadata = doc.getMap('metadata');

      const initialVersion = metadata.get('version');
      
      // Simulate update
      metadata.set('updatedAt', new Date().toISOString());
      metadata.set('version', (metadata.get('version') || 0) + 1);

      expect(metadata.get('version')).toBeGreaterThan(initialVersion || 0);
    });
  });

  describe('Character References', () => {
    it('should store and retrieve character references', () => {
      const doc = createScreenplayDoc('project-1', {});
      const characters = doc.getMap('characters');

      const character = {
        id: 'char-1',
        name: 'John Doe',
        shortName: 'John',
        description: 'Protagonist',
      };

      characters.set('char-1', character);

      const retrieved = characters.get('char-1');
      expect(retrieved?.name).toBe('John Doe');
      expect(retrieved?.shortName).toBe('John');
    });
  });

  describe('Scene Metadata', () => {
    it('should store scene metadata', () => {
      const doc = createScreenplayDoc('project-1', {});
      const scenes = doc.getMap('scenes');

      const scene = {
        id: 'scene-1',
        slugline: 'INT. OFFICE - DAY',
        startTime: 0,
        duration: 150,
      };

      scenes.set('scene-1', scene);

      const retrieved = scenes.get('scene-1');
      expect(retrieved?.slugline).toBe('INT. OFFICE - DAY');
      expect(retrieved?.duration).toBe(150);
    });
  });
});
