/**
 * IRON RULE regression test (per D1 from /plan-eng-review for v0.42.0.0).
 *
 * Pins byte-identical output between:
 *   - `gbrain orphans --json` (CLI orchestrator `runOrphans`)
 *   - `findOrphans(engine, opts)` (canonical pure data fn)
 *   - `getOrphansData(engine, opts)` (v0.42.0.0 alias for findOrphans)
 *
 * If a future refactor lets the CLI filter results differently after
 * `findOrphans` returns, this test catches the drift. Doctor's
 * `orphan_ratio` check imports `getOrphansData`; this test guarantees
 * the doctor count cannot disagree with `gbrain orphans --count`.
 *
 * Hermetic via PGLite. No DATABASE_URL needed.
 */

import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import {
  findOrphans,
  getOrphansData,
  shouldExclude,
  runOrphans,
} from '../src/commands/orphans.ts';

let engine: PGLiteEngine;
let logBuffer: string[];
const originalLog = console.log;

function captureConsoleLog(): void {
  logBuffer = [];
  console.log = (msg?: unknown) => {
    logBuffer.push(typeof msg === 'string' ? msg : String(msg));
  };
}

function restoreConsoleLog(): void {
  console.log = originalLog;
}

beforeAll(async () => {
  engine = new PGLiteEngine();
  await engine.connect({});
  await engine.initSchema();
}, 60_000);

afterAll(async () => {
  await engine.disconnect();
  restoreConsoleLog();
});

beforeEach(async () => {
  // Clean slate per test — keeps the IRON RULE assertions deterministic
  // across the file's test suite.
  await engine.executeRaw('DELETE FROM links');
  await engine.executeRaw('DELETE FROM pages');
});

async function seedFixture(): Promise<void> {
  // 5 entity pages: 2 will have inbound links, 3 will be orphans.
  // 2 pseudo-pages: should be excluded by default filter.
  // 1 content page: links to person-1 + company-1.
  await engine.putPage('people/person-1', {
    type: 'person', title: 'Person 1', compiled_truth: 'p1', timeline: '', frontmatter: { domain: 'people' },
  });
  await engine.putPage('people/person-2', {
    type: 'person', title: 'Person 2', compiled_truth: 'p2', timeline: '', frontmatter: { domain: 'people' },
  });
  await engine.putPage('people/person-3', {
    type: 'person', title: 'Person 3', compiled_truth: 'p3', timeline: '', frontmatter: { domain: 'people' },
  });
  await engine.putPage('companies/company-1', {
    type: 'company', title: 'Company 1', compiled_truth: 'c1', timeline: '', frontmatter: { domain: 'companies' },
  });
  await engine.putPage('companies/company-2', {
    type: 'company', title: 'Company 2', compiled_truth: 'c2', timeline: '', frontmatter: { domain: 'companies' },
  });
  // Pseudo-pages — should be excluded from default orphan results.
  await engine.putPage('_atlas', {
    type: 'note', title: 'Atlas', compiled_truth: 'atlas', timeline: '', frontmatter: {},
  });
  await engine.putPage('templates/meeting', {
    type: 'note', title: 'Meeting template', compiled_truth: 'tmpl', timeline: '', frontmatter: {},
  });
  // Content page that links to person-1 + company-1.
  await engine.putPage('writing/post-1', {
    type: 'note', title: 'Post 1', compiled_truth: 'content', timeline: '', frontmatter: {},
  });
  await engine.addLinksBatch([
    { from_slug: 'writing/post-1', to_slug: 'people/person-1', link_type: 'mentions', link_source: 'markdown', context: '' },
    { from_slug: 'writing/post-1', to_slug: 'companies/company-1', link_type: 'mentions', link_source: 'markdown', context: '' },
  ]);
}

describe('orphans pure data fn — IRON RULE byte-identical contract', () => {
  test('getOrphansData is the same function reference as findOrphans', () => {
    expect(getOrphansData).toBe(findOrphans);
  });

  test('findOrphans and getOrphansData produce deep-equal output', async () => {
    await seedFixture();
    const viaFindOrphans = await findOrphans(engine, { includePseudo: false });
    const viaGetOrphansData = await getOrphansData(engine, { includePseudo: false });
    expect(viaGetOrphansData).toEqual(viaFindOrphans);
  });

  test('includePseudo: false vs true changes excluded count', async () => {
    await seedFixture();
    const def = await findOrphans(engine, { includePseudo: false });
    const all = await findOrphans(engine, { includePseudo: true });
    expect(all.excluded).toBe(0);
    expect(def.excluded).toBeGreaterThan(0);
    expect(all.total_orphans).toBeGreaterThanOrEqual(def.total_orphans);
  });

  test('CLI --json output deep-equals findOrphans return value', async () => {
    await seedFixture();
    const direct = await findOrphans(engine, { includePseudo: false });
    captureConsoleLog();
    try {
      await runOrphans(engine, ['--json']);
    } finally {
      restoreConsoleLog();
    }
    expect(logBuffer.length).toBe(1);
    const cliOutput = JSON.parse(logBuffer[0]!);
    // IRON RULE: CLI --json output must deep-equal the pure-fn output.
    // If a future change adds CLI-side post-filtering, this fires.
    expect(cliOutput).toEqual(direct);
  });

  test('CLI --count matches total_orphans from pure fn', async () => {
    await seedFixture();
    const direct = await findOrphans(engine, { includePseudo: false });
    captureConsoleLog();
    try {
      await runOrphans(engine, ['--count']);
    } finally {
      restoreConsoleLog();
    }
    expect(logBuffer.length).toBe(1);
    expect(logBuffer[0]).toBe(String(direct.total_orphans));
  });

  test('CLI --count with --include-pseudo matches pure-fn total_orphans (includePseudo: true)', async () => {
    await seedFixture();
    const direct = await findOrphans(engine, { includePseudo: true });
    captureConsoleLog();
    try {
      await runOrphans(engine, ['--count', '--include-pseudo']);
    } finally {
      restoreConsoleLog();
    }
    expect(logBuffer[0]).toBe(String(direct.total_orphans));
  });
});

describe('shouldExclude — orphan filter regression (preserve curation)', () => {
  test('pseudo-pages are excluded', () => {
    expect(shouldExclude('_atlas')).toBe(true);
    expect(shouldExclude('_index')).toBe(true);
    expect(shouldExclude('_orphans')).toBe(true);
  });

  test('auto-suffix patterns are excluded', () => {
    expect(shouldExclude('people/_index')).toBe(true);
    expect(shouldExclude('writing/log')).toBe(true);
  });

  test('raw segment is excluded', () => {
    expect(shouldExclude('media/x/raw/post')).toBe(true);
    expect(shouldExclude('raw/chats/claude-code/session')).toBe(true);
  });

  test('leading raw/ segment is excluded (same archive convention)', () => {
    expect(shouldExclude('raw/whatsapp/2025-01/chat-log')).toBe(true);
    expect(shouldExclude('raw/transcripts/meeting')).toBe(true);
    // 'rawhide/...' must NOT match — prefix is 'raw/', not 'raw'.
    expect(shouldExclude('rawhide/notes')).toBe(false);
  });

  test('daily-log pages are excluded (calendar/email integrations write these)', () => {
    expect(shouldExclude('daily/calendar/2025/2025-01-01')).toBe(true);
    expect(shouldExclude('daily/x/2025-06-13')).toBe(true);
  });

  test('outputs/ plural prefix is excluded like output/', () => {
    expect(shouldExclude('outputs/render-batch-3')).toBe(true);
  });

  test('readme folder descriptors are excluded at any depth', () => {
    expect(shouldExclude('readme')).toBe(true);
    expect(shouldExclude('index')).toBe(true);
    expect(shouldExclude('projects/readme')).toBe(true);
    expect(shouldExclude('media/readme')).toBe(true);
    // A page merely mentioning readme in its name is NOT excluded.
    expect(shouldExclude('concepts/readme-driven-development')).toBe(false);
  });

  test('machine-generated extracts pages are excluded', () => {
    expect(shouldExclude('extracts/2026-06-12/takes.proposed/host/propose-x/round-single')).toBe(true);
  });

  test('inbox intake-tray pages are excluded (same rationale as daily)', () => {
    expect(shouldExclude('inbox/some-renewal-notice-2026-06-22')).toBe(true);
  });

  test('root schema and log pages are excluded', () => {
    expect(shouldExclude('schema')).toBe(true);
    expect(shouldExclude('log')).toBe(true);
    expect(shouldExclude('concepts/schema-design')).toBe(false);
  });

  test('deny-prefixes are excluded', () => {
    expect(shouldExclude('templates/meeting')).toBe(true);
    expect(shouldExclude('dashboards/_index')).toBe(true);
    expect(shouldExclude('scripts/build')).toBe(true);
    expect(shouldExclude('output/foo')).toBe(true);
    // #2264 — auto_chronicle event volume (life/events/…) is a machine leaf.
    expect(shouldExclude('life/events/2026-08-01-abc123')).toBe(true);
  });

  test('first-segment exclusions fire', () => {
    expect(shouldExclude('scratch/notes')).toBe(true);
    expect(shouldExclude('thoughts/today')).toBe(true);
    expect(shouldExclude('catalog/movies')).toBe(true);
    expect(shouldExclude('entities/anonymous')).toBe(true);
    expect(shouldExclude('atoms/fact-123')).toBe(true);
    expect(shouldExclude('skills/gbrain-operations')).toBe(true);
    expect(shouldExclude('dreaming/light/2026-07-20')).toBe(true);
    expect(shouldExclude('daily/2026-07-20')).toBe(true);
    expect(shouldExclude('agent-openclaw/daily/2026-07-20')).toBe(true);
  });

  test('workspace convention slugs are excluded', () => {
    expect(shouldExclude('_brain-conventions')).toBe(true);
    expect(shouldExclude('_templates/decision')).toBe(true);
    expect(shouldExclude('extracts/2026-06-30/takes.proposed/round-single')).toBe(true);
    expect(shouldExclude('2026-07-20')).toBe(true);
    expect(shouldExclude('2026-07-20-qa-sweep')).toBe(true);
    expect(shouldExclude('agents/arya/identity')).toBe(true);
    expect(shouldExclude('agents/arya/memory/dreaming/deep/2026-07-20')).toBe(true);
  });

  test('regular slugs are NOT excluded', () => {
    expect(shouldExclude('people/alice')).toBe(false);
    expect(shouldExclude('companies/acme')).toBe(false);
    expect(shouldExclude('writing/post-1')).toBe(false);
    expect(shouldExclude('agents/arya/qa-reports/launch-review')).toBe(false);
    // #2264 — knowledge classes must STAY in the denominator so real graph decay still trips.
    expect(shouldExclude('concepts/information-architecture')).toBe(false);
    expect(shouldExclude('notes/some-note')).toBe(false);
    expect(shouldExclude('projects/proj-x')).toBe(false);
    // #2264 — only life/events/ is excluded; human-authored life/diary/ stays counted.
    expect(shouldExclude('life/diary/2026-08-01-xyz')).toBe(false);
  });
});

describe('getHealth orphan_pages uses shared exclusion policy', () => {
  test('excluded convention islands do not count against health', async () => {
    await engine.putPage('_templates/decision', {
      type: 'template', title: 'Decision', compiled_truth: 'template', timeline: '', frontmatter: {},
    });
    await engine.putPage('skills/arya/source-check', {
      type: 'concept', title: 'Skill', compiled_truth: 'skill', timeline: '', frontmatter: {},
    });
    await engine.putPage('agents/arya/identity', {
      type: 'note', title: 'Identity', compiled_truth: 'identity', timeline: '', frontmatter: {},
    });
    await engine.putPage('people/alice', {
      type: 'person', title: 'Alice', compiled_truth: 'real island', timeline: '', frontmatter: {},
    });

    const health = await engine.getHealth();

    expect(health.orphan_pages).toBe(1);
  });

  test('per-brain config overrides (orphans.exclude_*) also apply to health', async () => {
    await engine.putPage('my-private-folder/secret-ref', {
      type: 'note', title: 'Ref', compiled_truth: 'ref', timeline: '', frontmatter: {},
    });
    await engine.putPage('one-off-fixture-page', {
      type: 'note', title: 'Fixture', compiled_truth: 'fixture', timeline: '', frontmatter: {},
    });
    await engine.putPage('people/alice', {
      type: 'person', title: 'Alice', compiled_truth: 'real island', timeline: '', frontmatter: {},
    });

    expect((await engine.getHealth()).orphan_pages).toBe(3);

    await engine.setConfig('orphans.exclude_prefixes', 'my-private-folder/');
    await engine.setConfig('orphans.exclude_slugs', 'one-off-fixture-page');
    expect((await engine.getHealth()).orphan_pages).toBe(1);

    await engine.unsetConfig('orphans.exclude_prefixes');
    await engine.unsetConfig('orphans.exclude_slugs');
  });
});
