import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Job } from 'bullmq';
import { EventsProcessor } from './events.processor';
import { AggregationService } from './aggregation.service';

describe('EventsProcessor', () => {
  let processor: EventsProcessor;
  let mockAggregationService: {
    processEvent: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    mockAggregationService = {
      processEvent: vi.fn().mockResolvedValue(undefined),
    };

    // Manually create instance with mocked dependency
    processor = new EventsProcessor(mockAggregationService as any);
  });

  describe('process', () => {
    it('should process pageView event successfully', async () => {
      const mockJob: Job = {
        id: 'job-123',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-abc',
          properties: {
            path: '/home',
            title: 'Home Page',
          },
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledTimes(1);
      expect(mockAggregationService.processEvent).toHaveBeenCalledWith({
        eventType: 'pageView',
        timestamp: new Date('2026-01-29T12:00:00Z'),
        sessionId: 'session-abc',
        properties: {
          path: '/home',
          title: 'Home Page',
        },
      });
    });

    it('should process session_start event successfully', async () => {
      const mockJob: Job = {
        id: 'job-456',
        data: {
          eventType: 'session_start',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-xyz',
          properties: {
            userId: 'user-123',
            trafficSource: 'google',
            deviceType: 'desktop',
          },
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledWith({
        eventType: 'session_start',
        timestamp: new Date('2026-01-29T12:00:00Z'),
        sessionId: 'session-xyz',
        properties: {
          userId: 'user-123',
          trafficSource: 'google',
          deviceType: 'desktop',
        },
      });
    });

    it('should process conversion event successfully', async () => {
      const mockJob: Job = {
        id: 'job-789',
        data: {
          eventType: 'purchase',
          timestamp: '2026-01-29T12:30:00Z',
          sessionId: 'session-abc',
          properties: {
            revenue: 99.99,
            currency: 'USD',
            conversionType: 'product_purchase',
          },
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledWith({
        eventType: 'purchase',
        timestamp: new Date('2026-01-29T12:30:00Z'),
        sessionId: 'session-abc',
        properties: {
          revenue: 99.99,
          currency: 'USD',
          conversionType: 'product_purchase',
        },
      });
    });

    it('should convert timestamp string to Date object', async () => {
      const timestamp = '2026-01-29T15:45:30.123Z';
      const mockJob: Job = {
        id: 'job-time',
        data: {
          eventType: 'pageView',
          timestamp,
          sessionId: 'session-123',
          properties: {},
        },
      } as Job;

      await processor.process(mockJob);

      const callArg = mockAggregationService.processEvent.mock.calls[0][0];
      expect(callArg.timestamp).toBeInstanceOf(Date);
      expect(callArg.timestamp.toISOString()).toBe(timestamp);
    });

    it('should throw error when aggregation service fails', async () => {
      const error = new Error('Database connection failed');
      mockAggregationService.processEvent.mockRejectedValueOnce(error);

      const mockJob: Job = {
        id: 'job-fail',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-fail',
          properties: {},
        },
      } as Job;

      await expect(processor.process(mockJob)).rejects.toThrow('Database connection failed');
      expect(mockAggregationService.processEvent).toHaveBeenCalledTimes(1);
    });

    it('should handle events with empty properties', async () => {
      const mockJob: Job = {
        id: 'job-empty',
        data: {
          eventType: 'custom_event',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-empty',
          properties: {},
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledWith({
        eventType: 'custom_event',
        timestamp: new Date('2026-01-29T12:00:00Z'),
        sessionId: 'session-empty',
        properties: {},
      });
    });

    it('should handle events with complex nested properties', async () => {
      const mockJob: Job = {
        id: 'job-nested',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-nested',
          properties: {
            user: {
              id: 'user-123',
              preferences: {
                theme: 'dark',
              },
            },
            metadata: {
              tags: ['tag1', 'tag2'],
            },
          },
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledWith({
        eventType: 'pageView',
        timestamp: new Date('2026-01-29T12:00:00Z'),
        sessionId: 'session-nested',
        properties: {
          user: {
            id: 'user-123',
            preferences: {
              theme: 'dark',
            },
          },
          metadata: {
            tags: ['tag1', 'tag2'],
          },
        },
      });
    });
  });

  describe('onCompleted', () => {
    it('should log completion message', () => {
      const mockJob: Job = {
        id: 'job-complete',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-abc',
          properties: {},
        },
      } as Job;

      processor.onCompleted(mockJob);
      // Logger is tested through integration - just verify method exists and doesn't throw
      expect(true).toBe(true);
    });

    it('should handle different event types in completion', () => {
      const mockJobs = [
        { id: 'job-1', data: { eventType: 'pageView', timestamp: '', sessionId: '', properties: {} } },
        { id: 'job-2', data: { eventType: 'session_start', timestamp: '', sessionId: '', properties: {} } },
        { id: 'job-3', data: { eventType: 'purchase', timestamp: '', sessionId: '', properties: {} } },
      ];

      mockJobs.forEach((job) => {
        processor.onCompleted(job as Job);
      });

      expect(true).toBe(true);
    });
  });

  describe('onFailed', () => {
    it('should log failure with error message', () => {
      const mockJob: Job = {
        id: 'job-failed',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-fail',
          properties: {},
        },
      } as Job;

      const error = new Error('Processing failed');
      processor.onFailed(mockJob, error);
      // Logger is tested through integration - just verify method exists and doesn't throw
      expect(true).toBe(true);
    });

    it('should handle different error types', () => {
      const mockJob: Job = {
        id: 'job-error',
        data: {
          eventType: 'session_start',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-error',
          properties: {},
        },
      } as Job;

      const errors = [
        new Error('Network error'),
        new TypeError('Invalid type'),
        new RangeError('Value out of range'),
      ];

      errors.forEach((error) => {
        processor.onFailed(mockJob, error);
      });

      expect(true).toBe(true);
    });
  });

  describe('retry behavior', () => {
    it('should propagate errors for retry handling', async () => {
      const retryableError = new Error('Temporary database issue');
      mockAggregationService.processEvent.mockRejectedValueOnce(retryableError);

      const mockJob: Job = {
        id: 'job-retry',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-retry',
          properties: {},
        },
      } as Job;

      await expect(processor.process(mockJob)).rejects.toThrow(retryableError);
    });

    it('should allow multiple retry attempts', async () => {
      mockAggregationService.processEvent
        .mockRejectedValueOnce(new Error('First attempt failed'))
        .mockRejectedValueOnce(new Error('Second attempt failed'))
        .mockResolvedValueOnce(undefined);

      const mockJob: Job = {
        id: 'job-multi-retry',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-retry',
          properties: {},
        },
      } as Job;

      // First attempt
      await expect(processor.process(mockJob)).rejects.toThrow('First attempt failed');

      // Second attempt
      await expect(processor.process(mockJob)).rejects.toThrow('Second attempt failed');

      // Third attempt succeeds
      await expect(processor.process(mockJob)).resolves.toBeUndefined();

      expect(mockAggregationService.processEvent).toHaveBeenCalledTimes(3);
    });
  });

  describe('edge cases', () => {
    it('should handle very long session IDs', async () => {
      const longSessionId = 'session-' + 'x'.repeat(1000);
      const mockJob: Job = {
        id: 'job-long',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: longSessionId,
          properties: {},
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          sessionId: longSessionId,
        }),
      );
    });

    it('should handle special characters in event data', async () => {
      const mockJob: Job = {
        id: 'job-special',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-特殊文字-🎉',
          properties: {
            path: '/path/with spaces/and-dashes',
            query: '?foo=bar&baz=qux',
            fragment: '#section-1',
          },
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          sessionId: 'session-特殊文字-🎉',
          properties: {
            path: '/path/with spaces/and-dashes',
            query: '?foo=bar&baz=qux',
            fragment: '#section-1',
          },
        }),
      );
    });

    it('should handle events with null/undefined property values', async () => {
      const mockJob: Job = {
        id: 'job-null',
        data: {
          eventType: 'pageView',
          timestamp: '2026-01-29T12:00:00Z',
          sessionId: 'session-null',
          properties: {
            userId: null,
            deviceType: undefined,
            country: 'US',
          },
        },
      } as Job;

      await processor.process(mockJob);

      expect(mockAggregationService.processEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: {
            userId: null,
            deviceType: undefined,
            country: 'US',
          },
        }),
      );
    });
  });
});
