import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { getQueueToken } from '@nestjs/bullmq';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { Request } from 'express';

import { TrackingService } from './tracking.service';
import { DeviceEnrichmentService } from './device-enrichment.service';
import { AttributionService } from './attribution.service';
import { RawEvent } from '../entities/raw-event.entity';
import { SessionFingerprint } from '../entities/session-fingerprint.entity';
import { createMockRepository, createMockQueue, type MockRepository, type MockQueue } from '@test/mocks';

describe('TrackingService', () => {
  let service: TrackingService;
  let deviceEnrichmentService: DeviceEnrichmentService;
  let attributionService: AttributionService;
  let rawEventRepository: MockRepository<RawEvent>;
  let fingerprintRepository: MockRepository<SessionFingerprint>;
  let eventsQueue: MockQueue;

  beforeEach(async () => {
    rawEventRepository = createMockRepository<RawEvent>();
    fingerprintRepository = createMockRepository<SessionFingerprint>();
    eventsQueue = createMockQueue();

    const module = await Test.createTestingModule({
      providers: [
        TrackingService,
        DeviceEnrichmentService,
        AttributionService,
        { provide: getRepositoryToken(RawEvent), useValue: rawEventRepository },
        { provide: getRepositoryToken(SessionFingerprint), useValue: fingerprintRepository },
        { provide: getQueueToken('analytics-events'), useValue: eventsQueue },
      ],
    }).compile();

    service = module.get<TrackingService>(TrackingService);
    deviceEnrichmentService = module.get<DeviceEnrichmentService>(DeviceEnrichmentService);
    attributionService = module.get<AttributionService>(AttributionService);
  });

  describe('trackView', () => {
    it('creates pageview event with enriched device data', async () => {
      const mockRequest = {
        headers: {
          'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0',
          'x-forwarded-for': '192.168.1.1',
        },
        socket: {},
      } as unknown as Request;

      const mockEvent = {
        id: 'event-123',
        eventType: 'pageview',
        sessionId: 'session-abc',
        pageUrl: 'https://example.com/page',
        processed: false,
      };

      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);
      fingerprintRepository.findOne.mockResolvedValue(null);

      const result = await service.trackView(
        {
          pageUrl: 'https://example.com/page',
          sessionId: 'session-abc',
          referrer: 'https://google.com',
          userId: 'user-123',
          clientDevice: {
            screenWidth: 1920,
            screenHeight: 1080,
            viewportWidth: 1440,
            viewportHeight: 900,
          },
          attribution: {
            utmSource: 'google',
            utmMedium: 'cpc',
            utmCampaign: 'winter-sale',
          },
        },
        mockRequest,
      );

      expect(result.success).toBe(true);
      expect(result.eventId).toBe('event-123');
      expect(rawEventRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: 'pageview',
          sessionId: 'session-abc',
          userId: 'user-123',
          pageUrl: 'https://example.com/page',
          referrer: 'https://google.com',
          processed: false,
        }),
      );
      expect(rawEventRepository.save).toHaveBeenCalledWith(mockEvent);
      expect(eventsQueue.add).toHaveBeenCalledWith('process-event', {
        eventId: 'event-123',
        eventType: 'pageview',
        sessionId: 'session-abc',
      });
    });

    it('creates session fingerprint on first pageview', async () => {
      const mockRequest = {
        headers: {
          'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) Mobile/15E148 Safari/604.1',
        },
        socket: { remoteAddress: '10.0.0.1' },
      } as unknown as Request;

      const mockEvent = { id: 'event-456', eventType: 'pageview', sessionId: 'session-new' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);
      fingerprintRepository.findOne.mockResolvedValue(null);

      await service.trackView(
        {
          pageUrl: 'https://example.com/landing',
          sessionId: 'session-new',
          attribution: {
            utmSource: 'facebook',
            utmMedium: 'social',
          },
        },
        mockRequest,
      );

      expect(fingerprintRepository.findOne).toHaveBeenCalledWith({
        where: { sessionId: 'session-new' },
      });
      expect(fingerprintRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          sessionId: 'session-new',
          deviceType: 'mobile',
          browser: 'Safari',
          os: 'iOS',
          trafficSource: 'social',
          utmSource: 'facebook',
          utmMedium: 'social',
          landingPage: 'https://example.com/landing',
        }),
      );
      expect(fingerprintRepository.save).toHaveBeenCalled();
    });

    it('updates existing session fingerprint with viewport and userId', async () => {
      const mockRequest = {
        headers: { 'user-agent': 'Chrome' },
        socket: {},
      } as unknown as Request;

      const existingFingerprint = {
        id: 'fp-123',
        sessionId: 'session-existing',
        userId: null,
        viewportWidth: 1024,
        viewportHeight: 768,
        trafficSource: 'direct',
      };

      const mockEvent = { id: 'event-789' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);
      fingerprintRepository.findOne.mockResolvedValue(existingFingerprint as SessionFingerprint);

      await service.trackView(
        {
          pageUrl: 'https://example.com/dashboard',
          sessionId: 'session-existing',
          userId: 'user-456',
          clientDevice: {
            viewportWidth: 1440,
            viewportHeight: 900,
          },
        },
        mockRequest,
      );

      expect(fingerprintRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          sessionId: 'session-existing',
          userId: 'user-456',
          viewportWidth: 1440,
          viewportHeight: 900,
        }),
      );
    });

    it('handles errors gracefully and returns failure result', async () => {
      const mockRequest = {
        headers: {},
        socket: {},
      } as unknown as Request;

      rawEventRepository.create.mockImplementation(() => {
        throw new Error('Database connection failed');
      });

      const result = await service.trackView(
        {
          pageUrl: 'https://example.com/error',
          sessionId: 'session-error',
        },
        mockRequest,
      );

      expect(result.success).toBe(false);
      expect(result.eventId).toBeNull();
      expect(result.error).toBe('Database connection failed');
    });

    it('extracts IP from x-forwarded-for header', async () => {
      const mockRequest = {
        headers: {
          'x-forwarded-for': '203.0.113.1, 192.168.1.1, 10.0.0.1',
        },
        socket: { remoteAddress: '127.0.0.1' },
      } as unknown as Request;

      const enrichSpy = vi.spyOn(deviceEnrichmentService, 'enrich');

      const mockEvent = { id: 'event-ip' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);
      fingerprintRepository.findOne.mockResolvedValue(null);

      await service.trackView(
        {
          pageUrl: 'https://example.com',
          sessionId: 'session-ip',
        },
        mockRequest,
      );

      expect(enrichSpy).toHaveBeenCalledWith(
        undefined,
        '203.0.113.1',
        undefined,
      );
    });

    it('extracts IP from x-real-ip header', async () => {
      const mockRequest = {
        headers: {
          'x-real-ip': '198.51.100.5',
        },
        socket: {},
      } as unknown as Request;

      const enrichSpy = vi.spyOn(deviceEnrichmentService, 'enrich');

      const mockEvent = { id: 'event-real-ip' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);
      fingerprintRepository.findOne.mockResolvedValue(null);

      await service.trackView(
        {
          pageUrl: 'https://example.com',
          sessionId: 'session-real-ip',
        },
        mockRequest,
      );

      expect(enrichSpy).toHaveBeenCalledWith(
        undefined,
        '198.51.100.5',
        undefined,
      );
    });
  });

  describe('trackEvent', () => {
    it('creates generic event with required fields', async () => {
      const mockEvent = {
        id: 'event-click-123',
        eventType: 'click',
        sessionId: 'session-xyz',
        userId: 'user-789',
        pageUrl: 'https://example.com/page',
      };

      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);

      const result = await service.trackEvent({
        eventType: 'click',
        sessionId: 'session-xyz',
        userId: 'user-789',
        pageUrl: 'https://example.com/page',
        metadata: {
          elementId: 'cta-button',
          elementText: 'Sign Up',
        },
      });

      expect(result.success).toBe(true);
      expect(result.eventId).toBe('event-click-123');
      expect(rawEventRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: 'click',
          sessionId: 'session-xyz',
          userId: 'user-789',
          pageUrl: 'https://example.com/page',
          metadata: {
            elementId: 'cta-button',
            elementText: 'Sign Up',
          },
          processed: false,
        }),
      );
      expect(eventsQueue.add).toHaveBeenCalledWith('process-event', {
        eventId: 'event-click-123',
        eventType: 'click',
        sessionId: 'session-xyz',
      });
    });

    it('handles optional fields correctly', async () => {
      const mockEvent = { id: 'event-minimal' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);

      const result = await service.trackEvent({
        eventType: 'custom_event',
        sessionId: 'session-minimal',
      });

      expect(result.success).toBe(true);
      expect(rawEventRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: 'custom_event',
          sessionId: 'session-minimal',
          userId: null,
          pageUrl: null,
          metadata: null,
          processed: false,
        }),
      );
    });

    it('uses custom timestamp if provided', async () => {
      const customTimestamp = Date.now() - 60000; // 1 minute ago
      const mockEvent = { id: 'event-timestamp' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);

      await service.trackEvent({
        eventType: 'scroll',
        sessionId: 'session-ts',
        timestamp: customTimestamp,
      });

      expect(rawEventRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          timestamp: new Date(customTimestamp),
        }),
      );
    });

    it('handles errors and returns failure result', async () => {
      rawEventRepository.save.mockRejectedValue(new Error('Save failed'));

      const result = await service.trackEvent({
        eventType: 'error_event',
        sessionId: 'session-err',
      });

      expect(result.success).toBe(false);
      expect(result.eventId).toBeNull();
      expect(result.error).toBe('Save failed');
    });
  });

  describe('trackBatch', () => {
    it('processes multiple events successfully', async () => {
      const mockEvents = [
        { id: 'event-1', eventType: 'click', sessionId: 'session-1' },
        { id: 'event-2', eventType: 'scroll', sessionId: 'session-1' },
        { id: 'event-3', eventType: 'submit', sessionId: 'session-1' },
      ];

      rawEventRepository.create
        .mockReturnValueOnce(mockEvents[0] as RawEvent)
        .mockReturnValueOnce(mockEvents[1] as RawEvent)
        .mockReturnValueOnce(mockEvents[2] as RawEvent);

      rawEventRepository.save
        .mockResolvedValueOnce(mockEvents[0] as RawEvent)
        .mockResolvedValueOnce(mockEvents[1] as RawEvent)
        .mockResolvedValueOnce(mockEvents[2] as RawEvent);

      const result = await service.trackBatch([
        { eventType: 'click', sessionId: 'session-1', metadata: { target: 'button-1' } },
        { eventType: 'scroll', sessionId: 'session-1', metadata: { depth: 50 } },
        { eventType: 'submit', sessionId: 'session-1', metadata: { formId: 'contact' } },
      ]);

      expect(result.success).toBe(true);
      expect(result.count).toBe(3);
      expect(result.errors).toHaveLength(0);
      expect(eventsQueue.add).toHaveBeenCalledTimes(3);
    });

    it('handles partial failures in batch', async () => {
      const mockEvent1 = { id: 'event-success' };
      const mockEvent3 = { id: 'event-success-2' };

      rawEventRepository.create
        .mockReturnValueOnce(mockEvent1 as RawEvent)
        .mockImplementationOnce(() => {
          throw new Error('Invalid data');
        })
        .mockReturnValueOnce(mockEvent3 as RawEvent);

      rawEventRepository.save
        .mockResolvedValueOnce(mockEvent1 as RawEvent)
        .mockResolvedValueOnce(mockEvent3 as RawEvent);

      const result = await service.trackBatch([
        { eventType: 'event1', sessionId: 'session-1' },
        { eventType: 'event2', sessionId: 'session-2' },
        { eventType: 'event3', sessionId: 'session-3' },
      ]);

      expect(result.success).toBe(false);
      expect(result.count).toBe(2);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toBe('Invalid data');
    });

    it('handles empty batch', async () => {
      const result = await service.trackBatch([]);

      expect(result.success).toBe(true);
      expect(result.count).toBe(0);
      expect(result.errors).toHaveLength(0);
    });
  });

  describe('trackConversion', () => {
    it('creates conversion event with value and currency', async () => {
      const mockEvent = {
        id: 'conversion-123',
        eventType: 'conversion',
        sessionId: 'session-conv',
        userId: 'user-123',
        metadata: {
          conversionType: 'purchase',
          value: 99.99,
          currency: 'USD',
          productId: 'prod-456',
        },
      };

      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);

      const result = await service.trackConversion({
        conversionType: 'purchase',
        sessionId: 'session-conv',
        userId: 'user-123',
        value: 99.99,
        currency: 'USD',
        metadata: {
          productId: 'prod-456',
        },
      });

      expect(result.success).toBe(true);
      expect(result.eventId).toBe('conversion-123');
      expect(rawEventRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: 'conversion',
          sessionId: 'session-conv',
          userId: 'user-123',
          metadata: {
            conversionType: 'purchase',
            value: 99.99,
            currency: 'USD',
            productId: 'prod-456',
          },
          processed: false,
        }),
      );
    });

    it('queues conversion event with high priority', async () => {
      const mockEvent = { id: 'conversion-priority' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);

      await service.trackConversion({
        conversionType: 'signup',
        sessionId: 'session-signup',
        userId: 'user-new',
      });

      expect(eventsQueue.add).toHaveBeenCalledWith(
        'process-event',
        {
          eventId: 'conversion-priority',
          eventType: 'conversion',
          sessionId: 'session-signup',
          conversionType: 'signup',
        },
        { priority: 1 },
      );
    });

    it('handles conversion without value or currency', async () => {
      const mockEvent = { id: 'conversion-no-value' };
      rawEventRepository.create.mockReturnValue(mockEvent as RawEvent);
      rawEventRepository.save.mockResolvedValue(mockEvent as RawEvent);

      const result = await service.trackConversion({
        conversionType: 'newsletter_signup',
        sessionId: 'session-newsletter',
      });

      expect(result.success).toBe(true);
      expect(rawEventRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          metadata: expect.objectContaining({
            conversionType: 'newsletter_signup',
            value: undefined,
            currency: undefined,
          }),
        }),
      );
    });

    it('handles errors gracefully', async () => {
      rawEventRepository.create.mockImplementation(() => {
        throw new Error('Conversion tracking failed');
      });

      const result = await service.trackConversion({
        conversionType: 'error_conversion',
        sessionId: 'session-error',
      });

      expect(result.success).toBe(false);
      expect(result.eventId).toBeNull();
      expect(result.error).toBe('Conversion tracking failed');
    });
  });
});
