import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { NotFoundException } from '@nestjs/common';
import { Model } from 'mongoose';
import {
  PluggyEventsService,
  CreateEventDto,
  UpdateEventDto,
  EventFilters,
} from './pluggy-events.service';
import { PluggyWebhookEvent } from '../pluggy-webhook/schemas/pluggy-webhook-event.schema';

describe('PluggyEventsService', () => {
  let service: PluggyEventsService;
  let model: Model<PluggyWebhookEvent>;

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

  const mockModel = jest.fn().mockImplementation(() => mockEvent) as any;
  mockModel.find = jest.fn();
  mockModel.findById = jest.fn();
  mockModel.findByIdAndUpdate = jest.fn();
  mockModel.findByIdAndDelete = jest.fn();
  mockModel.countDocuments = jest.fn();
  mockModel.aggregate = jest.fn();

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

    service = module.get<PluggyEventsService>(PluggyEventsService);
    model = module.get<Model<PluggyWebhookEvent>>(
      getModelToken(PluggyWebhookEvent.name),
    );
  });

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

  describe('create', () => {
    it('should create a new event', async () => {
      const createEventDto: CreateEventDto = {
        event: 'ITEM_UPDATED',
        payload: { item: { id: 'item_123' } },
        itemId: 'item_123',
        userId: 'user_123',
      };

      const expectedEvent = {
        ...createEventDto,
        _id: 'event_123',
        receivedAt: expect.any(Date),
      };

      // Mock the constructor to return our mock event
      (model as any).mockImplementation = jest.fn().mockReturnValue(mockEvent);
      jest.spyOn(mockEvent, 'save').mockResolvedValue(expectedEvent as any);

      const result = await service.create(createEventDto);

      expect(mockEvent.save).toHaveBeenCalled();
      expect(mockEvent.save).toHaveBeenCalled();
      expect(result).toEqual(expectedEvent);
    });
  });

  describe('findAll', () => {
    it('should return all events with default filters', async () => {
      const mockEvents = [mockEvent];
      const mockCount = 1;

      jest.spyOn(model, 'find').mockReturnValue({
        sort: jest.fn().mockReturnValue({
          skip: jest.fn().mockReturnValue({
            limit: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue(mockEvents),
            }),
          }),
        }),
      } as any);

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockCount),
      } as any);

      const result = await service.findAll();

      expect(result).toEqual({
        events: mockEvents,
        total: mockCount,
        limit: 10,
        offset: 0,
      });
    });

    it('should return events with custom filters', async () => {
      const filters: EventFilters = {
        event: 'ITEM_UPDATED',
        itemId: 'item_123',
        limit: 5,
        offset: 10,
      };

      const mockEvents = [mockEvent];
      const mockCount = 1;

      jest.spyOn(model, 'find').mockReturnValue({
        sort: jest.fn().mockReturnValue({
          skip: jest.fn().mockReturnValue({
            limit: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue(mockEvents),
            }),
          }),
        }),
      } as any);

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockCount),
      } as any);

      const result = await service.findAll(filters);

      expect(result).toEqual({
        events: mockEvents,
        total: mockCount,
        limit: 5,
        offset: 10,
      });
    });

    it('should handle date filters', async () => {
      const startDate = new Date('2024-01-01');
      const endDate = new Date('2024-01-31');
      const filters: EventFilters = {
        startDate,
        endDate,
      };

      const mockEvents = [mockEvent];
      const mockCount = 1;

      jest.spyOn(model, 'find').mockReturnValue({
        sort: jest.fn().mockReturnValue({
          skip: jest.fn().mockReturnValue({
            limit: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue(mockEvents),
            }),
          }),
        }),
      } as any);

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockCount),
      } as any);

      const result = await service.findAll(filters);

      expect(result).toEqual({
        events: mockEvents,
        total: mockCount,
        limit: 10,
        offset: 0,
      });
    });
  });

  describe('findOne', () => {
    it('should return an event by id', async () => {
      const id = 'event_123';

      jest.spyOn(model, 'findById').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockEvent),
      } as any);

      const result = await service.findOne(id);

      expect(model.findById).toHaveBeenCalledWith(id);
      expect(result).toEqual(mockEvent);
    });

    it('should throw NotFoundException for non-existent event', async () => {
      const id = 'non-existent';

      jest.spyOn(model, 'findById').mockReturnValue({
        exec: jest.fn().mockResolvedValue(null),
      } as any);

      await expect(service.findOne(id)).rejects.toThrow(NotFoundException);
      await expect(service.findOne(id)).rejects.toThrow(
        `Evento com ID ${id} não encontrado`,
      );
    });
  });

  describe('update', () => {
    it('should update an event', async () => {
      const id = 'event_123';
      const updateEventDto: UpdateEventDto = {
        status: 'processed',
        message: 'Event processed successfully',
      };

      const updatedEvent = { ...mockEvent, ...updateEventDto };

      jest.spyOn(model, 'findByIdAndUpdate').mockReturnValue({
        exec: jest.fn().mockResolvedValue(updatedEvent),
      } as any);

      const result = await service.update(id, updateEventDto);

      expect(model.findByIdAndUpdate).toHaveBeenCalledWith(id, updateEventDto, {
        new: true,
      });
      expect(result).toEqual(updatedEvent);
    });

    it('should throw NotFoundException for non-existent event', async () => {
      const id = 'non-existent';
      const updateEventDto: UpdateEventDto = {
        status: 'processed',
      };

      jest.spyOn(model, 'findByIdAndUpdate').mockReturnValue({
        exec: jest.fn().mockResolvedValue(null),
      } as any);

      await expect(service.update(id, updateEventDto)).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  describe('remove', () => {
    it('should remove an event', async () => {
      const id = 'event_123';

      jest.spyOn(model, 'findByIdAndDelete').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockEvent),
      } as any);

      const result = await service.remove(id);

      expect(model.findByIdAndDelete).toHaveBeenCalledWith(id);
      expect(result).toEqual(mockEvent);
    });

    it('should throw NotFoundException for non-existent event', async () => {
      const id = 'non-existent';

      jest.spyOn(model, 'findByIdAndDelete').mockReturnValue({
        exec: jest.fn().mockResolvedValue(null),
      } as any);

      await expect(service.remove(id)).rejects.toThrow(NotFoundException);
    });
  });

  describe('getEventsByType', () => {
    it('should return events by type', async () => {
      const eventType = 'ITEM_UPDATED';
      const limit = 5;
      const offset = 10;

      const mockEvents = [mockEvent];
      const mockCount = 1;

      jest.spyOn(model, 'find').mockReturnValue({
        sort: jest.fn().mockReturnValue({
          skip: jest.fn().mockReturnValue({
            limit: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue(mockEvents),
            }),
          }),
        }),
      } as any);

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockCount),
      } as any);

      const result = await service.getEventsByType(eventType, limit, offset);

      expect(result).toEqual({
        events: mockEvents,
        total: mockCount,
        limit: 5,
        offset: 10,
      });
    });
  });

  describe('getEventsByItem', () => {
    it('should return events by item', async () => {
      const itemId = 'item_123';
      const limit = 5;
      const offset = 10;

      const mockEvents = [mockEvent];
      const mockCount = 1;

      jest.spyOn(model, 'find').mockReturnValue({
        sort: jest.fn().mockReturnValue({
          skip: jest.fn().mockReturnValue({
            limit: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue(mockEvents),
            }),
          }),
        }),
      } as any);

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockCount),
      } as any);

      const result = await service.getEventsByItem(itemId, limit, offset);

      expect(result).toEqual({
        events: mockEvents,
        total: mockCount,
        limit: 5,
        offset: 10,
      });
    });
  });

  describe('getEventsByUser', () => {
    it('should return events by user', async () => {
      const userId = 'user_123';
      const limit = 5;
      const offset = 10;

      const mockEvents = [mockEvent];
      const mockCount = 1;

      jest.spyOn(model, 'find').mockReturnValue({
        sort: jest.fn().mockReturnValue({
          skip: jest.fn().mockReturnValue({
            limit: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue(mockEvents),
            }),
          }),
        }),
      } as any);

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockCount),
      } as any);

      const result = await service.getEventsByUser(userId, limit, offset);

      expect(result).toEqual({
        events: mockEvents,
        total: mockCount,
        limit: 5,
        offset: 10,
      });
    });
  });

  describe('getEventStats', () => {
    it('should return event statistics', async () => {
      const filters: EventFilters = {
        event: 'ITEM_UPDATED',
        startDate: new Date('2024-01-01'),
        endDate: new Date('2024-01-31'),
      };

      const mockTotalEvents = 100;
      const mockEventsByType = [
        { event: 'ITEM_UPDATED', count: 50 },
        { event: 'ITEM_CREATED', count: 30 },
        { event: 'ITEM_DELETED', count: 20 },
      ];
      const mockRecentEvents = 10;

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockTotalEvents),
      } as any);

      jest.spyOn(model, 'aggregate').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockEventsByType),
      } as any);

      // Mock the second countDocuments call for recent events
      jest
        .spyOn(model, 'countDocuments')
        .mockReturnValueOnce({
          exec: jest.fn().mockResolvedValue(mockTotalEvents),
        } as any)
        .mockReturnValueOnce({
          exec: jest.fn().mockResolvedValue(mockRecentEvents),
        } as any);

      const result = await service.getEventStats(filters);

      expect(result).toEqual({
        totalEvents: mockTotalEvents,
        eventsByType: mockEventsByType,
        recentEvents: mockRecentEvents,
        period: {
          startDate: filters.startDate,
          endDate: filters.endDate,
        },
      });
    });

    it('should return event statistics with default period', async () => {
      const mockTotalEvents = 50;
      const mockEventsByType = [
        { event: 'ITEM_UPDATED', count: 30 },
        { event: 'ITEM_CREATED', count: 20 },
      ];
      const mockRecentEvents = 5;

      jest.spyOn(model, 'countDocuments').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockTotalEvents),
      } as any);

      jest.spyOn(model, 'aggregate').mockReturnValue({
        exec: jest.fn().mockResolvedValue(mockEventsByType),
      } as any);

      // Mock multiple countDocuments calls
      jest
        .spyOn(model, 'countDocuments')
        .mockReturnValueOnce({
          exec: jest.fn().mockResolvedValue(mockTotalEvents),
        } as any)
        .mockReturnValueOnce({
          exec: jest.fn().mockResolvedValue(mockRecentEvents),
        } as any);

      const result = await service.getEventStats();

      expect(result).toHaveProperty('totalEvents', mockTotalEvents);
      expect(result).toHaveProperty('eventsByType', mockEventsByType);
      expect(result).toHaveProperty('recentEvents', mockRecentEvents);
      expect(result).toHaveProperty('period');
      expect(result.period).toHaveProperty('startDate');
      expect(result.period).toHaveProperty('endDate');
    });
  });
});
