/**
 * Tests for webhook delivery system.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  signWebhookPayload,
  verifyWebhookSignature,
  buildWebhookPayload,
  triggerJobWebhook,
  getWebhookDelivery,
  listJobDeliveries,
  getDeliveryStats,
  retryWebhookDelivery,
  resetDeliveryLog,
  type WebhookConfig,
} from './webhooks.js';

describe('webhooks', () => {
  const mockJob = {
    id: 'job-123',
    status: 'completed' as const,
    inputs: [{ text: 'test input' }],
    results: [
      {
        extractions: [{ extraction_class: 'test', extraction_text: 'result' }],
        metadata: { model_id: 'gemini', duration_ms: 100, char_count: 10 },
      },
    ],
    errors: [],
    progress: { completed: 1, total: 1 },
    createdAt: new Date().toISOString(),
    completedAt: new Date().toISOString(),
  };

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

  beforeEach(() => {
    vi.restoreAllMocks();
    resetDeliveryLog();
  });

  describe('signWebhookPayload', () => {
    it('generates consistent HMAC signatures', () => {
      const payload = '{"test": true}';
      const sig1 = signWebhookPayload(payload, 'secret');
      const sig2 = signWebhookPayload(payload, 'secret');
      expect(sig1).toBe(sig2);
      expect(sig1).toHaveLength(64); // SHA-256 hex
    });

    it('produces different signatures for different secrets', () => {
      const payload = '{"test": true}';
      const sig1 = signWebhookPayload(payload, 'secret1');
      const sig2 = signWebhookPayload(payload, 'secret2');
      expect(sig1).not.toBe(sig2);
    });
  });

  describe('verifyWebhookSignature', () => {
    it('returns true for valid signature', () => {
      const payload = '{"test": true}';
      const secret = 'my-secret';
      const signature = signWebhookPayload(payload, secret);
      expect(verifyWebhookSignature(payload, signature, secret)).toBe(true);
    });

    it('returns false for invalid signature', () => {
      const payload = '{"test": true}';
      const signature = signWebhookPayload(payload, 'correct-secret');
      expect(verifyWebhookSignature(payload, signature, 'wrong-secret')).toBe(false);
    });

    it('returns false for tampered payload', () => {
      const originalPayload = '{"test": true}';
      const tamperedPayload = '{"test": false}';
      const secret = 'my-secret';
      const signature = signWebhookPayload(originalPayload, secret);
      expect(verifyWebhookSignature(tamperedPayload, signature, secret)).toBe(false);
    });
  });

  describe('buildWebhookPayload', () => {
    it('includes job metadata', () => {
      const payload = JSON.parse(buildWebhookPayload(mockJob));
      expect(payload.event).toBe('job.completed');
      expect(payload.jobId).toBe('job-123');
      expect(payload.status).toBe('completed');
      expect(payload.timestamp).toBeDefined();
    });

    it('includes result summary', () => {
      const payload = JSON.parse(buildWebhookPayload(mockJob));
      expect(payload.resultSummary).toEqual({
        totalInputs: 1,
        successfulResults: 1,
        errorCount: 0,
      });
    });
  });

  describe('triggerJobWebhook', () => {
    it('delivers webhook successfully', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
      });

      await triggerJobWebhook(mockJob, mockConfig);

      expect(fetch).toHaveBeenCalledWith(
        'https://example.com/webhook',
        expect.objectContaining({
          method: 'POST',
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
            'X-Webhook-Event': 'job.completed',
          }),
        })
      );

      const delivery = listJobDeliveries('job-123')[0];
      expect(delivery).toBeDefined();
      expect(delivery.status).toBe('delivered');
    });

    it('records delivery on failure', async () => {
      globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'));

      await triggerJobWebhook(mockJob, mockConfig);

      const delivery = listJobDeliveries('job-123')[0];
      expect(delivery).toBeDefined();
      expect(delivery.status).toBe('failed');
      expect(delivery.error).toContain('Network error');
    });

    it('includes correct signature in request', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });

      await triggerJobWebhook(mockJob, mockConfig);

      const call = vi.mocked(fetch).mock.calls[0];
      const headers = call[1]?.headers as Record<string, string>;
      const payload = call[1]?.body as string;

      expect(headers['X-Webhook-Signature']).toMatch(/^sha256=/);
      const signature = headers['X-Webhook-Signature'].replace('sha256=', '');
      expect(verifyWebhookSignature(payload, signature, mockConfig.secret)).toBe(true);
    });
  });

  describe('delivery stats', () => {
    it('returns zero stats when no deliveries', () => {
      const stats = getDeliveryStats();
      expect(stats.total).toBe(0);
      expect(stats.delivered).toBe(0);
      expect(stats.failed).toBe(0);
      expect(stats.pending).toBe(0);
    });

    it('accurately counts delivery statuses', async () => {
      globalThis.fetch = vi.fn()
        .mockResolvedValueOnce({ ok: true, status: 200 })
        .mockRejectedValueOnce(new Error('Failed'));

      const job1 = { ...mockJob, id: 'job-1' };
      const job2 = { ...mockJob, id: 'job-2' };

      await triggerJobWebhook(job1, mockConfig);
      await triggerJobWebhook(job2, mockConfig);

      const stats = getDeliveryStats();
      expect(stats.total).toBe(2);
      expect(stats.delivered).toBe(1);
      expect(stats.failed).toBe(1);
    });
  });

  describe('retryWebhookDelivery', () => {
    it('retries failed delivery successfully', async () => {
      // First, make all fetch calls fail (for initial trigger with retries)
      globalThis.fetch = vi.fn().mockRejectedValue(new Error('Persistent failure'));

      await triggerJobWebhook(mockJob, mockConfig);
      const delivery = listJobDeliveries('job-123')[0];
      expect(delivery.status).toBe('failed');

      // Now set up mock to succeed for the retry
      globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });

      const retried = await retryWebhookDelivery(delivery.id, mockJob, mockConfig);
      expect(retried).toBe(true);

      const updated = getWebhookDelivery(delivery.id);
      expect(updated?.status).toBe('delivered');
    });

    it('returns false for unknown delivery ID', async () => {
      const result = await retryWebhookDelivery('unknown-id', mockJob, mockConfig);
      expect(result).toBe(false);
    });
  });

  describe('listJobDeliveries', () => {
    it('returns deliveries sorted by creation time', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });

      const job1 = { ...mockJob, id: 'job-1' };
      const job2 = { ...mockJob, id: 'job-2' };

      await triggerJobWebhook(job1, mockConfig);
      await new Promise(r => setTimeout(r, 10));
      await triggerJobWebhook(job2, mockConfig);

      const deliveries = listJobDeliveries('job-1');
      expect(deliveries).toHaveLength(1);
      expect(deliveries[0].jobId).toBe('job-1');
    });
  });
});
