/**
 * Tests for flywheel_doctor report=health (formerly health_check) and report=diagnosis.
 */

import { describe, test, expect, beforeAll, afterAll } from 'vitest';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { connectTestClient, type TestClient, createTestServer, type TestServerContext } from '../helpers/createTestServer.js';
import { recordIndexEvent } from '../../../src/core/shared/indexActivity.js';
import { setEmbeddingsDatabase, setEmbeddingsBuildState } from '../../../src/core/read/embeddings.js';
import { setWriteState } from '@velvetmonkey/vault-core';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURES_PATH = path.join(__dirname, '..', 'fixtures');

describe('flywheel_doctor report=health (formerly health_check)', () => {
  let context: TestServerContext;
  let client: TestClient;

  beforeAll(async () => {
    context = await createTestServer(FIXTURES_PATH);
    client = connectTestClient(context.server);
  });

  afterAll(() => {
    if (context.stateDb) {
      context.stateDb.close();
    }
    const flywheelDir = path.join(FIXTURES_PATH, '.flywheel');
    try {
      if (fs.existsSync(flywheelDir)) {
        fs.rmSync(flywheelDir, { recursive: true });
      }
    } catch { /* cleanup best-effort */ }
  });

  test('returns healthy status on valid vault', async () => {
    const result = await client.callTool('doctor', { action: 'health' });
    expect(result.content).toBeDefined();
    const data = JSON.parse(result.content[0].text);
    expect(data.status).toBeDefined();
    expect(['healthy', 'degraded']).toContain(data.status);
    expect(data.recommendations).toBeDefined();
    expect(Array.isArray(data.recommendations)).toBe(true);
  });

  test('returns structured output with expected fields', async () => {
    const result = await client.callTool('doctor', { action: 'health' });
    const data = JSON.parse(result.content[0].text);
    expect(data).toHaveProperty('status');
    expect(data).toHaveProperty('recommendations');
    expect(data).toHaveProperty('note_count');
    expect(data).toHaveProperty('entity_count');
  });

  test('does not throw ReferenceError on database integrity check', async () => {
    // This test verifies the fix for the `overall` variable bug.
    // The handler should never throw a ReferenceError —
    // even if the database integrity check fails, it should return
    // a structured response with status 'unhealthy'.
    //
    // We can't easily mock the SQLite pragma in this test setup,
    // but we verify the handler completes without throwing.
    const result = await client.callTool('doctor', { action: 'health' });
    expect(result.content).toBeDefined();
    expect(result.isError).toBeFalsy();

    const data = JSON.parse(result.content[0].text);
    // Status should be a valid enum value, not undefined
    expect(['healthy', 'degraded', 'unhealthy']).toContain(data.status);
  });

  test('database integrity section exists in response', async () => {
    const result = await client.callTool('doctor', { action: 'health' });
    const data = JSON.parse(result.content[0].text);
    // The database check ran without error (dbIntegrityFailed = false)
    // so status should NOT be 'unhealthy' on a valid test vault
    expect(data.status).not.toBe('unhealthy');
  });

  test('summary health does not run quick_check inline', async () => {
    expect(context.stateDb).toBeTruthy();
    const stateDb = context.stateDb!;
    const originalPragma = stateDb.db.pragma.bind(stateDb.db);
    let quickCheckCalls = 0;

    (stateDb.db as any).pragma = ((sql: string, ...args: unknown[]) => {
      if (typeof sql === 'string' && sql.includes('quick_check')) {
        quickCheckCalls++;
        throw new Error('quick_check should not run in summary health');
      }
      return originalPragma(sql as any, ...(args as any[]));
    }) as typeof stateDb.db.pragma;

    context.runtimeState.integrityState = 'checking';
    context.runtimeState.integrityCheckInProgress = true;

    try {
      const result = await client.callTool('doctor', { action: 'health', detail: 'summary' });
      const data = JSON.parse(result.content[0].text);
      expect(result.isError).toBeFalsy();
      expect(quickCheckCalls).toBe(0);
      expect(data.integrity_state).toBe('checking');
      expect(data.integrity_check_in_progress).toBe(true);
    } finally {
      (stateDb.db as any).pragma = originalPragma;
      context.runtimeState.integrityState = 'healthy';
      context.runtimeState.integrityCheckInProgress = false;
    }
  });

  test('summary detail omits full-scan dead-link fields', async () => {
    const result = await client.callTool('doctor', { action: 'health', detail: 'summary' });
    const data = JSON.parse(result.content[0].text);

    expect(data).not.toHaveProperty('dead_link_count');
    expect(data).not.toHaveProperty('top_dead_link_targets');
  });

  test('full detail includes dead-link fields', async () => {
    const result = await client.callTool('doctor', { action: 'health', detail: 'full' });
    const data = JSON.parse(result.content[0].text);

    expect(data).toHaveProperty('dead_link_count');
    expect(data).toHaveProperty('top_dead_link_targets');
    expect(Array.isArray(data.top_dead_link_targets)).toBe(true);
  });

  test('pipeline_status exposes boot and integrity state', async () => {
    context.runtimeState.bootState = 'booting';
    context.runtimeState.integrityState = 'checking';
    context.runtimeState.integrityCheckInProgress = true;

    try {
      const result = await client.callTool('doctor', { action: 'pipeline' });
      const data = JSON.parse(result.content[0].text);
      expect(data.boot_state).toBe('booting');
      expect(data.integrity_state).toBe('checking');
      expect(data.integrity_check_in_progress).toBe(true);
    } finally {
      context.runtimeState.bootState = 'ready';
      context.runtimeState.integrityState = 'healthy';
      context.runtimeState.integrityCheckInProgress = false;
    }
  });

  test('counts recent watcher batches as freshness (incremental pipeline keeps index live)', async () => {
    // Contract: index_age is min-age across {full rebuild, watcher batch, build}.
    // The watcher's 25-step pipeline (fts5_incremental, embeddings, hub scores,
    // etc.) IS a fresh-index signal — treating only full rebuilds as freshness
    // caused doctor to recommend `refresh_index` 5 min after every manual
    // refresh on actively-watched vaults, even though the index was current.
    expect(context.stateDb).toBeTruthy();
    const stateDb = context.stateDb!;
    const now = Date.now();
    const rebuildAgoSeconds = 800;
    const watcherAgoSeconds = 30;

    stateDb.db.prepare('DELETE FROM index_events').run();

    recordIndexEvent(stateDb, {
      trigger: 'startup_build',
      duration_ms: 1200,
      success: true,
      note_count: context.vaultIndex.notes.size,
    });
    stateDb.db.prepare('UPDATE index_events SET timestamp = ? WHERE trigger = ?')
      .run(now - rebuildAgoSeconds * 1000, 'startup_build');

    recordIndexEvent(stateDb, {
      trigger: 'watcher',
      duration_ms: 80,
      success: true,
      files_changed: 1,
      changed_paths: ['daily/2026-01-01.md'],
    });
    stateDb.db.prepare('UPDATE index_events SET timestamp = ? WHERE trigger = ?')
      .run(now - watcherAgoSeconds * 1000, 'watcher');

    const result = await client.callTool('doctor', { action: 'health' });
    const data = JSON.parse(result.content[0].text);

    // index_age tracks the watcher batch (newer than the rebuild).
    expect(data.index_age_seconds).toBeGreaterThanOrEqual(watcherAgoSeconds - 5);
    expect(data.index_age_seconds).toBeLessThanOrEqual(watcherAgoSeconds + 5);
    // last_rebuild still reports the full rebuild — we still surface that
    // separately so users can see when the last full rebuild ran.
    expect(data.last_rebuild.trigger).toBe('startup_build');
    expect(data.last_rebuild.ago_seconds).toBeGreaterThanOrEqual(rebuildAgoSeconds - 5);
    expect(data.last_rebuild.ago_seconds).toBeLessThanOrEqual(rebuildAgoSeconds + 5);
    // last_watcher_batch_at lines up with watcherAgoSeconds.
    expect(data.last_watcher_batch_at).toBeGreaterThanOrEqual(now - (watcherAgoSeconds + 5) * 1000);
    expect(data.last_watcher_batch_at).toBeLessThanOrEqual(now - (watcherAgoSeconds - 5) * 1000);
    // With watcherAgoSeconds=30s under the staleness threshold, index is NOT stale.
    expect(data.index_stale).toBe(false);
  });
});

describe('flywheel_doctor', () => {
  let context: TestServerContext;
  let client: TestClient;

  beforeAll(async () => {
    context = await createTestServer(FIXTURES_PATH);
    client = connectTestClient(context.server);
    if (!context.stateDb) {
      throw new Error('stateDb is null — openStateDb failed during test setup');
    }
    // Inject embeddings DB handle so getEntityEmbeddingsCount() can query
    setEmbeddingsDatabase(context.stateDb.db);
    // Mark embeddings as built so the doctor check includes entity_embedding_coverage
    setEmbeddingsBuildState('complete');
  });

  afterAll(() => {
    setEmbeddingsBuildState('none');
    setEmbeddingsDatabase(null as any);
    if (context.stateDb) {
      context.stateDb.close();
    }
    const flywheelDir = path.join(FIXTURES_PATH, '.flywheel');
    try {
      if (fs.existsSync(flywheelDir)) {
        fs.rmSync(flywheelDir, { recursive: true });
      }
    } catch { /* cleanup best-effort */ }
  });

  test('entity_embedding_coverage denominator uses canonical entity count from DB', async () => {
    const stateDb = context.stateDb!;

    // Seed some canonical entities in the DB so the check has a nonzero denominator.
    // The fixture vault's entities table may be empty since the pipeline hasn't run.
    const insertEntity = stateDb.db.prepare(
      `INSERT OR IGNORE INTO entities (name, name_lower, path, category) VALUES (?, ?, ?, ?)`
    );
    insertEntity.run('Alice', 'alice', 'people/Alice.md', 'person');
    insertEntity.run('Bob', 'bob', 'people/Bob.md', 'person');
    insertEntity.run('Acme', 'acme', 'orgs/Acme.md', 'organization');

    // Get the canonical entity count directly from the DB
    const dbEntityCount = (stateDb.db.prepare('SELECT COUNT(*) as cnt FROM entities').get() as { cnt: number }).cnt;
    expect(dbEntityCount).toBeGreaterThan(0);

    // Get the linkable surface count from the in-memory index
    const indexEntitySize = context.getIndex().entities.size;

    const result = await client.callTool('doctor', { action: 'diagnosis' });
    const data = JSON.parse(result.content[0].text);
    const check = data.checks.find((c: any) => c.name === 'entity_embedding_coverage');

    // The check should exist (we set embeddings state to 'complete' and seeded entities)
    expect(check).toBeDefined();

    // Parse the denominator from the detail string (format: "N/M canonical entities embedded (P%)")
    const match = check.detail.match(/(\d+)\/(\d+)/);
    expect(match).toBeTruthy();
    const denominator = parseInt(match![2], 10);

    // Denominator must match canonical entity count from DB
    expect(denominator).toBe(dbEntityCount);

    // Denominator must NOT match the linkable target surface (which includes aliases + paths)
    // This only holds when there are aliases/paths expanding the surface beyond canonical count
    if (indexEntitySize !== dbEntityCount) {
      expect(denominator).not.toBe(indexEntitySize);
    }
  });

  test('proactive_queue stall check fires when pending>0 and total_applied=0', async () => {
    const stateDb = context.stateDb!;

    stateDb.db.prepare(`DELETE FROM proactive_queue`).run();
    stateDb.db.prepare(`DELETE FROM wikilink_applications WHERE source = 'proactive'`).run();

    const now = Date.now();
    stateDb.db.prepare(
      `INSERT INTO proactive_queue (note_path, entity, score, confidence, queued_at, expires_at, status)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
    ).run('notes/stall-a.md', 'EntA', 25, 'high', now - 3600_000, now + 86400_000, 'pending');
    stateDb.db.prepare(
      `INSERT INTO proactive_queue (note_path, entity, score, confidence, queued_at, expires_at, status)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
    ).run('notes/stall-b.md', 'EntB', 25, 'high', now - 3600_000, now + 86400_000, 'pending');

    setWriteState(stateDb, 'last_proactive_drain', {
      at: now,
      total_applied: 0,
      applied_files: 0,
      expired: 0,
      skipped_active: 0,
      skipped_mtime: 0,
      skipped_daily_cap: 0,
      rejection_count: 5,
      rejection_sample: [],
      rejection_breakdown: { active_edit: 3, apply_empty: 2 },
    });

    try {
      const result = await client.callTool('doctor', { action: 'diagnosis' });
      const data = JSON.parse(result.content[0].text);
      const check = data.checks.find((c: any) => c.name === 'proactive_queue');
      expect(check).toBeDefined();
      expect(check.status).toBe('warning');
      expect(check.detail).toMatch(/2 pending, 0 applied/);
      expect(check.detail).toMatch(/active_edit \(3\)/);
      expect(check.detail).toMatch(/apply_empty \(2\)/);
      expect(check.fix).toMatch(/proactive_min_score|pipeline_status|pipeline/);
    } finally {
      stateDb.db.prepare(`DELETE FROM proactive_queue`).run();
      setWriteState(stateDb, 'last_proactive_drain', null);
    }
  });

  test('proactive_queue check stays ok when queue is drained cleanly', async () => {
    const stateDb = context.stateDb!;
    stateDb.db.prepare(`DELETE FROM proactive_queue`).run();
    setWriteState(stateDb, 'last_proactive_drain', null);

    const result = await client.callTool('doctor', { action: 'diagnosis' });
    const data = JSON.parse(result.content[0].text);
    const check = data.checks.find((c: any) => c.name === 'proactive_queue');
    expect(check).toBeDefined();
    expect(check.status).toBe('ok');
  });

  test('health report surfaces rejection_breakdown on last_drain', async () => {
    const stateDb = context.stateDb!;
    const now = Date.now();

    setWriteState(stateDb, 'last_proactive_drain', {
      at: now,
      total_applied: 4,
      applied_files: 2,
      expired: 0,
      skipped_active: 1,
      skipped_mtime: 0,
      skipped_daily_cap: 0,
      rejection_count: 6,
      rejection_sample: [],
      rejection_breakdown: { active_edit: 4, apply_empty: 2 },
    });

    try {
      const result = await client.callTool('doctor', { action: 'health', detail: 'full' });
      const data = JSON.parse(result.content[0].text);
      expect(data.proactive_linking?.last_drain).toBeDefined();
      expect(data.proactive_linking.last_drain.rejection_breakdown).toEqual({
        active_edit: 4,
        apply_empty: 2,
      });
    } finally {
      setWriteState(stateDb, 'last_proactive_drain', null);
    }
  });
});
