/**
 * Tests for vault_growth tool (growth metrics)
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
  createTempVault,
  cleanupTempVault,
  openStateDb,
  deleteStateDb,
  type StateDb,
} from '../../helpers/testUtils.js';
import {
  computeMetrics,
  recordMetrics,
  getMetricHistory,
  computeTrends,
  purgeOldMetrics,
  ALL_METRICS,
} from '../../../src/core/shared/metrics.js';
import { recordFeedback, updateSuppressionList } from '../../../src/core/write/wikilinkFeedback.js';
import type { VaultIndex, VaultNote, Backlink } from '../../../src/core/read/types.js';

// =============================================================================
// HELPERS
// =============================================================================

function makeNote(notePath: string, outlinks: Array<{ target: string }> = [], tags: string[] = []): VaultNote {
  return {
    path: notePath,
    title: notePath.replace(/\.md$/, '').split('/').pop() || notePath,
    aliases: [],
    frontmatter: {},
    outlinks: outlinks.map(ol => ({ target: ol.target, line: 1 })),
    tags,
    modified: new Date(),
  };
}

function buildIndex(notes: VaultNote[], backlinks?: Map<string, Backlink[]>): VaultIndex {
  const noteMap = new Map<string, VaultNote>();
  const tags = new Map<string, Set<string>>();
  const entities = new Map<string, string>();

  for (const note of notes) {
    noteMap.set(note.path, note);
    entities.set(note.title.toLowerCase(), note.path);
    for (const tag of note.tags) {
      if (!tags.has(tag)) tags.set(tag, new Set());
      tags.get(tag)!.add(note.path);
    }
  }

  return {
    notes: noteMap,
    backlinks: backlinks || new Map(),
    entities,
    tags,
    builtAt: new Date(),
  };
}

// =============================================================================
// TESTS
// =============================================================================

describe('vault_growth', () => {
  let vaultPath: string;
  let stateDb: StateDb;

  beforeEach(async () => {
    vaultPath = await createTempVault();
    stateDb = openStateDb(vaultPath);
  });

  afterEach(async () => {
    stateDb.db.close();
    deleteStateDb(vaultPath);
    await cleanupTempVault(vaultPath);
  });

  // --------------------------------------------------------
  // Compute metrics from index
  // --------------------------------------------------------
  describe('computeMetrics', () => {
    it('should compute metrics from empty vault', () => {
      const index = buildIndex([]);
      const metrics = computeMetrics(index);

      expect(metrics.note_count).toBe(0);
      expect(metrics.link_count).toBe(0);
      expect(metrics.orphan_count).toBe(0);
      expect(metrics.tag_count).toBe(0);
      expect(metrics.entity_count).toBe(0);
      expect(metrics.avg_links_per_note).toBe(0);
      expect(metrics.link_density).toBe(0);
      expect(metrics.connected_ratio).toBe(0);
    });

    it('should compute metrics from populated vault', () => {
      const notes = [
        makeNote('note-a.md', [{ target: 'note-b' }], ['tag1', 'tag2']),
        makeNote('note-b.md', [{ target: 'note-a' }, { target: 'note-c' }], ['tag1']),
        makeNote('note-c.md', [], ['tag3']),
        makeNote('orphan.md', [], []),
      ];

      const backlinks = new Map<string, Backlink[]>();
      backlinks.set('note-b', [{ source: 'note-a.md', line: 1 }]);
      backlinks.set('note-a', [{ source: 'note-b.md', line: 1 }]);
      backlinks.set('note-c', [{ source: 'note-b.md', line: 1 }]);

      const index = buildIndex(notes, backlinks);
      const metrics = computeMetrics(index);

      expect(metrics.note_count).toBe(4);
      expect(metrics.link_count).toBe(3);
      expect(metrics.tag_count).toBe(3);
      expect(metrics.entity_count).toBe(4);
      expect(metrics.avg_links_per_note).toBe(0.75);
      expect(metrics.connected_ratio).toBeGreaterThan(0);
    });

    it('should not count nested backlink targets as orphans', () => {
      // Nested note with backlinks should be connected, not an orphan
      const notes = [
        makeNote('daily/2026-01-01.md', [{ target: 'Flywheel' }]),
        makeNote('tech/flywheel/Flywheel.md', []),  // no outlinks, but has backlinks
      ];

      const backlinks = new Map<string, Backlink[]>();
      // Key is normalizeNotePath(targetPath) = "tech/flywheel/flywheel"
      backlinks.set('tech/flywheel/flywheel', [{ source: 'daily/2026-01-01.md', line: 1 }]);

      const index = buildIndex(notes, backlinks);
      // buildIndex maps title "Flywheel" (lowercased) -> path, and
      // normalizeNotePath -> path. The backlink key should resolve.
      // Also add the path-based entity entry
      index.entities.set('tech/flywheel/flywheel', 'tech/flywheel/Flywheel.md');

      const metrics = computeMetrics(index);

      // daily note has outlinks -> connected
      // Flywheel has backlinks -> connected (NOT an orphan)
      expect(metrics.orphan_count).toBe(0);
    });

    it('should still count truly disconnected notes as orphans', () => {
      const notes = [
        makeNote('connected.md', [{ target: 'other' }]),
        makeNote('orphan.md', []),
      ];

      const index = buildIndex(notes);
      const metrics = computeMetrics(index);

      expect(metrics.orphan_count).toBe(1);
    });
  });

  // --------------------------------------------------------
  // Record + retrieve snapshots
  // --------------------------------------------------------
  describe('recordMetrics / getMetricHistory', () => {
    it('should record and retrieve metric snapshots', () => {
      const metrics = { note_count: 100, link_count: 50 };
      recordMetrics(stateDb, metrics);

      const history = getMetricHistory(stateDb, 'note_count', 30);
      expect(history.length).toBe(1);
      expect(history[0].metric).toBe('note_count');
      expect(history[0].value).toBe(100);
    });

    it('should retrieve all metrics when no filter', () => {
      const metrics = { note_count: 100, link_count: 50, orphan_count: 5 };
      recordMetrics(stateDb, metrics);

      const history = getMetricHistory(stateDb, undefined, 30);
      expect(history.length).toBe(3);
    });

    it('should filter by days_back', () => {
      // Insert a metric with old timestamp
      stateDb.db.prepare(
        'INSERT INTO vault_metrics (timestamp, metric, value) VALUES (?, ?, ?)'
      ).run(Date.now() - 60 * 24 * 60 * 60 * 1000, 'note_count', 50);

      // Insert a recent metric
      recordMetrics(stateDb, { note_count: 100 });

      const history = getMetricHistory(stateDb, 'note_count', 30);
      expect(history.length).toBe(1);
      expect(history[0].value).toBe(100);
    });
  });

  // --------------------------------------------------------
  // Trend computation
  // --------------------------------------------------------
  describe('computeTrends', () => {
    it('should compute trends vs previous values', () => {
      // Record old values
      const oldTimestamp = Date.now() - 25 * 24 * 60 * 60 * 1000;
      stateDb.db.prepare(
        'INSERT INTO vault_metrics (timestamp, metric, value) VALUES (?, ?, ?)'
      ).run(oldTimestamp, 'note_count', 80);
      stateDb.db.prepare(
        'INSERT INTO vault_metrics (timestamp, metric, value) VALUES (?, ?, ?)'
      ).run(oldTimestamp, 'link_count', 40);

      const current = { note_count: 100, link_count: 60, orphan_count: 5 };
      const trends = computeTrends(stateDb, current, 30);

      const noteTrend = trends.find(t => t.metric === 'note_count');
      expect(noteTrend).toBeDefined();
      expect(noteTrend!.current).toBe(100);
      expect(noteTrend!.previous).toBe(80);
      expect(noteTrend!.delta).toBe(20);
      expect(noteTrend!.direction).toBe('up');
    });

    it('should show stable when no change', () => {
      const oldTimestamp = Date.now() - 25 * 24 * 60 * 60 * 1000;
      stateDb.db.prepare(
        'INSERT INTO vault_metrics (timestamp, metric, value) VALUES (?, ?, ?)'
      ).run(oldTimestamp, 'note_count', 100);

      const trends = computeTrends(stateDb, { note_count: 100 }, 30);
      const noteTrend = trends.find(t => t.metric === 'note_count');
      expect(noteTrend!.direction).toBe('stable');
      expect(noteTrend!.delta).toBe(0);
    });
  });

  // --------------------------------------------------------
  // Purge old metrics
  // --------------------------------------------------------
  describe('purgeOldMetrics', () => {
    it('should purge metrics older than retention period', () => {
      // Insert old metric (100 days ago)
      stateDb.db.prepare(
        'INSERT INTO vault_metrics (timestamp, metric, value) VALUES (?, ?, ?)'
      ).run(Date.now() - 100 * 24 * 60 * 60 * 1000, 'note_count', 50);

      // Insert recent metric
      recordMetrics(stateDb, { note_count: 100 });

      const purged = purgeOldMetrics(stateDb, 90);
      expect(purged).toBe(1);

      const remaining = getMetricHistory(stateDb, 'note_count', 365);
      expect(remaining.length).toBe(1);
      expect(remaining[0].value).toBe(100);
    });
  });

  // --------------------------------------------------------
  // Wikilink quality metrics
  // --------------------------------------------------------
  describe('wikilink quality metrics', () => {
    it('should return zero wikilink metrics without StateDb', () => {
      const index = buildIndex([makeNote('note.md')]);
      const metrics = computeMetrics(index);

      expect(metrics.wikilink_accuracy).toBe(0);
      expect(metrics.wikilink_feedback_volume).toBe(0);
      expect(metrics.wikilink_suppressed_count).toBe(0);
    });

    it('should compute accuracy from mixed feedback entries', () => {
      const index = buildIndex([makeNote('note.md')]);

      // 8 correct, 2 incorrect for "React"
      for (let i = 0; i < 8; i++) {
        recordFeedback(stateDb, 'React', `context ${i}`, `note${i}.md`, true);
      }
      for (let i = 0; i < 2; i++) {
        recordFeedback(stateDb, 'React', `context ${i}`, `note${i + 8}.md`, false);
      }

      // 3 correct, 7 incorrect for "Java"
      for (let i = 0; i < 3; i++) {
        recordFeedback(stateDb, 'Java', `context ${i}`, `note${i}.md`, true);
      }
      for (let i = 0; i < 7; i++) {
        recordFeedback(stateDb, 'Java', `context ${i}`, `note${i + 3}.md`, false);
      }

      const metrics = computeMetrics(index, stateDb);

      // Total: 11 correct / 20 entries = 0.55
      expect(metrics.wikilink_accuracy).toBe(0.55);
      expect(metrics.wikilink_feedback_volume).toBe(20);
    });

    it('should count suppressed entities', () => {
      const index = buildIndex([makeNote('note.md')]);

      // Create a suppressed entity (25 negatives)
      // Posterior: Beta(4, 26) = 4/30 = 0.133 < 0.45, totalObs=30 >= 15
      for (let i = 0; i < 25; i++) {
        recordFeedback(stateDb, 'Go', `fp ${i}`, `note${i}.md`, false);
      }
      updateSuppressionList(stateDb);

      const metrics = computeMetrics(index, stateDb);
      expect(metrics.wikilink_suppressed_count).toBe(1);
    });
  });
});
