import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { PluggyWebhookService } from './pluggy-webhook.service';
import { PluggyWebhookEvent } from './schemas/pluggy-webhook-event.schema';

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

  const mockEvent = {
    _id: 'event_123',
    event: 'item/created',
    payload: { event: 'item/created', item: { id: 'item_123' } },
    receivedAt: new Date(),
    itemId: 'item_123',
    userId: 'user_123',
    save: jest.fn().mockResolvedValue(this),
  };

  const mockModel = jest.fn().mockImplementation(() => mockEvent) as any;
  mockModel.find = jest.fn();
  mockModel.sort = jest.fn();
  mockModel.limit = jest.fn();
  mockModel.exec = jest.fn();

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PluggyWebhookService,
        {
          provide: getModelToken(PluggyWebhookEvent.name),
          useValue: mockModel,
        },
      ],
    }).compile();

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

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('processWebhook', () => {
    it('should process valid webhook successfully', async () => {
      const webhookBody = {
        event: 'item/created',
        item: { id: 'item_123' },
        user: { id: 'user_123' },
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
    });

    it('should throw error for invalid body (null)', async () => {
      try {
        await service.processWebhook(null as any);
        fail('Expected error to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(Error);
        expect(error.message).toBe(
          'Invalid webhook payload: body must be an object',
        );
      }
    });

    it('should throw error for invalid body (not object)', async () => {
      try {
        await service.processWebhook('invalid' as any);
        fail('Expected error to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(Error);
        expect(error.message).toBe(
          'Invalid webhook payload: body must be an object',
        );
      }
    });

    it('should throw error for missing event field', async () => {
      const webhookBody = { item: { id: 'item_123' } };

      try {
        await service.processWebhook(webhookBody);
        fail('Expected error to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(Error);
        expect(error.message).toBe(
          'Invalid webhook payload: event field is required and must be a string',
        );
      }
    });

    it('should throw error for invalid event type', async () => {
      const webhookBody = { event: 'INVALID_EVENT' };

      try {
        await service.processWebhook(webhookBody);
        fail('Expected error to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(Error);
        expect(error.message).toBe(
          "Invalid webhook payload: unknown event type 'INVALID_EVENT'",
        );
      }
    });

    it('should process item/updated event', async () => {
      const webhookBody = {
        event: 'item/updated',
        item: { id: 'item_456' },
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
    });

    it('should process transactions/created event', async () => {
      const webhookBody = {
        event: 'transactions/created',
        transaction: { id: 'transaction_123' },
        account: { id: 'account_123' },
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
    });

    it('should accept event format "eventType:id" (ex: transactions/created:undefined)', async () => {
      const webhookBody = {
        event: 'transactions/created:undefined',
        transaction: { id: 'txn_123' },
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
      expect(mockModel).toHaveBeenCalledWith(
        expect.objectContaining({ event: 'transactions/created' }),
      );
    });

    it('should accept uppercase format (ITEM_CREATED) and normalize to item/created', async () => {
      const webhookBody = {
        event: 'ITEM_CREATED',
        item: { id: 'item_123' },
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
      expect(mockModel).toHaveBeenCalledWith(
        expect.objectContaining({ event: 'item/created' }),
      );
    });

    it('should accept item/login_succeeded with format eventType:id', async () => {
      const webhookBody = {
        event: 'item/login_succeeded:cc4ac697-5812-449b-84fd-93d8656c3af7',
        item: { id: 'cc4ac697-5812-449b-84fd-93d8656c3af7' },
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
      expect(mockModel).toHaveBeenCalledWith(
        expect.objectContaining({ event: 'item/login_succeeded' }),
      );
    });

    it('should accept item/updated with format eventType:id', async () => {
      const webhookBody = {
        event: 'item/updated:0b81679d-d521-4145-abdc-1af38804ded2',
        itemId: '0b81679d-d521-4145-abdc-1af38804ded2',
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
      expect(mockModel).toHaveBeenCalledWith(
        expect.objectContaining({ event: 'item/updated' }),
      );
    });

    it('should extract all event data fields', async () => {
      const webhookBody = {
        event: 'item/created',
        item: { id: 'item_123' },
        user: { id: 'user_123' },
        connector: { id: 'connector_123' },
        account: { id: 'account_123' },
        transaction: { id: 'transaction_123' },
        category: { id: 'category_123' },
        parameter: { id: 'parameter_123' },
        institution: { id: 'institution_123' },
        error: { code: 'ERROR_001' },
        status: 'SUCCESS',
        message: 'Item created successfully',
        details: { additional: 'info' },
      };

      mockEvent.save.mockResolvedValue(mockEvent);

      await service.processWebhook(webhookBody);

      expect(mockEvent.save).toHaveBeenCalled();
    });

    it('should handle save error', async () => {
      const webhookBody = {
        event: 'item/created',
        item: { id: 'item_123' },
      };

      const saveError = new Error('Database error');
      mockEvent.save.mockRejectedValue(saveError);

      try {
        await service.processWebhook(webhookBody);
        fail('Expected error to be thrown');
      } catch (error) {
        expect(error).toBe(saveError);
      }
    });
  });

  describe('getEventsByType', () => {
    it('should return events by type', async () => {
      const mockEvents = [mockEvent];
      const findChain = {
        sort: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        exec: jest.fn().mockResolvedValue(mockEvents),
      };
      mockModel.find.mockReturnValue(findChain);

      const result = await service.getEventsByType('item/created', 5);

      expect(mockModel.find).toHaveBeenCalledWith({ event: 'item/created' });
      expect(findChain.sort).toHaveBeenCalledWith({ receivedAt: -1 });
      expect(findChain.limit).toHaveBeenCalledWith(5);
      expect(result).toEqual(mockEvents);
    });

    it('should use default limit when not provided', async () => {
      const mockEvents = [mockEvent];
      const findChain = {
        sort: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        exec: jest.fn().mockResolvedValue(mockEvents),
      };
      mockModel.find.mockReturnValue(findChain);

      await service.getEventsByType('item/created');

      expect(findChain.limit).toHaveBeenCalledWith(10);
    });
  });

  describe('getEventsByItem', () => {
    it('should return events by item ID', async () => {
      const mockEvents = [mockEvent];
      const findChain = {
        sort: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        exec: jest.fn().mockResolvedValue(mockEvents),
      };
      mockModel.find.mockReturnValue(findChain);

      const result = await service.getEventsByItem('item_123', 3);

      expect(mockModel.find).toHaveBeenCalledWith({ itemId: 'item_123' });
      expect(findChain.sort).toHaveBeenCalledWith({ receivedAt: -1 });
      expect(findChain.limit).toHaveBeenCalledWith(3);
      expect(result).toEqual(mockEvents);
    });

    it('should use default limit when not provided', async () => {
      const mockEvents = [mockEvent];
      const findChain = {
        sort: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        exec: jest.fn().mockResolvedValue(mockEvents),
      };
      mockModel.find.mockReturnValue(findChain);

      await service.getEventsByItem('item_123');

      expect(findChain.limit).toHaveBeenCalledWith(10);
    });
  });
});
