import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { EventEmitter } from 'events';
import type { ChildProcess } from 'child_process';
import { JobManager } from '../../src/server/analyze-job.js';

describe('JobManager', () => {
  let manager: JobManager;

  beforeEach(() => {
    manager = new JobManager();
  });

  afterEach(async () => {
    await manager.dispose();
  });

  it('creates a job with queued status', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    expect(job.id).toBeTruthy();
    expect(job.status).toBe('queued');
    expect(job.repoUrl).toBe('https://github.com/user/repo');
  });

  it('retrieves a job by id', () => {
    const created = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    const retrieved = manager.getJob(created.id);
    expect(retrieved).toEqual(created);
  });

  it('returns undefined for unknown job id', () => {
    expect(manager.getJob('nonexistent')).toBeUndefined();
  });

  it('allows multi-repo concurrency', () => {
    const job1 = manager.createJob({ repoPath: '/path/to/repo1' });
    manager.updateJob(job1.id, { status: 'analyzing' });
    const job2 = manager.createJob({ repoPath: '/path/to/repo2' });
    expect(job2.status).toBe('queued');
  });

  it('deduplicates jobs for the same repo path', () => {
    const job1 = manager.createJob({ repoPath: '/path/to/repo1' });
    manager.updateJob(job1.id, { status: 'analyzing' });
    const job2 = manager.createJob({ repoPath: '/path/to/repo1' });
    expect(job2.id).toBe(job1.id);
  });

  it('recovers jobs from ledger on init', async () => {
    const fs = await import('fs/promises');
    const path = await import('path');
    const os = await import('os');
    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ontoindex-job-ledger-'));

    try {
      const manager1 = new JobManager(tempDir);
      const job1 = manager1.createJob({ repoPath: '/repo1' });
      manager1.updateJob(job1.id, { status: 'analyzing' });
      await manager1.dispose();

      const manager2 = new JobManager(tempDir);
      await manager2.init();
      const recovered = manager2.getJob(job1.id);
      expect(recovered).toBeDefined();
      expect(recovered!.status).toBe('failed'); // Recovered active jobs should be failed
      expect(recovered!.error).toBe('Server restart');

      await manager2.dispose();
    } finally {
      await fs.rm(tempDir, { recursive: true, force: true });
    }
  });

  it('allows new job after previous completes', () => {
    const job1 = manager.createJob({ repoUrl: 'https://github.com/user/repo1' });
    manager.updateJob(job1.id, { status: 'analyzing' });
    manager.updateJob(job1.id, { status: 'complete' });
    const job2 = manager.createJob({ repoUrl: 'https://github.com/user/repo2' });
    expect(job2.status).toBe('queued');
  });

  it('returns existing job for same repoUrl when active', () => {
    const job1 = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    manager.updateJob(job1.id, { status: 'analyzing' });
    const job2 = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    expect(job2.id).toBe(job1.id);
  });

  it('updates job progress', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    manager.updateJob(job.id, {
      status: 'analyzing',
      progress: { phase: 'parsing', percent: 30, message: 'Parsing code' },
    });
    const updated = manager.getJob(job.id)!;
    expect(updated.status).toBe('analyzing');
    expect(updated.progress.percent).toBe(30);
  });

  it('emits progress events', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    const events: any[] = [];
    manager.onProgress(job.id, (data) => events.push(data));

    manager.updateJob(job.id, {
      progress: { phase: 'parsing', percent: 50, message: 'Parsing code' },
    });

    expect(events).toHaveLength(1);
    expect(events[0].percent).toBe(50);
  });

  it('emits terminal event on complete', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    const events: any[] = [];
    manager.onProgress(job.id, (data) => events.push(data));

    manager.updateJob(job.id, { status: 'complete', repoName: 'repo' });

    expect(events).toHaveLength(1);
    expect(events[0].phase).toBe('complete');
    expect(events[0].percent).toBe(100);
  });

  it('sets completedAt on terminal status', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    manager.updateJob(job.id, { status: 'complete' });
    expect(manager.getJob(job.id)!.completedAt).toBeDefined();
  });

  it('unsubscribe stops events', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    const events: any[] = [];
    const unsub = manager.onProgress(job.id, (data) => events.push(data));

    manager.updateJob(job.id, {
      progress: { phase: 'p1', percent: 10, message: 'm1' },
    });
    unsub();
    manager.updateJob(job.id, {
      progress: { phase: 'p2', percent: 20, message: 'm2' },
    });

    expect(events).toHaveLength(1);
  });

  it('cancelJob sets status to failed with reason', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    manager.updateJob(job.id, { status: 'analyzing' });
    const cancelled = manager.cancelJob(job.id, 'Cancelled by user');
    expect(cancelled).toBe(true);
    expect(manager.getJob(job.id)!.status).toBe('failed');
    expect(manager.getJob(job.id)!.error).toBe('Cancelled by user');
  });

  it('runs registered cancel handlers', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    manager.updateJob(job.id, { status: 'analyzing' });
    const handler = vi.fn();
    manager.registerCancelHandler(job.id, handler);

    expect(manager.cancelJob(job.id, 'Cancelled by user')).toBe(true);
    expect(handler).toHaveBeenCalledTimes(1);
  });

  it('runs terminal child cleanup after cancelled child exits', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    manager.updateJob(job.id, { status: 'analyzing' });
    const child = new EventEmitter() as ChildProcess;
    child.kill = vi.fn(() => true) as any;
    const cleanup = vi.fn();

    manager.registerChild(job.id, child, { onTerminalExit: cleanup, killGraceMs: 60_000 });
    expect(manager.cancelJob(job.id, 'Cancelled by user')).toBe(true);
    child.emit('exit', null, 'SIGTERM');

    expect(cleanup).toHaveBeenCalledTimes(1);
  });

  it('escalates cancelled child processes after the kill grace period', () => {
    vi.useFakeTimers();
    try {
      const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
      manager.updateJob(job.id, { status: 'analyzing' });
      const child = new EventEmitter() as ChildProcess;
      child.kill = vi.fn(() => true) as any;

      manager.registerChild(job.id, child, { killGraceMs: 100 });
      expect(manager.cancelJob(job.id, 'Cancelled by user')).toBe(true);
      expect(child.kill).toHaveBeenCalledWith('SIGTERM');

      vi.advanceTimersByTime(100);

      expect(child.kill).toHaveBeenCalledWith('SIGKILL');
    } finally {
      vi.useRealTimers();
    }
  });

  it('cancelJob returns false for terminal jobs', () => {
    const job = manager.createJob({ repoUrl: 'https://github.com/user/repo' });
    manager.updateJob(job.id, { status: 'complete' });
    expect(manager.cancelJob(job.id)).toBe(false);
  });

  it('cancelJob returns false for unknown job', () => {
    expect(manager.cancelJob('nonexistent')).toBe(false);
  });
});
