import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
  formatDoctorReport,
  runKtxDoctor,
  runSetupDoctorChecks,
  type DoctorCheck,
} from '../src/doctor.js';

function makeIo() {
  let stdout = '';
  let stderr = '';
  return {
    io: {
      stdout: {
        write: (chunk: string) => {
          stdout += chunk;
        },
      },
      stderr: {
        write: (chunk: string) => {
          stderr += chunk;
        },
      },
    },
    stdout: () => stdout,
    stderr: () => stderr,
  };
}

function fakeDoctorHistoricSqlRunner() {
  return {
    dialect: 'postgres' as const,
    catalogName: 'pg_stat_statements',
    async run() {
      return { warnings: [], info: [] };
    },
    formatSuccessDetail(result: unknown) {
      const typed = result as { pgServerVersion?: string; warnings: string[]; info?: string[] };
      const info = typed.info && typed.info.length > 0 ? `; ${typed.info.join('; ')}` : '';
      return {
        detail: `pg_stat_statements ready (${typed.pgServerVersion ?? 'PostgreSQL 16.4'})${info}`,
        warnings: typed.warnings,
      };
    },
    fixAdvice(error: unknown) {
      return {
        failHeadline: error instanceof Error ? error.message : String(error),
        remediation: 'Fix query-history grants.',
      };
    },
  };
}

describe('formatDoctorReport', () => {
  it('shows the failing check and its fix in plain output', () => {
    const checks: DoctorCheck[] = [
      { id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127', group: 'toolchain' },
      {
        id: 'native-sqlite',
        label: 'Native SQLite',
        status: 'fail',
        detail: 'Cannot load better-sqlite3',
        fix: 'Run: pnpm run native:rebuild',
        group: 'toolchain',
      },
    ];

    const output = formatDoctorReport({ title: 'KTX status', checks });
    expect(output).toContain('KTX status');
    expect(output).toContain('✗ Environment');
    expect(output).toContain('1 of 2 need attention');
    expect(output).toContain('✗ Native SQLite: Cannot load better-sqlite3');
    expect(output).toContain('→ Run: pnpm run native:rebuild');
    expect(output).toContain('1 issue to fix.');
  });

  it('lists what was checked when a group has all passing checks', () => {
    const checks: DoctorCheck[] = [
      { id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0', group: 'toolchain' },
      { id: 'pnpm', label: 'pnpm 10.20+', status: 'pass', detail: '10.28.0', group: 'toolchain' },
    ];

    const output = formatDoctorReport({ title: 'KTX status', checks });
    expect(output).toContain('✓ Environment');
    expect(output).toContain('Node 22+ · pnpm 10.20+');
    expect(output).not.toContain('v22.16.0');
    expect(output).toContain('Everything ready.');
    expect(output).toContain('ktx status --json');
    expect(output).toContain('ktx sl');
    expect(output).toContain('ktx wiki');
    expect(output).not.toContain('ktx scan');
    expect(output).not.toContain('ktx sl ask');
  });

  it('shows the underlying detail for a single-check group on the group line', () => {
    const checks: DoctorCheck[] = [
      {
        id: 'semantic-search-embeddings',
        label: 'Semantic search embeddings',
        status: 'pass',
        detail: 'openai/text-embedding-3-small (1536d) probe succeeded',
        group: 'search',
      },
    ];

    const output = formatDoctorReport({ title: 'KTX status', checks });
    expect(output).toContain('✓ Semantic search');
    expect(output).toContain('openai/text-embedding-3-small (1536d) probe succeeded');
  });

  it('lists every check in verbose mode', () => {
    const checks: DoctorCheck[] = [
      { id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0', group: 'toolchain' },
    ];

    const output = formatDoctorReport({ title: 'KTX status', checks }, { verbose: true });
    expect(output).toContain('✓ Node 22+: v22.16.0');
  });
});

describe('runSetupDoctorChecks', () => {
  it('returns pass checks when injected commands and file checks succeed', async () => {
    const checks = await runSetupDoctorChecks({
      env: { PATH: '/bin' },
      workspaceRoot: '/workspace/ktx',
      execText: async (command, args) => {
        if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
        if (command === 'corepack' && args[0] === '--version') return '0.32.0';
        if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
        if (command === process.execPath && args.includes('--version')) return '@kaelio/ktx 0.0.0-private';
        throw new Error(`${command} ${args.join(' ')}`);
      },
      pathExists: async () => true,
      importBetterSqlite3: async () => ({ default: function Database() {} }),
    });

    expect(checks.map((check) => [check.id, check.status])).toEqual([
      ['node', 'pass'],
      ['pnpm', 'pass'],
      ['corepack', 'pass'],
      ['uv', 'pass'],
      ['native-sqlite', 'pass'],
      ['package-build', 'pass'],
      ['workspace-cli', 'pass'],
    ]);
  });

  it('returns exact fixes when setup checks fail', async () => {
    const checks = await runSetupDoctorChecks({
      env: {},
      workspaceRoot: '/workspace/ktx',
      execText: async (command) => {
        throw new Error(`${command} not found`);
      },
      pathExists: async () => false,
      importBetterSqlite3: async () => {
        throw new Error('Cannot find module better-sqlite3');
      },
    });

    expect(checks).toContainEqual({
      id: 'pnpm',
      label: 'pnpm 10.20+',
      status: 'fail',
      detail: 'pnpm not found',
      fix: 'Run: corepack enable && corepack prepare pnpm@10.28.0 --activate',
      group: 'toolchain',
    });
    expect(checks).toContainEqual({
      id: 'package-build',
      label: 'TypeScript package build',
      status: 'fail',
      detail: 'Missing packages/cli/dist/bin.js',
      fix: 'Run: pnpm run build',
      group: 'toolchain',
    });
  });

  it('treats missing corepack as a warning so setup doctor can still pass', async () => {
    const checks = await runSetupDoctorChecks({
      env: { PATH: '/bin' },
      workspaceRoot: '/workspace/ktx',
      execText: async (command, args) => {
        if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
        if (command === 'corepack' && args[0] === '--version') throw new Error('spawn corepack ENOENT');
        if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
        if (command === process.execPath && args.includes('--version')) return '@kaelio/ktx 0.0.0-private';
        throw new Error(`${command} ${args.join(' ')}`);
      },
      pathExists: async () => true,
      importBetterSqlite3: async () => ({ default: function Database() {} }),
    });
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'setup', outputMode: 'plain', inputMode: 'disabled', verbose: true },
        testIo.io,
        { runSetupChecks: async () => checks },
      ),
    ).resolves.toBe(0);

    expect(checks).toContainEqual({
      id: 'corepack',
      label: 'Corepack',
      status: 'warn',
      detail: 'spawn corepack ENOENT',
      fix: 'Run: corepack enable',
      group: 'toolchain',
    });
    expect(testIo.stdout()).toContain('⚠ Corepack: spawn corepack ENOENT');
    expect(testIo.stderr()).toBe('');
  });
});

describe('runKtxDoctor', () => {
  let tempDir: string;

  beforeEach(async () => {
    tempDir = await mkdtemp(join(tmpdir(), 'ktx-doctor-'));
  });

  afterEach(async () => {
    await rm(tempDir, { recursive: true, force: true });
  });

  it('prints setup report and exits nonzero when a check fails', async () => {
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'setup', outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {
          runSetupChecks: async () => [
            { id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127' },
            {
              id: 'package-build',
              label: 'TypeScript package build',
              status: 'fail',
              detail: 'Missing packages/cli/dist/bin.js',
              fix: 'Run: pnpm run build',
            },
          ],
        },
      ),
    ).resolves.toBe(1);

    expect(testIo.stdout()).toContain('KTX status');
    expect(testIo.stdout()).toContain('No project here yet.');
    expect(testIo.stdout()).toContain('Before you can run');
    expect(testIo.stdout()).toContain('✗ TypeScript package build: Missing packages/cli/dist/bin.js');
    expect(testIo.stdout()).toContain('→ Run: pnpm run build');
    expect(testIo.stderr()).toBe('');
  });

  it('leads with `ktx setup` and hides toolchain warnings when no project exists', async () => {
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'setup', outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {
          runSetupChecks: async () => [
            { id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0', group: 'toolchain' },
            {
              id: 'corepack',
              label: 'Corepack',
              status: 'warn',
              detail: 'spawn corepack ENOENT',
              fix: 'Run: corepack enable',
              group: 'toolchain',
            },
          ],
        },
      ),
    ).resolves.toBe(0);

    const out = testIo.stdout();
    expect(out).toContain('No project here yet.');
    expect(out).toContain('Run');
    expect(out).toContain('ktx setup');
    expect(out).not.toContain('Corepack');
    expect(out).not.toContain('Node 22+');
  });

  it('prints JSON setup report', async () => {
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'setup', outputMode: 'json', inputMode: 'disabled' },
        testIo.io,
        {
          runSetupChecks: async () => [
            { id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127' },
          ],
        },
      ),
    ).resolves.toBe(0);

    expect(JSON.parse(testIo.stdout())).toEqual({
      title: 'KTX status',
      checks: [{ id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127' }],
    });
  });

  it('prints a friendly message when ktx.yaml is missing at the project dir', async () => {
    const originalEnvProjectDir = process.env.KTX_PROJECT_DIR;
    process.env.KTX_PROJECT_DIR = tempDir;
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(1);

    const out = testIo.stdout();
    expect(out).toContain('KTX status');
    expect(out).toContain('No KTX project here yet.');
    expect(out).toContain('ktx setup');
    expect(out).toContain('KTX_PROJECT_DIR');
    expect(out).not.toContain('ENOENT');
    expect(testIo.stderr()).toBe('');

    if (originalEnvProjectDir === undefined) {
      delete process.env.KTX_PROJECT_DIR;
    } else {
      process.env.KTX_PROJECT_DIR = originalEnvProjectDir;
    }
  });

  it('emits a structured JSON error when ktx.yaml is missing and JSON output is requested', async () => {
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(1);

    const parsed = JSON.parse(testIo.stdout()) as { error: string; projectDir: string };
    expect(parsed.error).toBe('missing_project');
    expect(parsed.projectDir).toBe(tempDir);
  });

  it('prints schema issues and exits 1 when ktx.yaml fails Zod validation', async () => {
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'storrage:',
        '  state: sqlite',
        'ingest:',
        '  llm:',
        '    backend: anthropic',
        '',
      ].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(1);

    const out = testIo.stdout();
    expect(out).toContain('KTX status');
    expect(out).toContain('Config');
    expect(out).toContain('Unsupported storrage: unknown field');
    expect(out).toContain('Unsupported ingest.llm: unknown field');
    expect(out).toContain('ktx.yaml');
  });

  it('emits structured JSON when ktx.yaml fails Zod validation', async () => {
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      ['storrage: {}', ''].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(1);

    const parsed = JSON.parse(testIo.stdout()) as {
      error: string;
      projectDir: string;
      issues: Array<{ path: string; message: string }>;
    };
    expect(parsed.error).toBe('invalid_config');
    expect(parsed.projectDir).toBe(tempDir);
    expect(parsed.issues.some((issue) => issue.path === 'storrage')).toBe(true);
  });

  it('shows a Config row labelled "ktx.yaml schema valid" on the happy path', async () => {
    process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'connections:',
        '  warehouse:',
        '    driver: sqlite',
        '    path: ./warehouse.db',
        'llm:',
        '  provider:',
        '    backend: anthropic',
        '',
      ].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(0);

    expect(testIo.stdout()).toContain('ktx.yaml schema valid');
    delete process.env.ANTHROPIC_API_KEY;
  });

  it('runs project checks against a valid ktx.yaml', async () => {
    process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'connections:',
        '  warehouse:',
        '    driver: sqlite',
        '    path: ./warehouse.db',
        'llm:',
        '  provider:',
        '    backend: anthropic',
        '  models:',
        '    default: claude-sonnet-4-5',
        'ingest:',
        '  adapters:',
        '    - live-database',
        '  embeddings:',
        '    backend: openai',
        '    model: text-embedding-3-small',
        '    dimensions: 1536',
        '',
      ].join('\n'),
      'utf-8',
    );
    process.env.OPENAI_API_KEY = 'test-key'; // pragma: allowlist secret
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(0);

    const out = testIo.stdout();
    expect(out).toContain('KTX status');
    expect(out).toContain(`· ${basename(tempDir)}`);
    expect(out).toContain('Connections (1)');
    expect(out).toContain('LLM');
    expect(out).toContain('anthropic');
    expect(out).toContain('Embeddings');
    expect(out).toContain('Ready.');
    delete process.env.ANTHROPIC_API_KEY;
    delete process.env.OPENAI_API_KEY;
  });

  it('reports Claude Code auth failures and ignored prompt-caching fields in project doctor output', async () => {
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'llm:',
        '  provider:',
        '    backend: claude-code',
        '  models:',
        '    default: sonnet',
        '  promptCaching:',
        '    enabled: true',
        '    systemTtl: 1h',
        '    toolsTtl: 1h',
        '    historyTtl: 5m',
        '',
      ].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {
          claudeCodeAuthProbe: async () => ({
            ok: false as const,
            message: 'Authenticate Claude Code locally.',
          }),
        },
      ),
    ).resolves.toBe(1);

    expect(testIo.stdout()).toContain('claude-code');
    expect(testIo.stdout()).toContain('Authenticate Claude Code locally');
    expect(testIo.stdout()).toContain('claude-code ignores llm.promptCaching');
  });

  it('includes Postgres query-history readiness in project doctor output', async () => {
    process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret
    process.env.OPENAI_API_KEY = 'test-key'; // pragma: allowlist secret
    process.env.WAREHOUSE_DATABASE_URL = 'postgresql://reader@example.test/warehouse';
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'connections:',
        '  warehouse:',
        '    driver: postgres',
        '    url: env:WAREHOUSE_DATABASE_URL',
        '    context:',
        '      queryHistory:',
        '        enabled: true',
        'llm:',
        '  provider:',
        '    backend: anthropic',
        'ingest:',
        '  adapters:',
        '    - live-database',
        '    - historic-sql',
        '  embeddings:',
        '    backend: openai',
        '    model: text-embedding-3-small',
        '    dimensions: 1536',
        '',
      ].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();
    let probeCalls = 0;

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {
          queryHistoryReadinessProbe: async () => {
            probeCalls += 1;
            return {
              ok: true,
              dialect: 'postgres',
              runner: fakeDoctorHistoricSqlRunner(),
              result: {
                pgServerVersion: 'PostgreSQL 16.4',
                warnings: [],
                info: [
                  'pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
                ],
              },
            };
          },
        },
      ),
    ).resolves.toBe(0);

    const out = testIo.stdout();
    expect(probeCalls).toBe(1);
    expect(out).toContain('Query history');
    expect(out).toContain('warehouse');
    expect(out).toContain('pg_stat_statements ready (PostgreSQL 16.4)');
    expect(out).toContain('pg_stat_statements.max is 1000');
    expect(out).not.toContain('Update the Postgres parameter group or config');
    expect(out).toContain('ktx status --json');
    expect(out).toContain('ktx sl');
    expect(out).toContain('ktx wiki');
    expect(out).not.toContain('ktx scan');
    expect(out).not.toContain('ktx sl ask');
    delete process.env.ANTHROPIC_API_KEY;
    delete process.env.OPENAI_API_KEY;
    delete process.env.WAREHOUSE_DATABASE_URL;
  });

  it('returns blocked verdict when LLM is not configured', async () => {
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'connections:',
        '  warehouse:',
        '    driver: sqlite',
        '    path: ./warehouse.db',
        '',
      ].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(1);

    expect(testIo.stdout()).toContain('no LLM configured');
    expect(testIo.stdout()).not.toContain('ktx ask');
    expect(testIo.stdout()).toContain('ktx setup');
  });

  it('does not warn about removed-field migration hints', async () => {
    process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret
    process.env.WAREHOUSE_DATABASE_URL = 'postgresql://reader@example.test/warehouse';
    process.env.NOTION_TOKEN = 'notion-secret';
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'connections:',
        '  warehouse:',
        '    driver: postgres',
        '    url: env:WAREHOUSE_DATABASE_URL',
        '    readonly: true',
        '  local:',
        '    driver: sqlite',
        '    file_path: ./warehouse.db',
        '  docs:',
        '    driver: notion',
        '    auth_token_ref: env:NOTION_TOKEN',
        '    crawl_mode: all_accessible',
        '    last_successful_cursor: \'{"phase":"all_accessible_pages","cursor":"cursor-1"}\'',
        'ingest:',
        '  adapters:',
        '    - live-database',
        'llm:',
        '  provider:',
        '    backend: anthropic',
        '',
      ].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {
          queryHistoryReadinessProbe: async () => ({
            ok: true,
            dialect: 'postgres',
            runner: fakeDoctorHistoricSqlRunner(),
            result: {
              pgServerVersion: 'PostgreSQL 16.4',
              warnings: [],
              info: [],
            },
          }),
        },
      ),
    ).resolves.toBe(0);

    const out = testIo.stdout();
    expect(out).not.toContain('connections.warehouse.readonly is no longer used.');
    expect(out).not.toContain('connections.local.file_path was removed.');
    expect(out).not.toContain('connections.docs.last_successful_cursor is local sync state.');
    delete process.env.ANTHROPIC_API_KEY;
    delete process.env.WAREHOUSE_DATABASE_URL;
    delete process.env.NOTION_TOKEN;
  });

  it('warns when semantic-search embeddings are not configured', async () => {
    process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret
    await writeFile(
      join(tempDir, 'ktx.yaml'),
      [
        'connections:',
        '  warehouse:',
        '    driver: sqlite',
        '    path: ./warehouse.db',
        'llm:',
        '  provider:',
        '    backend: anthropic',
        'ingest:',
        '  adapters:',
        '    - live-database',
        '  embeddings:',
        '    backend: none',
        '    dimensions: 8',
        '',
      ].join('\n'),
      'utf-8',
    );
    const testIo = makeIo();

    await expect(
      runKtxDoctor(
        { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
        testIo.io,
        {},
      ),
    ).resolves.toBe(0);

    expect(testIo.stdout()).toContain('Embeddings');
    expect(testIo.stdout()).toContain('none');
    expect(testIo.stdout()).toContain('semantic search will be skipped');
    delete process.env.ANTHROPIC_API_KEY;
  });

  describe('command: validate', () => {
    it('prints a success line and exits 0 when ktx.yaml is schema-valid', async () => {
      await writeFile(
        join(tempDir, 'ktx.yaml'),
        [
          'connections:',
          '  warehouse:',
          '    driver: sqlite',
          '    path: ./warehouse.db',
          'llm:',
          '  provider:',
          '    backend: anthropic',
          '',
        ].join('\n'),
        'utf-8',
      );
      const testIo = makeIo();

      await expect(
        runKtxDoctor(
          { command: 'validate', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
          testIo.io,
          {},
        ),
      ).resolves.toBe(0);

      const out = testIo.stdout();
      expect(out).toContain('KTX status');
      expect(out).toContain('Config');
      expect(out).toContain('ktx.yaml schema valid');
      expect(out).not.toContain('LLM');
      expect(out).not.toContain('Connections');
      expect(out).not.toContain('Pipeline');
    });

    it('emits {ok: true} JSON when ktx.yaml is schema-valid', async () => {
      await writeFile(
        join(tempDir, 'ktx.yaml'),
        [
          'connections:',
          '  warehouse:',
          '    driver: sqlite',
          '    path: ./warehouse.db',
          'llm:',
          '  provider:',
          '    backend: anthropic',
          '',
        ].join('\n'),
        'utf-8',
      );
      const testIo = makeIo();

      await expect(
        runKtxDoctor(
          { command: 'validate', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
          testIo.io,
          {},
        ),
      ).resolves.toBe(0);

      expect(JSON.parse(testIo.stdout())).toEqual({ ok: true, projectDir: tempDir });
    });

    it('prints schema issues and exits 1 when ktx.yaml fails Zod validation', async () => {
      await writeFile(
        join(tempDir, 'ktx.yaml'),
        [
          'storrage:',
          '  state: sqlite',
          'ingest:',
          '  llm:',
          '    backend: anthropic',
          '',
        ].join('\n'),
        'utf-8',
      );
      const testIo = makeIo();

      await expect(
        runKtxDoctor(
          { command: 'validate', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
          testIo.io,
          {},
        ),
      ).resolves.toBe(1);

      const out = testIo.stdout();
      expect(out).toContain('Unsupported storrage: unknown field');
      expect(out).toContain('Unsupported ingest.llm: unknown field');
    });

    it('emits structured JSON issues when validation fails', async () => {
      await writeFile(
        join(tempDir, 'ktx.yaml'),
        ['storrage: {}', ''].join('\n'),
        'utf-8',
      );
      const testIo = makeIo();

      await expect(
        runKtxDoctor(
          { command: 'validate', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
          testIo.io,
          {},
        ),
      ).resolves.toBe(1);

      const parsed = JSON.parse(testIo.stdout()) as { error: string; issues: Array<{ path: string }> };
      expect(parsed.error).toBe('invalid_config');
      expect(parsed.issues.some((issue) => issue.path === 'storrage')).toBe(true);
    });

    it('prints the missing-project message and exits 1 when ktx.yaml is absent', async () => {
      const testIo = makeIo();

      await expect(
        runKtxDoctor(
          { command: 'validate', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
          testIo.io,
          {},
        ),
      ).resolves.toBe(1);

      expect(testIo.stdout()).toContain('No KTX project here yet.');
    });

    it('does not invoke the Postgres query-history probe in validate mode', async () => {
      await writeFile(
        join(tempDir, 'ktx.yaml'),
        [
          'connections:',
          '  warehouse:',
          '    driver: postgres',
          '    url: env:WAREHOUSE_DATABASE_URL',
          '    context:',
          '      queryHistory:',
          '        enabled: true',
          'llm:',
          '  provider:',
          '    backend: anthropic',
          '',
        ].join('\n'),
        'utf-8',
      );
      const testIo = makeIo();
      let probeCalls = 0;

      await expect(
        runKtxDoctor(
          { command: 'validate', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
          testIo.io,
          {
            queryHistoryReadinessProbe: async () => {
              probeCalls += 1;
              return {
                ok: true,
                dialect: 'postgres',
                runner: fakeDoctorHistoricSqlRunner(),
                result: { pgServerVersion: 'PostgreSQL 16.4', warnings: [], info: [] },
              };
            },
          },
        ),
      ).resolves.toBe(0);

      expect(probeCalls).toBe(0);
      expect(testIo.stdout()).toContain('ktx.yaml schema valid');
    });
  });
});
