import { describe, it, expect, afterEach } from 'vitest';
import { runChecks, type CheckResult, type DoctorContext } from './doctor.js';
import { writeFileSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';

describe('doctor checks', () => {
  const testDir = join(tmpdir(), 'flynn-test-doctor');

  afterEach(() => {
    try { rmSync(testDir, { recursive: true }); } catch {}
  });

  it('reports PASS when config file exists and is valid', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const configExists = results.find(r => r.label.includes('Config file'));
    expect(configExists?.status).toBe('pass');

    const configParses = results.find(r => r.label.includes('parses'));
    expect(configParses?.status).toBe('pass');

    const configValidates = results.find(r => r.label.includes('validates'));
    expect(configValidates?.status).toBe('pass');
  });

  it('reports FAIL when config file does not exist', async () => {
    const ctx: DoctorContext = { configPath: '/nonexistent/config.yaml', dataDir: testDir };
    const results = await runChecks(ctx);

    const configExists = results.find(r => r.label.includes('Config file'));
    expect(configExists?.status).toBe('fail');
  });

  it('reports FAIL on invalid YAML', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'bad.yaml');
    writeFileSync(configPath, '{{{{bad yaml');

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const configParses = results.find(r => r.label.includes('parses'));
    expect(configParses?.status).toBe('fail');
  });

  it('reports FAIL on schema validation failure', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'invalid.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: ""
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const configValidates = results.find(r => r.label.includes('validates'));
    expect(configValidates?.status).toBe('fail');
  });

  it('warns when deprecated server.tailscale_only key is present', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
server:
  tailscale_only: true
models:
  default:
    provider: anthropic
    model: claude-sonnet
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const deprecated = results.find(r => r.label.includes('deprecated keys'));
    expect(deprecated?.status).toBe('warn');
  });

  it('reports PASS for writable data directory', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const dataDir = results.find(r => r.label.includes('Data directory'));
    expect(dataDir?.status).toBe('pass');
  });

  it('reports PASS for accessible session DB', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const sessionDb = results.find(r => r.label.includes('Session DB'));
    expect(sessionDb?.status).toBe('pass');
  });

  it('reports SKIP for Gmail when not enabled', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const gmailCheck = results.find(r => r.label.includes('Gmail'));
    expect(gmailCheck?.status).toBe('skip');
  });

  it('reports WARN for Gmail when token missing', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    const credsPath = join(testDir, 'gmail-creds.json');
    writeFileSync(credsPath, '{}');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
automation:
  gmail:
    enabled: true
    credentials_file: "${credsPath}"
    token_file: "${join(testDir, 'nonexistent-token.json')}"
    output:
      channel: telegram
      peer: "123"
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const gmailCheck = results.find(r => r.label.includes('Gmail'));
    expect(gmailCheck?.status).toBe('warn');
    expect(gmailCheck?.detail).toContain('flynn gmail-auth');
  });

  it('reports PASS for Gmail when enabled (poll only)', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    const credsPath = join(testDir, 'gmail-creds.json');
    const tokenPath = join(testDir, 'gmail-token.json');
    writeFileSync(credsPath, JSON.stringify({ installed: { project_id: 'test-project' } }));
    writeFileSync(tokenPath, JSON.stringify({ refresh_token: 'x' }));

    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
automation:
  gmail:
    enabled: true
    credentials_file: "${credsPath}"
    token_file: "${tokenPath}"
    output:
      channel: telegram
      peer: "123"
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const gmailCheck = results.find(r => r.label.includes('Gmail configured'));
    expect(gmailCheck?.status).toBe('pass');
    expect(gmailCheck?.detail).toContain('poll');
  });

  it('reports WARN for Gmail when pubsub_topic shorthand used without project_id', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'config.yaml');
    const credsPath = join(testDir, 'gmail-creds.json');
    const tokenPath = join(testDir, 'gmail-token.json');
    writeFileSync(credsPath, '{}');
    writeFileSync(tokenPath, JSON.stringify({ refresh_token: 'x' }));

    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
automation:
  gmail:
    enabled: true
    credentials_file: "${credsPath}"
    token_file: "${tokenPath}"
    pubsub_topic: gmail-push
    output:
      channel: telegram
      peer: "123"
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const gmailCheck = results.find(r => r.label.includes('Gmail configured'));
    expect(gmailCheck?.status).toBe('warn');
    expect(gmailCheck?.detail).toContain('pubsub_topic shorthand');
  });

  it('skips downstream checks when config is invalid', async () => {
    const ctx: DoctorContext = { configPath: '/nonexistent/config.yaml', dataDir: testDir };
    const results = await runChecks(ctx);

    const modelCheck = results.find(r => r.label.includes('Model connectivity'));
    expect(modelCheck?.status).toBe('skip');

    const telegramCheck = results.find(r => r.label.includes('Telegram'));
    expect(telegramCheck?.status).toBe('skip');
  });

  it('reports WARN for skills when configured directories are missing', async () => {
    mkdirSync(testDir, { recursive: true });
    const configPath = join(testDir, 'skills-missing.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
skills:
  bundled_dir: "${join(testDir, 'missing-bundled')}"
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
    expect(skillsCheck?.status).toBe('warn');
    expect(skillsCheck?.detail).toContain('missing dirs');
  });

  it('reports PASS for skills when configured directory exists', async () => {
    const bundledDir = join(testDir, 'bundled');
    const sampleSkillDir = join(bundledDir, 'sample');
    mkdirSync(sampleSkillDir, { recursive: true });
    writeFileSync(join(sampleSkillDir, 'SKILL.md'), '# Sample skill');

    const configPath = join(testDir, 'skills-pass.yaml');
    writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
skills:
  bundled_dir: "${bundledDir}"
`);

    const ctx: DoctorContext = { configPath, dataDir: testDir };
    const results = await runChecks(ctx);

    const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
    expect(skillsCheck?.status).toBe('pass');
    expect(skillsCheck?.detail).toContain('1 skill(s), 1 available, 0 unavailable');
  });

  it('reports FAIL when OpenAI auth_mode=api_key has no available key sources', async () => {
    const originalHome = process.env.HOME;
    const originalKey = process.env.OPENAI_API_KEY;
    delete process.env.OPENAI_API_KEY;

    const homeDir = join(tmpdir(), `flynn-test-doctor-home-${Date.now()}`);
    process.env.HOME = homeDir;

    try {
      mkdirSync(testDir, { recursive: true });
      const configPath = join(testDir, 'openai-missing.yaml');
      writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: openai
    model: gpt-4o
    auth_mode: api_key
`);

      const ctx: DoctorContext = { configPath, dataDir: testDir };
      const results = await runChecks(ctx);
      const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
      expect(modelCheck?.status).toBe('fail');
      expect(modelCheck?.detail).toContain('OPENAI_API_KEY');
      expect(modelCheck?.detail).toContain('flynn openai-key');
    } finally {
      process.env.HOME = originalHome;
      if (originalKey) {
        process.env.OPENAI_API_KEY = originalKey;
      } else {
        delete process.env.OPENAI_API_KEY;
      }
    }
  });

  it('reports WARN when Vercel AI Gateway has no available API key sources', async () => {
    const originalKey = process.env.AI_GATEWAY_API_KEY;
    delete process.env.AI_GATEWAY_API_KEY;

    try {
      mkdirSync(testDir, { recursive: true });
      const configPath = join(testDir, 'vercel-missing.yaml');
      writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: vercel
    model: openai/gpt-4.1
`);

      const ctx: DoctorContext = { configPath, dataDir: testDir };
      const results = await runChecks(ctx);
      const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
      expect(modelCheck?.status).toBe('warn');
      expect(modelCheck?.detail).toContain('AI_GATEWAY_API_KEY');
    } finally {
      if (originalKey) {
        process.env.AI_GATEWAY_API_KEY = originalKey;
      } else {
        delete process.env.AI_GATEWAY_API_KEY;
      }
    }
  });

  it('reports FAIL when Anthropic auth_mode=oauth has no available auth token sources', async () => {
    const originalHome = process.env.HOME;
    const originalToken = process.env.ANTHROPIC_AUTH_TOKEN;
    delete process.env.ANTHROPIC_AUTH_TOKEN;

    const homeDir = join(tmpdir(), `flynn-test-doctor-home-${Date.now()}-2`);
    process.env.HOME = homeDir;

    try {
      mkdirSync(testDir, { recursive: true });
      const configPath = join(testDir, 'anthropic-missing-token.yaml');
      writeFileSync(configPath, `
telegram:
  bot_token: "test-token"
  allowed_chat_ids: [123]
models:
  default:
    provider: anthropic
    model: claude-sonnet
    auth_mode: oauth
`);

      const ctx: DoctorContext = { configPath, dataDir: testDir };
      const results = await runChecks(ctx);
      const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
      expect(modelCheck?.status).toBe('fail');
      expect(modelCheck?.detail).toContain('ANTHROPIC_AUTH_TOKEN');
      expect(modelCheck?.detail).toContain('flynn anthropic-auth --token');
    } finally {
      process.env.HOME = originalHome;
      if (originalToken) {
        process.env.ANTHROPIC_AUTH_TOKEN = originalToken;
      } else {
        delete process.env.ANTHROPIC_AUTH_TOKEN;
      }
    }
  });
});
