/**
 * Unit tests for Change Tracker and Merge Logic
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { Doc } from 'yjs';
import { ChangeTracker } from './change-tracker';
import { MergeLogic, ServerChange } from './merge-logic';

describe('ChangeTracker', () => {
  let doc: Doc;
  let tracker: ChangeTracker;

  beforeEach(() => {
    doc = new Doc();
    tracker = new ChangeTracker(doc, 'user-1', 'Test User');
  });

  describe('Change Recording', () => {
    it('should record manual changes', () => {
      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 5,
        content: 'Hello',
      });

      const changes = tracker.getAllChanges();
      expect(changes).toHaveLength(1);
      const firstChange = changes[0]!;
      expect(firstChange.type).toBe('insert');
      expect(firstChange.userId).toBe('user-1');
      expect(firstChange.userName).toBe('Test User');
      expect(firstChange.accepted).toBe(true);
    });

    it('should track change statistics', () => {
      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 5,
      });

      const stats = tracker.getStats();
      expect(stats.totalChanges).toBe(1);
      expect(stats.totalSnapshots).toBe(0);
      expect(stats.lastChangeAt).toBeDefined();
    });
  });

  describe('Snapshot Management', () => {
    it('should create snapshots', () => {
      const text = doc.getText('main');
      text.insert(0, 'Initial content');

      const snapshot = tracker.createSnapshot('Initial state');
      
      expect(snapshot.id).toBeDefined();
      expect(snapshot.description).toBe('Initial state');
      expect(snapshot.state).toBeDefined();
      expect(snapshot.state.length).toBeGreaterThan(0);
    });

    it('should restore snapshots', () => {
      const text = doc.getText('main');
      text.insert(0, 'Initial');
      
      const snapshot = tracker.createSnapshot('Before edit');
      
      // Modify document
      text.insert(7, ' Content');
      expect(text.toString()).toBe('Initial Content');

      // Restore snapshot
      tracker.restoreSnapshot(snapshot);
      
      // Document should be restored (note: Yjs snapshot restore applies the state)
      const restoredText = doc.getText('main').toString();
      expect(restoredText).toBeDefined();
    });

    it('should store multiple snapshots', () => {
      tracker.createSnapshot('Snapshot 1');
      tracker.createSnapshot('Snapshot 2');
      tracker.createSnapshot('Snapshot 3');

      const snapshots = tracker.getSnapshots();
      expect(snapshots).toHaveLength(3);
      expect(snapshots[0]!.description).toBe('Snapshot 1');
      expect(snapshots[2]!.description).toBe('Snapshot 3');
    });
  });

  describe('Change Acceptance/Rejection', () => {
    it('should accept changes', () => {
      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 5,
      });

      const changes = tracker.getAllChanges();
      tracker.acceptChange(changes[0]!.id);

      expect(changes[0]!.accepted).toBe(true);
    });

    it('should reject changes', () => {
      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 5,
      });

      const changes = tracker.getAllChanges();
      tracker.rejectChange(changes[0]!.id);

      expect(changes[0]!.accepted).toBe(false);
    });
  });

  describe('Change Diff', () => {
    it('should generate diff between snapshots', () => {
      const snapshot1 = tracker.createSnapshot('Before');
      
      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 10,
      });
      tracker.recordChange({
        type: 'delete',
        position: 5,
        length: 3,
      });

      const snapshot2 = tracker.createSnapshot('After');

      const diff = tracker.generateDiff(snapshot1, snapshot2);
      
      expect(diff.additions).toBeGreaterThanOrEqual(0);
      expect(diff.deletions).toBeGreaterThanOrEqual(0);
      expect(diff.changes).toBeDefined();
    });
  });

  describe('Change Listeners', () => {
    it('should notify listeners of changes', () => {
      let notifiedChange: any = null;
      
      tracker.onChange((change) => {
        notifiedChange = change;
      });

      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 5,
      });

      expect(notifiedChange).toBeDefined();
      expect(notifiedChange.type).toBe('insert');
    });

    it('should remove listeners', () => {
      let callCount = 0;
      const listener = () => callCount++;
      
      tracker.onChange(listener);
      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 5,
      });

      expect(callCount).toBe(1);

      tracker.removeChangeListener(listener);
      tracker.recordChange({
        type: 'insert',
        position: 0,
        length: 5,
      });

      expect(callCount).toBe(1); // Should not increase
    });
  });
});

describe('MergeLogic', () => {
  let doc: Doc;
  let mergeLogic: MergeLogic;

  beforeEach(() => {
    doc = new Doc();
    mergeLogic = new MergeLogic(doc, 'user-1');
  });

  describe('Server Change Application', () => {
    it('should apply server changes without conflicts', () => {
      const change: ServerChange = {
        id: 'change-1',
        userId: 'user-2',
        timestamp: new Date(),
        type: 'insert',
        position: 0,
        content: 'Hello',
        length: 5,
      };

      const result = mergeLogic.applyServerChange(change);
      
      expect(result.success).toBe(true);
      expect(result.conflicts).toHaveLength(0);
    });

    it('should detect concurrent edits', () => {
      // Initialize document
      const text = doc.getText('main');
      text.insert(0, 'Initial content');

      const change: ServerChange = {
        id: 'change-1',
        userId: 'user-2',
        timestamp: new Date(),
        type: 'insert',
        position: 0,
        content: 'Prefix',
        length: 6,
      };

      const result = mergeLogic.applyServerChange(change);
      
      // May or may not have conflicts depending on implementation
      expect(result).toBeDefined();
    });
  });

  describe('Conflict Resolution', () => {
    it('should auto-resolve non-overlapping edits', () => {
      const conflict = {
        id: 'conflict-1',
        type: 'concurrent-edit' as const,
        localChange: {
          id: 'local-1',
          userId: 'user-1',
          userName: 'Local User',
          timestamp: new Date(),
          type: 'insert' as const,
          position: 0,
          length: 100,
          accepted: true,
        },
        remoteChange: {
          id: 'remote-1',
          userId: 'user-2',
          userName: 'Remote User',
          timestamp: new Date(),
          type: 'insert' as const,
          position: 500,
          length: 50,
          accepted: true,
        },
      };

      const strategy = mergeLogic.handleConcurrentEdit(
        conflict.localChange,
        conflict.remoteChange
      );

      // Should auto-merge edits that are far apart
      expect(strategy).toBe('auto-merge');
    });

    it('should validate merge results', () => {
      const text = doc.getText('main');
      text.insert(0, 'Initial content');

      const result = {
        success: true,
        strategy: 'accept-remote' as const,
        conflicts: [],
        appliedChanges: [],
      };

      const isValid = mergeLogic.validateMerge(result);
      expect(isValid).toBe(true);
    });
  });

  describe('Screenplay-Specific Rules', () => {
    it('should handle same-scene conflicts', () => {
      const localChange = {
        id: 'local-1',
        userId: 'user-1',
        userName: 'Local User',
        timestamp: new Date(),
        type: 'insert' as const,
        position: 100,
        length: 50,
        accepted: true,
      };

      const remoteChange = {
        id: 'remote-1',
        userId: 'user-2',
        userName: 'Remote User',
        timestamp: new Date(),
        type: 'insert' as const,
        position: 120,
        length: 30,
        accepted: true,
      };

      const strategy = mergeLogic.handleConcurrentEdit(localChange, remoteChange);
      
      // Same scene, same type - should need manual resolution
      expect(strategy).toBe('manual');
    });

    it('should handle different-scene edits', () => {
      const localChange = {
        id: 'local-1',
        userId: 'user-1',
        userName: 'Local User',
        timestamp: new Date(),
        type: 'insert' as const,
        position: 100,
        length: 50,
        accepted: true,
      };

      const remoteChange = {
        id: 'remote-1',
        userId: 'user-2',
        userName: 'Remote User',
        timestamp: new Date(),
        type: 'insert' as const,
        position: 1000,
        length: 30,
        accepted: true,
      };

      const strategy = mergeLogic.handleConcurrentEdit(localChange, remoteChange);
      
      // Different scenes - should auto-merge
      expect(strategy).toBe('auto-merge');
    });
  });

  describe('Pending Conflicts', () => {
    it('should track pending conflicts', () => {
      const conflicts = mergeLogic.getPendingConflicts();
      expect(conflicts).toBeDefined();
      expect(Array.isArray(conflicts)).toBe(true);
    });
  });
});
