/**
 * Tests for `gbrain extract --source db` (v0.10.3 graph layer).
 *
 * Verifies the DB-source path of the unified `gbrain extract <subcommand>`
 * command. Companion to test/extract.test.ts which covers the fs-source path.
 *
 * Runs against in-memory PGLite. Idempotency, --type filtering, --dry-run
 * JSON output, and reconciliation correctness.
 */

import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { runExtract } from '../src/commands/extract.ts';
import type { PageInput } from '../src/core/types.ts';

let engine: PGLiteEngine;

beforeAll(async () => {
  engine = new PGLiteEngine();
  await engine.connect({});
  await engine.initSchema();
}, 60_000); // OAuth v25 + full migration chain needs breathing room

afterAll(async () => {
  if (engine) await engine.disconnect();
}, 60_000);

async function truncateAll() {
  for (const t of ['content_chunks', 'links', 'tags', 'raw_data', 'timeline_entries', 'page_versions', 'ingest_log', 'pages']) {
    await (engine as any).db.exec(`DELETE FROM ${t}`);
  }
}

const personPage = (title: string, body = ''): PageInput => ({
  type: 'person', title, compiled_truth: body, timeline: '',
});

const companyPage = (title: string, body = ''): PageInput => ({
  type: 'company', title, compiled_truth: body, timeline: '',
});

const meetingPage = (title: string, body = ''): PageInput => ({
  type: 'meeting', title, compiled_truth: body, timeline: '',
});

describe('gbrain extract links --source db', () => {
  beforeEach(truncateAll);

  test('extracts links from meeting page with attendee refs', async () => {
    await engine.putPage('people/alice', personPage('Alice'));
    await engine.putPage('people/bob', personPage('Bob'));
    await engine.putPage('meetings/standup', meetingPage(
      'Standup',
      'Attendees: [Alice](people/alice), [Bob](people/bob).',
    ));

    await runExtract(engine, ['links', '--source', 'db']);

    const links = await engine.getLinks('meetings/standup');
    expect(links.length).toBe(2);
    expect(new Set(links.map(l => l.to_slug))).toEqual(new Set(['people/alice', 'people/bob']));
    expect(links.every(l => l.link_type === 'attended')).toBe(true);
  });

  test('infers works_at type from CEO context', async () => {
    await engine.putPage('companies/acme', companyPage('Acme'));
    await engine.putPage('people/alice', personPage(
      'Alice',
      '[Alice](people/alice) is the CEO of [Acme](companies/acme).',
    ));

    await runExtract(engine, ['links', '--source', 'db']);
    const links = await engine.getLinks('people/alice');
    const acmeLink = links.find(l => l.to_slug === 'companies/acme');
    expect(acmeLink?.link_type).toBe('works_at');
  });

  test('idempotent: running twice produces same link count', async () => {
    await engine.putPage('people/alice', personPage('Alice'));
    await engine.putPage('companies/acme', companyPage('Acme', '[Alice](people/alice) advises us.'));

    await runExtract(engine, ['links', '--source', 'db']);
    const after1 = await engine.getLinks('companies/acme');

    await runExtract(engine, ['links', '--source', 'db']);
    const after2 = await engine.getLinks('companies/acme');
    expect(after2.length).toBe(after1.length);
  });

  test('skips refs to non-existent target pages', async () => {
    await engine.putPage('people/alice', personPage(
      'Alice',
      'Met [Phantom](people/phantom-ghost) at the event.',
    ));
    await runExtract(engine, ['links', '--source', 'db']);
    const links = await engine.getLinks('people/alice');
    expect(links.length).toBe(0);
  });

  test('--dry-run --json outputs JSON lines and writes nothing', async () => {
    await engine.putPage('people/alice', personPage('Alice'));
    await engine.putPage('companies/acme', companyPage(
      'Acme',
      '[Alice](people/alice) joined as CEO.',
    ));

    const lines: string[] = [];
    const originalWrite = process.stdout.write.bind(process.stdout);
    process.stdout.write = ((chunk: string | Uint8Array): boolean => {
      const str = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8');
      lines.push(str);
      return true;
    }) as any;

    try {
      await runExtract(engine, ['links', '--source', 'db', '--dry-run', '--json']);
    } finally {
      process.stdout.write = originalWrite;
    }

    const jsonLines = lines.filter(l => l.trim().startsWith('{'));
    expect(jsonLines.length).toBeGreaterThan(0);
    const parsed = JSON.parse(jsonLines[0].trim());
    expect(parsed.action).toBe('add_link');
    expect(parsed.from).toBeTruthy();
    expect(parsed.to).toBeTruthy();
    expect(parsed.type).toBeTruthy();

    const links = await engine.getLinks('companies/acme');
    expect(links.length).toBe(0);
  });

  test('--type filter only processes matching pages', async () => {
    await engine.putPage('people/alice', personPage('Alice'));
    await engine.putPage('people/bob', personPage('Bob', '[Alice](people/alice) is great.'));
    await engine.putPage('companies/acme', companyPage('Acme', '[Alice](people/alice) joined.'));

    await runExtract(engine, ['links', '--source', 'db', '--type', 'person']);

    const bobLinks = await engine.getLinks('people/bob');
    expect(bobLinks.length).toBe(1);
    const acmeLinks = await engine.getLinks('companies/acme');
    expect(acmeLinks.length).toBe(0);
  });
});

describe('gbrain extract timeline --source db', () => {
  beforeEach(truncateAll);

  test('extracts dated timeline entries from page content', async () => {
    await engine.putPage('people/alice', {
      type: 'person', title: 'Alice',
      compiled_truth: 'Alice is the CEO.',
      timeline: `## Timeline
- **2026-01-15** | Joined as CEO
- **2026-02-20** | Closed Series A`,
    });

    await runExtract(engine, ['timeline', '--source', 'db']);

    const entries = await engine.getTimeline('people/alice');
    expect(entries.length).toBe(2);
    expect(entries.map(e => e.summary).sort()).toEqual(['Closed Series A', 'Joined as CEO']);
  });

  test('idempotent via DB constraint', async () => {
    await engine.putPage('people/alice', {
      type: 'person', title: 'Alice', compiled_truth: '',
      timeline: '- **2026-01-15** | Same event',
    });
    await runExtract(engine, ['timeline', '--source', 'db']);
    await runExtract(engine, ['timeline', '--source', 'db']);
    const entries = await engine.getTimeline('people/alice');
    expect(entries.length).toBe(1);
  });

  test('skips invalid dates', async () => {
    await engine.putPage('people/alice', {
      type: 'person', title: 'Alice', compiled_truth: '',
      timeline: `- **2026-01-15** | Valid
- **2026-13-45** | Invalid month/day
- **2026-02-30** | Feb 30 doesnt exist`,
    });
    await runExtract(engine, ['timeline', '--source', 'db']);
    const entries = await engine.getTimeline('people/alice');
    expect(entries.length).toBe(1);
    expect(entries[0].summary).toBe('Valid');
  });

  test('handles multiple date format variants', async () => {
    await engine.putPage('people/alice', {
      type: 'person', title: 'Alice', compiled_truth: '',
      timeline: `- **2026-01-15** | Pipe variant
- **2026-02-20** -- Double dash variant
- **2026-03-10** - Single dash variant`,
    });
    await runExtract(engine, ['timeline', '--source', 'db']);
    const entries = await engine.getTimeline('people/alice');
    expect(entries.length).toBe(3);
  });

  test('--dry-run --json emits JSON, no DB writes', async () => {
    await engine.putPage('people/alice', {
      type: 'person', title: 'Alice', compiled_truth: '',
      timeline: '- **2026-01-15** | Test event',
    });

    const lines: string[] = [];
    const originalWrite = process.stdout.write.bind(process.stdout);
    process.stdout.write = ((chunk: string | Uint8Array): boolean => {
      const str = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8');
      lines.push(str);
      return true;
    }) as any;
    try {
      await runExtract(engine, ['timeline', '--source', 'db', '--dry-run', '--json']);
    } finally {
      process.stdout.write = originalWrite;
    }

    const jsonLines = lines.filter(l => l.trim().startsWith('{'));
    expect(jsonLines.length).toBeGreaterThan(0);
    const parsed = JSON.parse(jsonLines[0].trim());
    expect(parsed.action).toBe('add_timeline');
    expect(parsed.date).toBe('2026-01-15');
    expect(parsed.summary).toBe('Test event');

    const entries = await engine.getTimeline('people/alice');
    expect(entries.length).toBe(0);
  });
});

describe('gbrain extract all --source db', () => {
  beforeEach(truncateAll);

  test('runs both links and timeline in one command', async () => {
    await engine.putPage('people/alice', personPage('Alice'));
    await engine.putPage('companies/acme', {
      type: 'company', title: 'Acme',
      compiled_truth: '[Alice](people/alice) joined as CEO.',
      timeline: '- **2026-01-15** | Hired Alice',
    });

    await runExtract(engine, ['all', '--source', 'db']);

    const links = await engine.getLinks('companies/acme');
    expect(links.length).toBe(1);
    const entries = await engine.getTimeline('companies/acme');
    expect(entries.length).toBe(1);
  });
});
