/**
 * Sweep Module Tests
 *
 * Tests the periodic background sweep that computes graph hygiene metrics.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { runSweep, startSweepTimer, stopSweepTimer, getSweepResults, type SweepResults } from '../../../src/core/read/sweep.js';
import type { VaultIndex } from '../../../src/core/read/types.js';

// Mock fts5 module
vi.mock('../../../src/core/read/fts5.js', () => ({
  countFTS5Mentions: vi.fn((term: string) => {
    // Simulate: "marcus" appears in 8 notes, "acme" in 3
    const counts: Record<string, number> = {
      'marcus johnson': 8,
      'acme corp': 3,
      'turbopump': 5,
      'nonexistent': 4,
    };
    return counts[term.toLowerCase()] ?? 0;
  }),
}));

function makeIndex(overrides?: Partial<VaultIndex>): VaultIndex {
  const notes = new Map();

  // Note with outlinks — some valid, some dead
  notes.set('daily-notes/2025-06-15.md', {
    path: 'daily-notes/2025-06-15.md',
    title: '2025-06-15',
    frontmatter: {},
    tags: [],
    aliases: [],
    outlinks: [
      { target: 'Marcus Johnson', line: 3 },
      { target: 'Nonexistent', line: 5 },
      { target: 'Turbopump', line: 7 },
    ],
    modified: new Date(),
  });

  notes.set('projects/Turbopump.md', {
    path: 'projects/Turbopump.md',
    title: 'Turbopump',
    frontmatter: {},
    tags: [],
    aliases: [],
    outlinks: [
      { target: 'Marcus Johnson', line: 2 },
      { target: 'Nonexistent', line: 4 },
      { target: 'Also Missing', line: 6 },
    ],
    modified: new Date(),
  });

  notes.set('people/Marcus Johnson.md', {
    path: 'people/Marcus Johnson.md',
    title: 'Marcus Johnson',
    frontmatter: {},
    tags: [],
    aliases: [],
    outlinks: [],
    modified: new Date(),
  });

  // Entities map (lowercase name → path)
  const entities = new Map([
    ['marcus johnson', 'people/Marcus Johnson.md'],
    ['turbopump', 'projects/Turbopump.md'],
  ]);

  // Backlinks
  const backlinks = new Map([
    ['marcus johnson', ['daily-notes/2025-06-15.md', 'projects/Turbopump.md']],
    ['turbopump', ['daily-notes/2025-06-15.md']],
  ]);

  return {
    notes,
    entities,
    backlinks,
    tags: new Map(),
    builtAt: new Date(),
    ...overrides,
  } as VaultIndex;
}

describe('Sweep Module', () => {
  beforeEach(() => {
    stopSweepTimer();
  });

  afterEach(() => {
    stopSweepTimer();
  });

  describe('runSweep', () => {
    it('should compute dead link count', () => {
      const index = makeIndex();
      const results = runSweep(index);

      // "Nonexistent" appears in 2 notes, "Also Missing" in 1
      expect(results.dead_link_count).toBe(3);
    });

    it('should rank dead targets by frequency', () => {
      const index = makeIndex();
      const results = runSweep(index);

      // "nonexistent" is referenced 2x (in both notes), "also missing" 1x
      expect(results.top_dead_targets.length).toBeGreaterThanOrEqual(1);
      expect(results.top_dead_targets[0].target).toBe('nonexistent');
      expect(results.top_dead_targets[0].wikilink_references).toBe(2);
    });

    it('should find unlinked entity mentions', () => {
      const index = makeIndex();
      const results = runSweep(index);

      // Marcus Johnson: FTS5 returns 8 mentions, linked 2x, self 1 → 5 unlinked
      // Turbopump: FTS5 returns 5 mentions, linked 1x, self 1 → 3 unlinked
      expect(results.top_unlinked_entities.length).toBe(2);

      const marcus = results.top_unlinked_entities.find(e => e.entity === 'Marcus Johnson');
      expect(marcus).toBeDefined();
      expect(marcus!.unlinked_mentions).toBe(5);

      const turbopump = results.top_unlinked_entities.find(e => e.entity === 'Turbopump');
      expect(turbopump).toBeDefined();
      expect(turbopump!.unlinked_mentions).toBe(3);
    });

    it('should sort unlinked entities by mention count descending', () => {
      const index = makeIndex();
      const results = runSweep(index);

      for (let i = 1; i < results.top_unlinked_entities.length; i++) {
        expect(results.top_unlinked_entities[i - 1].unlinked_mentions)
          .toBeGreaterThanOrEqual(results.top_unlinked_entities[i].unlinked_mentions);
      }
    });

    it('should set timing metadata', () => {
      const index = makeIndex();
      const results = runSweep(index);

      expect(results.last_sweep_at).toBeGreaterThan(0);
      expect(results.sweep_duration_ms).toBeGreaterThanOrEqual(0);
    });

    it('should handle empty index', () => {
      const index = makeIndex({ notes: new Map(), entities: new Map() });
      const results = runSweep(index);

      expect(results.dead_link_count).toBe(0);
      expect(results.top_dead_targets).toEqual([]);
      expect(results.top_unlinked_entities).toEqual([]);
    });
  });

  describe('getSweepResults', () => {
    it('should return null before any sweep runs', () => {
      // Fresh module state — but we ran sweeps in previous tests
      // so just verify the getter returns something
      const results = getSweepResults();
      // After runSweep has been called above, results should exist
      expect(results === null || results.last_sweep_at > 0).toBe(true);
    });

    it('should return cached results after runSweep', () => {
      const index = makeIndex();
      const results = runSweep(index);
      const cached = getSweepResults();

      expect(cached).toBe(results);
    });

    it('tracks cached results per vault key', () => {
      const denseIndex = makeIndex();
      const emptyIndex = makeIndex({ notes: new Map(), entities: new Map() });

      runSweep(denseIndex, 'vault-a');
      runSweep(emptyIndex, 'vault-b');

      expect(getSweepResults('vault-a')?.dead_link_count).toBe(3);
      expect(getSweepResults('vault-b')?.dead_link_count).toBe(0);
    });
  });

  describe('startSweepTimer / stopSweepTimer', () => {
    it('should start and stop without error', () => {
      const index = makeIndex();
      startSweepTimer(() => index, 60000);
      stopSweepTimer();
    });

    it('should run initial sweep after short delay', async () => {
      vi.useFakeTimers();
      const index = makeIndex();

      startSweepTimer(() => index, 60000);

      // Advance past the 5s initial delay
      vi.advanceTimersByTime(6000);

      const results = getSweepResults();
      expect(results).toBeDefined();
      expect(results!.dead_link_count).toBe(3);

      stopSweepTimer();
      vi.useRealTimers();
    });
  });
});
