import { Test, TestingModule } from '@nestjs/testing';
import { createHmac } from 'crypto';
import { PrismaService } from '../../prisma/prisma.service';

// Mock @nestjs/bullmq before importing the processor
jest.mock('@nestjs/bullmq', () => ({
  Processor: () => () => {},
  WorkerHost: class {},
}));

import { WebhookDeliveryProcessor } from './webhook-delivery.processor';

describe('WebhookDeliveryProcessor', () => {
  let processor: WebhookDeliveryProcessor;
  let prisma: PrismaService;

  const mockPrisma = {
    webhookLog: {
      create: jest.fn(),
    },
  };

  beforeEach(async () => {
    jest.clearAllMocks();
    jest.restoreAllMocks();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        WebhookDeliveryProcessor,
        { provide: PrismaService, useValue: mockPrisma },
      ],
    }).compile();

    processor = module.get<WebhookDeliveryProcessor>(WebhookDeliveryProcessor);
    prisma = module.get<PrismaService>(PrismaService);
  });

  const createJob = (overrides = {}) => ({
    data: {
      clubId: 'club-1',
      subscriptionId: 'sub-1',
      event: 'client.created',
      payload: { id: 'client-1', firstName: 'Иван' },
      secret: 'webhook-secret-123',
      url: 'https://example.com/webhook',
      ...overrides,
    },
    attemptsMade: 0,
  });

  describe('process - successful delivery', () => {
    it('should deliver webhook and create log with deliveredAt set', async () => {
      const job = createJob();
      const mockResponse = {
        ok: true,
        status: 200,
        text: jest.fn().mockResolvedValue('OK'),
      };

      jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse as any);
      mockPrisma.webhookLog.create.mockResolvedValue({});

      await processor.process(job as any);

      expect(global.fetch).toHaveBeenCalledWith(
        'https://example.com/webhook',
        expect.objectContaining({
          method: 'POST',
          body: expect.any(String),
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
          }),
        }),
      );

      expect(mockPrisma.webhookLog.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          subscriptionId: 'sub-1',
          event: 'client.created',
          statusCode: 200,
          deliveredAt: expect.any(Date),
        }),
      });
    });
  });

  describe('process - failed delivery', () => {
    it('should create log with deliveredAt null and throw error on 500 response', async () => {
      const job = createJob();
      const mockResponse = {
        ok: false,
        status: 500,
        text: jest.fn().mockResolvedValue('Internal Server Error'),
      };

      jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse as any);
      mockPrisma.webhookLog.create.mockResolvedValue({});

      await expect(processor.process(job as any)).rejects.toThrow();

      expect(mockPrisma.webhookLog.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          subscriptionId: 'sub-1',
          event: 'client.created',
          statusCode: 500,
          deliveredAt: null,
        }),
      });
    });
  });

  describe('process - network error', () => {
    it('should create log and throw error when fetch throws', async () => {
      const job = createJob();

      jest.spyOn(global, 'fetch').mockRejectedValue(new Error('ECONNREFUSED'));
      mockPrisma.webhookLog.create.mockResolvedValue({});

      await expect(processor.process(job as any)).rejects.toThrow();

      expect(mockPrisma.webhookLog.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          subscriptionId: 'sub-1',
          event: 'client.created',
          deliveredAt: null,
        }),
      });
    });
  });

  describe('HMAC signature', () => {
    it('should send correct HMAC-SHA256 signature in header', async () => {
      const job = createJob();
      const payload = job.data.payload;
      const secret = job.data.secret;

      const mockResponse = {
        ok: true,
        status: 200,
        text: jest.fn().mockResolvedValue('OK'),
      };

      let capturedHeaders: Record<string, string> = {};
      let capturedBody = '';

      jest.spyOn(global, 'fetch').mockImplementation(async (_url, init: any) => {
        capturedHeaders = init.headers;
        capturedBody = init.body;
        return mockResponse as any;
      });

      mockPrisma.webhookLog.create.mockResolvedValue({});

      await processor.process(job as any);

      // Verify the signature matches what we'd expect
      const expectedSignature = createHmac('sha256', secret)
        .update(capturedBody)
        .digest('hex');

      // The processor should include a signature header
      const signatureHeader =
        capturedHeaders['X-Webhook-Signature'] ||
        capturedHeaders['x-webhook-signature'] ||
        capturedHeaders['X-Signature'];

      expect(signatureHeader).toBeDefined();
      expect(signatureHeader).toContain(expectedSignature);
    });
  });
});
