import { describe, it, expect, beforeEach } from 'vitest';
import { ConflictIndex } from '../conflict-index.js';

describe('ConflictIndex', () => {
  let idx: ConflictIndex;

  beforeEach(() => {
    idx = new ConflictIndex();
  });

  describe('registerChange', () => {
    it('adds an entry for a file path', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
      const entries = idx.getEntriesFor('src/a.ts');
      expect(entries.size).toBe(1);
      const entry = [...entries][0]!;
      expect(entry.worktreeId).toBe('wt-1');
      expect(entry.agent).toBe('agent-a');
      expect(entry.lineRange).toEqual({ start: 1, end: 10 });
      expect(entry.status).toBe('pending');
      expect(entry.timestamp).toBeGreaterThan(0);
    });

    it('supports multiple entries for the same file path', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
      idx.registerChange('src/a.ts', 'wt-2', 'agent-b', { start: 20, end: 30 });
      expect(idx.getEntriesFor('src/a.ts').size).toBe(2);
    });

    it('allows a worktree to have multiple entries (several edits to same file)', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 20, end: 30 });
      // Duplicate entries with same fields — the Set dedupes by ref,
      // so a second identical call is still a distinct object (allowed).
      expect(idx.getEntriesFor('src/a.ts').size).toBe(2);
    });

    it('separates files into distinct keys', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      idx.registerChange('src/b.ts', 'wt-2', 'agent-b');
      expect(idx.getEntriesFor('src/a.ts').size).toBe(1);
      expect(idx.getEntriesFor('src/b.ts').size).toBe(1);
    });
  });

  describe('removeWorktree', () => {
    it('removes all entries for a given worktree', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      idx.registerChange('src/a.ts', 'wt-2', 'agent-b');
      idx.registerChange('src/b.ts', 'wt-1', 'agent-a');
      idx.removeWorktree('wt-1');
      expect(idx.getEntriesFor('src/a.ts').size).toBe(1);
      expect([...idx.getEntriesFor('src/a.ts')][0]!.worktreeId).toBe('wt-2');
      expect(idx.getEntriesFor('src/b.ts').size).toBe(0);
    });

    it('is a no-op when worktree has no entries', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      idx.removeWorktree('wt-ghost');
      expect(idx.getEntriesFor('src/a.ts').size).toBe(1);
    });

    it('cleans up file key when last entry is removed', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      idx.removeWorktree('wt-1');
      // After removal the key should be gone
      expect(idx.snapshot().has('src/a.ts')).toBe(false);
    });
  });

  describe('sweepStale', () => {
    it('removes entries older than maxAgeMs', async () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      idx.registerChange('src/b.ts', 'wt-2', 'agent-b');
      // Wait a tick so timestamps diverge
      await new Promise((r) => setTimeout(r, 10));
      idx.registerChange('src/c.ts', 'wt-3', 'agent-c');
      const removed = idx.sweepStale(5); // 5ms cutoff — entries from before the await are stale
      expect(removed).toBeGreaterThanOrEqual(1);
    });

    it('removes file key when all entries swept', async () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      // Wait so timestamp is definitely older than cutoff
      await new Promise((r) => setTimeout(r, 10));
      const removed = idx.sweepStale(5);
      expect(removed).toBe(1);
      expect(idx.snapshot().has('src/a.ts')).toBe(false);
    });

    it('returns 0 when no entries are stale', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      const removed = idx.sweepStale(86_400_000); // 24h
      expect(removed).toBe(0);
    });
  });

  describe('getConflictsFor', () => {
    it('returns conflicts between worktrees', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
      idx.registerChange('src/a.ts', 'wt-2', 'agent-b', { start: 5, end: 15 });
      const conflicts = idx.getConflictsFor('src/a.ts');
      expect(conflicts).toHaveLength(1);
      expect(conflicts[0]!.filePath).toBe('src/a.ts');
      // getConflictsFor doesn't know the caller's line range,
      // so severity defaults to 'different_area'
      expect(conflicts[0]!.severity).toBe('different_area');
    });

    it('returns empty for files with only one worktree', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      expect(idx.getConflictsFor('src/a.ts')).toEqual([]);
    });

    it('returns empty for files not in index', () => {
      expect(idx.getConflictsFor('src/never-touched.ts')).toEqual([]);
    });
  });

  describe('query', () => {
    it('delegates to findConflicts with proper data', () => {
      idx.registerChange('src/a.ts', 'wt-2', 'agent-b', { start: 5, end: 15 });
      const ranges = new Map([['src/a.ts', { start: 10, end: 20 }]]);
      const result = idx.query(['src/a.ts'], 'wt-1', ranges);
      expect(result).toHaveLength(1);
      expect(result[0]!.severity).toBe('same_line');
    });

    it('returns empty when no conflicts', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
      const result = idx.query(['src/a.ts'], 'wt-1', new Map());
      expect(result).toEqual([]);
    });
  });

  describe('snapshot', () => {
    it('returns a copy of the internal map', () => {
      idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
      const snap = idx.snapshot();
      expect(snap.has('src/a.ts')).toBe(true);
      // Mutating the snapshot doesn't affect the original
      idx.removeWorktree('wt-1');
      expect(snap.has('src/a.ts')).toBe(true);
    });
  });
});
