import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { initCommand, listTemplates } from '../src/commands/init.js';
import { tryLoadConfig, loadConfig } from '../src/config.js';
import { doctorCommand } from '../src/commands/doctor.js';
import { createPrekeyServer, MemoryPrekeyStore } from '@shade/server';
import { SubtleCryptoProvider } from '@shade/crypto-web';

const crypto = new SubtleCryptoProvider();

describe('CLI: init command', () => {
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'shade-cli-'));
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  test('listTemplates returns the bundled templates', () => {
    const templates = listTemplates();
    expect(templates).toContain('bun-server');
    expect(templates).toContain('chat-demo');
  });

  test('init scaffolds a bun-server project with substitutions', async () => {
    await initCommand({ name: 'my-app', template: 'bun-server', cwd: tmpDir });

    const target = join(tmpDir, 'my-app');
    expect(existsSync(target)).toBe(true);
    expect(existsSync(join(target, 'package.json'))).toBe(true);
    expect(existsSync(join(target, 'src/index.ts'))).toBe(true);
    expect(existsSync(join(target, '.shaderc.json'))).toBe(true);

    const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf-8'));
    expect(pkg.name).toBe('my-app');

    const shaderc = JSON.parse(readFileSync(join(target, '.shaderc.json'), 'utf-8'));
    expect(shaderc.address).toBe('my-app');

    const index = readFileSync(join(target, 'src/index.ts'), 'utf-8');
    expect(index).not.toContain('__PROJECT_NAME__');
    expect(index).toContain('my-app');
  });

  test('init with custom prekey-server URL', async () => {
    await initCommand({
      name: 'app2',
      template: 'bun-server',
      cwd: tmpDir,
      prekeyServer: 'https://custom.example.com',
    });

    const target = join(tmpDir, 'app2');
    const shaderc = JSON.parse(readFileSync(join(target, '.shaderc.json'), 'utf-8'));
    expect(shaderc.prekeyServer).toBe('https://custom.example.com');
  });

  test('init refuses to overwrite existing directory', async () => {
    await initCommand({ name: 'foo', cwd: tmpDir });
    expect(initCommand({ name: 'foo', cwd: tmpDir })).rejects.toThrow(/already exists/);
  });

  test('init with unknown template throws with helpful error', async () => {
    expect(initCommand({ name: 'x', template: 'nonexistent', cwd: tmpDir })).rejects.toThrow(
      /not found/,
    );
  });

  test('init with --doctor runs diagnostics against the new project', async () => {
    const port = 19500 + Math.floor(Math.random() * 200);
    const app = createPrekeyServer({
      crypto,
      store: new MemoryPrekeyStore(),
      disableRateLimit: true,
    });
    const server = Bun.serve({ port, fetch: app.fetch });

    const logs: string[] = [];
    const originalLog = console.log;
    console.log = (...args) => logs.push(args.join(' '));

    try {
      await initCommand({
        name: 'doc-app',
        template: 'bun-server',
        cwd: tmpDir,
        prekeyServer: `http://localhost:${port}`,
        runDoctor: true,
      });
    } finally {
      console.log = originalLog;
      server.stop();
    }

    const out = logs.join('\n');
    expect(out).toContain('Shade doctor');
    expect(out).toContain('Prekey server is reachable');
  });

  test('chat-demo template scaffolds correctly', async () => {
    await initCommand({ name: 'chat', template: 'chat-demo', cwd: tmpDir });

    const target = join(tmpDir, 'chat');
    expect(existsSync(join(target, 'src/alice.ts'))).toBe(true);
    expect(existsSync(join(target, 'src/bob.ts'))).toBe(true);

    const alice = readFileSync(join(target, 'src/alice.ts'), 'utf-8');
    expect(alice).not.toContain('__PROJECT_NAME__');
  });

  test('bun-server template wires the resumable-stream prune cron by default', async () => {
    // V3.1 acceptance: a freshly-scaffolded bun-server must call
    // shade.pruneStreamStates on startup + on a daily interval, with
    // the SHADE_STREAM_RETENTION_DAYS env var as the override knob.
    await initCommand({ name: 'cron-app', template: 'bun-server', cwd: tmpDir });

    const indexSrc = readFileSync(
      join(tmpDir, 'cron-app', 'src/index.ts'),
      'utf-8',
    );

    expect(indexSrc).toContain('shade.pruneStreamStates(');
    expect(indexSrc).toContain('SHADE_STREAM_RETENTION_DAYS');
    expect(indexSrc).toContain('setInterval');
    // Default horizon stays 14 days unless overridden.
    expect(indexSrc).toMatch(/\?\?\s*14/);
    // Cron timer must not keep the process alive on its own.
    expect(indexSrc).toContain('.unref');
  });
});

describe('CLI: config loading', () => {
  let tmpDir: string;
  let originalEnv: typeof process.env;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'shade-config-'));
    originalEnv = { ...process.env };
    delete process.env.SHADE_PREKEY_SERVER;
    delete process.env.SHADE_DB_PATH;
    delete process.env.SHADE_OBSERVER_TOKEN;
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
    process.env = originalEnv;
  });

  test('loads .shaderc.json from cwd', () => {
    writeFileSync(
      join(tmpDir, '.shaderc.json'),
      JSON.stringify({ prekeyServer: 'https://example.com', storage: 'sqlite:./test.db' }),
    );
    const config = loadConfig(tmpDir);
    expect(config.prekeyServer).toBe('https://example.com');
    expect(config.storage).toBe('sqlite:./test.db');
  });

  test('falls back to SHADE_PREKEY_SERVER env var', () => {
    process.env.SHADE_PREKEY_SERVER = 'https://env.example.com';
    const config = loadConfig(tmpDir);
    expect(config.prekeyServer).toBe('https://env.example.com');
  });

  test('file config takes precedence over env var', () => {
    process.env.SHADE_PREKEY_SERVER = 'https://env.example.com';
    writeFileSync(
      join(tmpDir, '.shaderc.json'),
      JSON.stringify({ prekeyServer: 'https://file.example.com' }),
    );
    const config = loadConfig(tmpDir);
    expect(config.prekeyServer).toBe('https://file.example.com');
  });

  test('throws with clear error when missing prekeyServer', () => {
    expect(() => loadConfig(tmpDir)).toThrow(/Missing prekeyServer/);
  });

  test('tryLoadConfig returns error without throwing', () => {
    const result = tryLoadConfig(tmpDir);
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.error).toContain('Missing prekeyServer');
  });
});

describe('CLI: doctor command', () => {
  let tmpDir: string;
  let originalEnv: typeof process.env;
  let originalCwd: string;
  let serverStop: (() => void) | null = null;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'shade-doctor-'));
    originalEnv = { ...process.env };
    originalCwd = process.cwd();
    process.chdir(tmpDir);
  });

  afterEach(() => {
    process.chdir(originalCwd);
    if (serverStop) {
      serverStop();
      serverStop = null;
    }
    rmSync(tmpDir, { recursive: true, force: true });
    process.env = originalEnv;
  });

  test('doctor reports missing config', async () => {
    const logs: string[] = [];
    const originalLog = console.log;
    console.log = (...args) => logs.push(args.join(' '));

    try {
      await doctorCommand();
    } finally {
      console.log = originalLog;
    }

    const out = logs.join('\n');
    expect(out).toContain('Missing prekeyServer');
  });

  test('doctor reports reachable prekey server', async () => {
    // Spin up a real prekey server
    const port = 19300 + Math.floor(Math.random() * 200);
    const app = createPrekeyServer({
      crypto,
      store: new MemoryPrekeyStore(),
      disableRateLimit: true,
    });
    const server = Bun.serve({ port, fetch: app.fetch });
    serverStop = () => server.stop();

    writeFileSync(
      join(tmpDir, '.shaderc.json'),
      JSON.stringify({
        prekeyServer: `http://localhost:${port}`,
        storage: `sqlite:${tmpDir}/client.db`,
      }),
    );

    const logs: string[] = [];
    const originalLog = console.log;
    console.log = (...args) => logs.push(args.join(' '));

    try {
      await doctorCommand();
    } finally {
      console.log = originalLog;
    }

    const out = logs.join('\n');
    expect(out).toContain('Prekey server is reachable');
  });
});
