/**
 * Tests for async extraction job queue — createJob, getJob, listJobs.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createJob, getJob, listJobs, resetJobStore } from './jobs.js';
import { type WebhookConfig } from './webhooks.js';

// Mock the python-bridge to avoid real sidecar calls
vi.mock('../../lib/python-bridge.js', () => ({
  sidecarExtract: vi.fn(),
}));

// Mock the webhooks module
vi.mock('./webhooks.js', () => ({
  triggerJobWebhook: vi.fn(),
}));

import { sidecarExtract } from '../../lib/python-bridge.js';
import { triggerJobWebhook } from './webhooks.js';
const mockSidecarExtract = vi.mocked(sidecarExtract);
const mockTriggerJobWebhook = vi.mocked(triggerJobWebhook);

describe('extraction jobs', () => {
  beforeEach(() => {
    mockSidecarExtract.mockReset();
    mockTriggerJobWebhook.mockReset();
    resetJobStore();
  });

  describe('createJob', () => {
    it('creates a job and starts processing', () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      const job = createJob([{ text: 'Hello world' }]);

      expect(job.id).toBeDefined();
      expect(['pending', 'processing', 'completed']).toContain(job.status);
      expect(job.inputs).toHaveLength(1);
      expect(job.results).toEqual([]);
      expect(job.errors).toEqual([]);
      expect(job.progress).toEqual({ completed: 0, total: 1 });
      expect(job.createdAt).toBeDefined();
    });

    it('sets correct total from inputs length', () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      const job = createJob([{ text: 'First' }, { text: 'Second' }, { text: 'Third' }]);

      expect(job.progress.total).toBe(3);
    });

    it('generates unique IDs for each job', () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      const job1 = createJob([{ text: 'A' }]);
      const job2 = createJob([{ text: 'B' }]);

      expect(job1.id).not.toBe(job2.id);
    });

    it('stores the job for later retrieval', () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      const job = createJob([{ text: 'Stored' }]);
      const retrieved = getJob(job.id);

      expect(retrieved).toBeDefined();
      expect(retrieved!.id).toBe(job.id);
    });

    it('processes job and marks as completed on success', async () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [{ extraction_class: 'person', extraction_text: 'John' }],
        metadata: { model_id: 'gemini', duration_ms: 100, char_count: 20 },
      });

      const job = createJob([{ text: 'Meet John at 3pm' }]);

      // Wait for background processing
      await vi.waitFor(
        () => {
          const j = getJob(job.id);
          expect(j!.status).toBe('completed');
        },
        { timeout: 2000 }
      );

      const completed = getJob(job.id)!;
      expect(completed.results).toHaveLength(1);
      expect(completed.results[0].extractions[0].extraction_text).toBe('John');
      expect(completed.progress.completed).toBe(1);
      expect(completed.completedAt).toBeDefined();
    });

    it('records errors for failed extractions', async () => {
      mockSidecarExtract.mockRejectedValue(new Error('Sidecar unavailable'));

      const job = createJob([{ text: 'Will fail' }]);

      await vi.waitFor(
        () => {
          const j = getJob(job.id);
          expect(j!.status).not.toBe('pending');
          expect(j!.status).not.toBe('processing');
        },
        { timeout: 2000 }
      );

      const finished = getJob(job.id)!;
      expect(finished.errors).toHaveLength(1);
      expect(finished.errors[0].index).toBe(0);
      expect(finished.errors[0].error).toBe('Sidecar unavailable');
      expect(finished.status).toBe('failed');
    });

    it('handles mixed success/failure in batch', async () => {
      mockSidecarExtract
        .mockResolvedValueOnce({
          extractions: [{ extraction_class: 'person', extraction_text: 'Alice' }],
          metadata: { model_id: 'test', duration_ms: 50, char_count: 10 },
        })
        .mockRejectedValueOnce(new Error('timeout'))
        .mockResolvedValueOnce({
          extractions: [],
          metadata: { model_id: 'test', duration_ms: 30, char_count: 5 },
        });

      const job = createJob([
        { text: 'Alice is here' },
        { text: 'Will timeout' },
        { text: 'Empty result' },
      ]);

      await vi.waitFor(
        () => {
          const j = getJob(job.id);
          expect(j!.progress.completed).toBe(3);
        },
        { timeout: 2000 }
      );

      const finished = getJob(job.id)!;
      expect(finished.status).toBe('completed'); // Not all failed
      expect(finished.errors).toHaveLength(1);
      expect(finished.errors[0].index).toBe(1);
      expect(finished.results).toHaveLength(3); // Placeholder for failed one
    });
  });

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

  describe('listJobs', () => {
    it('returns jobs sorted by creation date (newest first)', () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      createJob([{ text: 'First' }]);
      createJob([{ text: 'Second' }]);
      createJob([{ text: 'Third' }]);

      const jobs = listJobs();
      expect(jobs.length).toBeGreaterThanOrEqual(3);

      // Verify sorted newest first
      for (let i = 0; i < jobs.length - 1; i++) {
        expect(jobs[i].createdAt >= jobs[i + 1].createdAt).toBe(true);
      }
    });

    it('respects limit parameter', () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      for (let i = 0; i < 5; i++) {
        createJob([{ text: `Job ${i}` }]);
      }

      const limited = listJobs(2);
      expect(limited.length).toBe(2);
    });

    it('defaults to limit of 50', () => {
      const jobs = listJobs();
      expect(jobs.length).toBeLessThanOrEqual(50);
    });
  });

  describe('webhook integration', () => {
    it('stores webhook config on job creation', () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      const webhookConfig: WebhookConfig = {
        url: 'https://example.com/webhook',
        secret: 'secret',
        retryAttempts: 3,
      };

      const job = createJob([{ text: 'test' }], 'req-123', webhookConfig);

      expect(job.webhookConfig).toEqual(webhookConfig);
    });

    it('triggers webhook on job completion', async () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [{ extraction_class: 'test', extraction_text: 'result' }],
        metadata: { model_id: 'gemini', duration_ms: 100, char_count: 10 },
      });
      mockTriggerJobWebhook.mockResolvedValue(undefined);

      const webhookConfig: WebhookConfig = {
        url: 'https://example.com/webhook',
        secret: 'secret',
      };

      const job = createJob([{ text: 'test' }], 'req-123', webhookConfig);

      // Wait for background processing
      await vi.waitFor(
        () => {
          const j = getJob(job.id);
          expect(j!.status).toBe('completed');
        },
        { timeout: 2000 }
      );

      expect(mockTriggerJobWebhook).toHaveBeenCalledWith(
        expect.objectContaining({ id: job.id, status: 'completed' }),
        webhookConfig
      );
    });

    it('does not fail job if webhook fails', async () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });
      mockTriggerJobWebhook.mockRejectedValue(new Error('Webhook failed'));

      const webhookConfig: WebhookConfig = {
        url: 'https://example.com/webhook',
        secret: 'secret',
      };

      const job = createJob([{ text: 'test' }], 'req-123', webhookConfig);

      // Wait for background processing
      await vi.waitFor(
        () => {
          const j = getJob(job.id);
          expect(j!.status).toBe('completed');
        },
        { timeout: 2000 }
      );

      // Job should still be completed even if webhook failed
      const completed = getJob(job.id)!;
      expect(completed.status).toBe('completed');
    });

    it('does not trigger webhook if not configured', async () => {
      mockSidecarExtract.mockResolvedValue({
        extractions: [],
        metadata: { model_id: 'test', duration_ms: 10, char_count: 5 },
      });

      const job = createJob([{ text: 'test' }]);

      // Wait for background processing
      await vi.waitFor(
        () => {
          const j = getJob(job.id);
          expect(j!.status).toBe('completed');
        },
        { timeout: 2000 }
      );

      expect(mockTriggerJobWebhook).not.toHaveBeenCalled();
    });
  });
});
