import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { WebhooksService } from './webhooks.service';
import { PrismaService } from '../../prisma/prisma.service';

jest.mock('@prisma/client', () => ({
  PrismaClient: class PrismaClient {},
  Prisma: {},
}));

describe('WebhooksService', () => {
  let service: WebhooksService;

  const mockPrisma = {
    webhookSubscription: {
      findMany: jest.fn(),
      findFirst: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
      count: jest.fn(),
    },
    webhookLog: {
      findMany: jest.fn(),
      create: jest.fn(),
    },
  };

  const CLUB_ID = 'club-uuid-1';
  const SUB_ID = 'sub-uuid-1';
  const LOG_ID = 'log-uuid-1';

  const mockSubscription = {
    id: SUB_ID,
    clubId: CLUB_ID,
    url: 'https://example.com/webhook',
    events: ['client.created', 'sale.created'],
    secret: 'supersecretvalue',
    isActive: true,
    createdAt: new Date('2026-01-01T10:00:00Z'),
    updatedAt: new Date('2026-01-01T10:00:00Z'),
    _count: { logs: 5 },
  };

  const mockLog = {
    id: LOG_ID,
    clubId: CLUB_ID,
    subscriptionId: SUB_ID,
    event: 'test',
    payload: {},
    statusCode: 200,
    response: 'OK',
    attempt: 1,
    deliveredAt: new Date('2026-01-01T10:00:00Z'),
    createdAt: new Date('2026-01-01T10:00:00Z'),
  };

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

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

    service = module.get<WebhooksService>(WebhooksService);
  });

  // ─── findAll ──────────────────────────────────────────────────────────────

  describe('findAll', () => {
    it('returns paginated subscriptions with secret stripped', async () => {
      mockPrisma.webhookSubscription.findMany.mockResolvedValue([
        mockSubscription,
      ]);

      const result = await service.findAll(CLUB_ID, { limit: 20 });

      expect(mockPrisma.webhookSubscription.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { clubId: CLUB_ID },
          take: 21,
          orderBy: { createdAt: 'desc' },
          include: { _count: { select: { logs: true } } },
        }),
      );
      expect(result.data).toHaveLength(1);
      expect(result.data[0].secret).toBeUndefined();
      expect(result.hasMore).toBe(false);
      expect(result.nextCursor).toBeNull();
    });

    it('sets hasMore and nextCursor when result exceeds limit', async () => {
      const subs = Array.from({ length: 3 }, (_, i) => ({
        ...mockSubscription,
        id: `sub-${i}`,
      }));
      mockPrisma.webhookSubscription.findMany.mockResolvedValue(subs);

      const result = await service.findAll(CLUB_ID, { limit: 2 });

      expect(result.hasMore).toBe(true);
      expect(result.data).toHaveLength(2);
      expect(result.nextCursor).toBe('sub-1');
    });

    it('applies cursor when provided', async () => {
      mockPrisma.webhookSubscription.findMany.mockResolvedValue([]);

      await service.findAll(CLUB_ID, { cursor: 'prev-cursor', limit: 10 });

      expect(mockPrisma.webhookSubscription.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          cursor: { id: 'prev-cursor' },
          skip: 1,
        }),
      );
    });
  });

  // ─── findById ─────────────────────────────────────────────────────────────

  describe('findById', () => {
    it('returns subscription with secret stripped', async () => {
      mockPrisma.webhookSubscription.findFirst.mockResolvedValue(
        mockSubscription,
      );

      const result = await service.findById(CLUB_ID, SUB_ID);

      expect(mockPrisma.webhookSubscription.findFirst).toHaveBeenCalledWith({
        where: { id: SUB_ID, clubId: CLUB_ID },
        include: { _count: { select: { logs: true } } },
      });
      expect(result.secret).toBeUndefined();
      expect(result.id).toBe(SUB_ID);
    });

    it('throws NotFoundException when subscription is not found', async () => {
      mockPrisma.webhookSubscription.findFirst.mockResolvedValue(null);

      await expect(service.findById(CLUB_ID, 'bad-id')).rejects.toThrow(
        new NotFoundException('Webhook subscription with id bad-id not found'),
      );
    });
  });

  // ─── create ───────────────────────────────────────────────────────────────

  describe('create', () => {
    const createDto = {
      url: 'https://example.com/webhook',
      events: ['client.created'] as any[],
    };

    it('creates a subscription with a generated secret and returns it', async () => {
      const created = { ...mockSubscription };
      mockPrisma.webhookSubscription.create.mockResolvedValue(created);

      const result = await service.create(CLUB_ID, createDto);

      expect(mockPrisma.webhookSubscription.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          clubId: CLUB_ID,
          url: createDto.url,
          events: createDto.events,
          isActive: true,
          secret: expect.any(String),
        }),
      });
      // Secret must be a non-empty hex string generated by randomBytes(32)
      const calledData =
        mockPrisma.webhookSubscription.create.mock.calls[0][0].data;
      expect(calledData.secret).toBeTruthy();
      expect(calledData.secret.length).toBe(64); // 32 bytes → 64 hex chars
      expect(result).toEqual(created);
    });

    it('generates a unique secret for each subscription', async () => {
      mockPrisma.webhookSubscription.create.mockResolvedValue(mockSubscription);

      await service.create(CLUB_ID, createDto);
      await service.create(CLUB_ID, createDto);

      const secret1 =
        mockPrisma.webhookSubscription.create.mock.calls[0][0].data.secret;
      const secret2 =
        mockPrisma.webhookSubscription.create.mock.calls[1][0].data.secret;
      expect(secret1).not.toBe(secret2);
    });
  });

  // ─── update ───────────────────────────────────────────────────────────────

  describe('update', () => {
    it('updates subscription fields when it exists', async () => {
      mockPrisma.webhookSubscription.count.mockResolvedValue(1);
      const updated = { ...mockSubscription, isActive: false };
      mockPrisma.webhookSubscription.update.mockResolvedValue(updated);

      const result = await service.update(CLUB_ID, SUB_ID, { isActive: false });

      expect(mockPrisma.webhookSubscription.count).toHaveBeenCalledWith({
        where: { id: SUB_ID, clubId: CLUB_ID },
      });
      expect(mockPrisma.webhookSubscription.update).toHaveBeenCalledWith({
        where: { id: SUB_ID },
        data: { isActive: false },
      });
      expect(result.isActive).toBe(false);
    });

    it('throws NotFoundException when subscription does not exist', async () => {
      mockPrisma.webhookSubscription.count.mockResolvedValue(0);

      await expect(
        service.update(CLUB_ID, 'bad-id', { isActive: false }),
      ).rejects.toThrow(NotFoundException);
      expect(mockPrisma.webhookSubscription.update).not.toHaveBeenCalled();
    });
  });

  // ─── remove ───────────────────────────────────────────────────────────────

  describe('remove', () => {
    it('deletes subscription when it exists', async () => {
      mockPrisma.webhookSubscription.count.mockResolvedValue(1);
      mockPrisma.webhookSubscription.delete.mockResolvedValue(mockSubscription);

      await service.remove(CLUB_ID, SUB_ID);

      expect(mockPrisma.webhookSubscription.delete).toHaveBeenCalledWith({
        where: { id: SUB_ID },
      });
    });

    it('throws NotFoundException when subscription does not exist', async () => {
      mockPrisma.webhookSubscription.count.mockResolvedValue(0);

      await expect(service.remove(CLUB_ID, 'bad-id')).rejects.toThrow(
        NotFoundException,
      );
      expect(mockPrisma.webhookSubscription.delete).not.toHaveBeenCalled();
    });
  });

  // ─── findLogs ─────────────────────────────────────────────────────────────

  describe('findLogs', () => {
    it('returns paginated logs for a subscription', async () => {
      mockPrisma.webhookSubscription.count.mockResolvedValue(1);
      mockPrisma.webhookLog.findMany.mockResolvedValue([mockLog]);

      const result = await service.findLogs(CLUB_ID, SUB_ID, { limit: 20 });

      expect(mockPrisma.webhookLog.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { clubId: CLUB_ID, subscriptionId: SUB_ID },
          take: 21,
          orderBy: { createdAt: 'desc' },
        }),
      );
      expect(result.data).toHaveLength(1);
      expect(result.hasMore).toBe(false);
    });

    it('filters logs by event when provided', async () => {
      mockPrisma.webhookSubscription.count.mockResolvedValue(1);
      mockPrisma.webhookLog.findMany.mockResolvedValue([]);

      await service.findLogs(CLUB_ID, SUB_ID, {
        limit: 20,
        event: 'client.created',
      });

      expect(mockPrisma.webhookLog.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            clubId: CLUB_ID,
            subscriptionId: SUB_ID,
            event: 'client.created',
          },
        }),
      );
    });

    it('throws NotFoundException when subscription does not exist', async () => {
      mockPrisma.webhookSubscription.count.mockResolvedValue(0);

      await expect(
        service.findLogs(CLUB_ID, 'bad-sub-id', { limit: 20 }),
      ).rejects.toThrow(NotFoundException);
      expect(mockPrisma.webhookLog.findMany).not.toHaveBeenCalled();
    });
  });

  // ─── testDelivery ─────────────────────────────────────────────────────────

  describe('testDelivery', () => {
    beforeEach(() => {
      global.fetch = jest.fn();
    });

    it('delivers test payload and creates a log entry on success', async () => {
      mockPrisma.webhookSubscription.findFirst.mockResolvedValue(
        mockSubscription,
      );
      (global.fetch as jest.Mock).mockResolvedValue({
        status: 200,
        text: jest.fn().mockResolvedValue('OK'),
      });
      mockPrisma.webhookLog.create.mockResolvedValue(mockLog);

      const result = await service.testDelivery(CLUB_ID, SUB_ID);

      expect(global.fetch).toHaveBeenCalledWith(
        mockSubscription.url,
        expect.objectContaining({
          method: 'POST',
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
            'X-Webhook-Signature': expect.any(String),
          }),
        }),
      );
      expect(mockPrisma.webhookLog.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            clubId: CLUB_ID,
            subscriptionId: SUB_ID,
            event: 'test',
            statusCode: 200,
          }),
        }),
      );
      expect(result.success).toBe(true);
      expect(result.statusCode).toBe(200);
      expect(result.log).toEqual(mockLog);
    });

    it('marks delivery as failed and logs when fetch returns non-2xx status', async () => {
      mockPrisma.webhookSubscription.findFirst.mockResolvedValue(
        mockSubscription,
      );
      (global.fetch as jest.Mock).mockResolvedValue({
        status: 500,
        text: jest.fn().mockResolvedValue('Internal Server Error'),
      });
      const failedLog = { ...mockLog, statusCode: 500, deliveredAt: null };
      mockPrisma.webhookLog.create.mockResolvedValue(failedLog);

      const result = await service.testDelivery(CLUB_ID, SUB_ID);

      expect(result.success).toBe(false);
      expect(result.statusCode).toBe(500);
      expect(mockPrisma.webhookLog.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ statusCode: 500, deliveredAt: null }),
        }),
      );
    });

    it('handles network errors gracefully and still creates a log entry', async () => {
      mockPrisma.webhookSubscription.findFirst.mockResolvedValue(
        mockSubscription,
      );
      (global.fetch as jest.Mock).mockRejectedValue(
        new Error('Network failure'),
      );
      const errorLog = {
        ...mockLog,
        statusCode: null,
        response: 'Network failure',
        deliveredAt: null,
      };
      mockPrisma.webhookLog.create.mockResolvedValue(errorLog);

      const result = await service.testDelivery(CLUB_ID, SUB_ID);

      expect(result.success).toBe(false);
      expect(result.statusCode).toBeNull();
      expect(mockPrisma.webhookLog.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            statusCode: null,
            response: 'Network failure',
          }),
        }),
      );
    });

    it('throws NotFoundException when subscription does not exist', async () => {
      mockPrisma.webhookSubscription.findFirst.mockResolvedValue(null);

      await expect(service.testDelivery(CLUB_ID, 'bad-id')).rejects.toThrow(
        new NotFoundException(
          'Webhook subscription with id bad-id not found',
        ),
      );
      expect(global.fetch).not.toHaveBeenCalled();
    });
  });
});
