import { Test, TestingModule } from '@nestjs/testing';
import { getQueueToken } from '@nestjs/bullmq';
import { WebhookDispatchService, calculateBackoff } from './webhook-dispatch.service';
import { PrismaService } from '../../prisma/prisma.service';

describe('WebhookDispatchService', () => {
  let service: WebhookDispatchService;
  let prisma: PrismaService;
  let mockQueue: { add: jest.Mock };

  const mockPrisma = {
    webhookSubscription: {
      findMany: jest.fn(),
    },
  };

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

    mockQueue = { add: jest.fn() };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        WebhookDispatchService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: getQueueToken('webhook-delivery'), useValue: mockQueue },
      ],
    }).compile();

    service = module.get<WebhookDispatchService>(WebhookDispatchService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  describe('onClientCreated', () => {
    it('should find subscriptions and enqueue jobs', async () => {
      const event = {
        clubId: 'club-1',
        data: {
          id: 'client-1',
          firstName: 'Иван',
          lastName: 'Иванов',
          phone: '+79001234567',
        },
      };

      const subscriptions = [
        {
          id: 'sub-1',
          url: 'https://example.com/webhook',
          secret: 'secret-1',
        },
      ];

      mockPrisma.webhookSubscription.findMany.mockResolvedValue(subscriptions);

      await service.onClientCreated(event);

      expect(mockPrisma.webhookSubscription.findMany).toHaveBeenCalledWith({
        where: {
          clubId: 'club-1',
          isActive: true,
          events: { has: 'client.created' },
        },
        select: { id: true, url: true, secret: true },
      });

      expect(mockQueue.add).toHaveBeenCalledWith(
        'webhook:client.created',
        expect.objectContaining({
          clubId: 'club-1',
          subscriptionId: 'sub-1',
          event: 'client.created',
          url: 'https://example.com/webhook',
          secret: 'secret-1',
          payload: expect.objectContaining({
            event: 'client.created',
            timestamp: expect.any(String),
            club_id: 'club-1',
            data: event.data,
          }),
        }),
        expect.objectContaining({
          attempts: 3,
        }),
      );
    });
  });

  describe('dispatch', () => {
    it('should skip when no subscriptions found', async () => {
      mockPrisma.webhookSubscription.findMany.mockResolvedValue([]);

      await service['dispatch']('club-1', 'client.created', { id: 'client-1' });

      expect(mockQueue.add).not.toHaveBeenCalled();
    });

    it('should create jobs with correct data for each subscription', async () => {
      const subscriptions = [
        {
          id: 'sub-1',
          url: 'https://example.com/hook1',
          secret: 'secret-1',
        },
        {
          id: 'sub-2',
          url: 'https://other.com/hook2',
          secret: 'secret-2',
        },
      ];

      mockPrisma.webhookSubscription.findMany.mockResolvedValue(subscriptions);

      const data = { id: 'client-1', firstName: 'Иван' };
      await service['dispatch']('club-1', 'client.created', data);

      expect(mockQueue.add).toHaveBeenCalledTimes(2);

      expect(mockQueue.add).toHaveBeenCalledWith(
        'webhook:client.created',
        expect.objectContaining({
          clubId: 'club-1',
          subscriptionId: 'sub-1',
          event: 'client.created',
          url: 'https://example.com/hook1',
          secret: 'secret-1',
          payload: expect.objectContaining({
            event: 'client.created',
            timestamp: expect.any(String),
            club_id: 'club-1',
            data,
          }),
        }),
        expect.objectContaining({ attempts: 3 }),
      );

      expect(mockQueue.add).toHaveBeenCalledWith(
        'webhook:client.created',
        expect.objectContaining({
          clubId: 'club-1',
          subscriptionId: 'sub-2',
          event: 'client.created',
          url: 'https://other.com/hook2',
          secret: 'secret-2',
          payload: expect.objectContaining({
            event: 'client.created',
            timestamp: expect.any(String),
            club_id: 'club-1',
            data,
          }),
        }),
        expect.objectContaining({ attempts: 3 }),
      );
    });

    it('should enqueue jobs for training.created event', async () => {
      const subscriptions = [
        {
          id: 'sub-3',
          url: 'https://hooks.example.com/training',
          secret: 'training-secret',
        },
      ];

      mockPrisma.webhookSubscription.findMany.mockResolvedValue(subscriptions);

      await service['dispatch']('club-2', 'training.created', { id: 'training-1' });

      expect(mockPrisma.webhookSubscription.findMany).toHaveBeenCalledWith({
        where: {
          clubId: 'club-2',
          isActive: true,
          events: { has: 'training.created' },
        },
        select: { id: true, url: true, secret: true },
      });

      expect(mockQueue.add).toHaveBeenCalledWith(
        'webhook:training.created',
        expect.objectContaining({
          event: 'training.created',
          subscriptionId: 'sub-3',
        }),
        expect.anything(),
      );
    });
  });

  describe('calculateBackoff', () => {
    it('should return 5000ms for first retry', () => {
      expect(calculateBackoff(1)).toBe(5000);
    });

    it('should return 30000ms for second retry', () => {
      expect(calculateBackoff(2)).toBe(30000);
    });

    it('should return 300000ms for third retry', () => {
      expect(calculateBackoff(3)).toBe(300000);
    });

    it('should return last delay for attempts beyond array length', () => {
      expect(calculateBackoff(4)).toBe(300000);
      expect(calculateBackoff(10)).toBe(300000);
    });
  });
});
