import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { Test } from '@nestjs/testing';
import type { Server, Socket } from 'socket.io';
import { AnalyticsGateway } from './analytics.gateway';
import { MetricsService } from '../metrics/metrics.service';

interface MockSocket {
  id: string;
  emit: ReturnType<typeof vi.fn>;
  on: ReturnType<typeof vi.fn>;
  disconnect: ReturnType<typeof vi.fn>;
}

interface MockServer {
  emit: ReturnType<typeof vi.fn>;
  on: ReturnType<typeof vi.fn>;
}

function createMockSocket(id: string = 'socket-123'): MockSocket {
  return {
    id,
    emit: vi.fn(),
    on: vi.fn(),
    disconnect: vi.fn(),
  };
}

function createMockServer(): MockServer {
  return {
    emit: vi.fn(),
    on: vi.fn(),
  };
}

describe('AnalyticsGateway', () => {
  let gateway: AnalyticsGateway;
  let mockMetricsService: {
    getRealtimeMetrics: ReturnType<typeof vi.fn>;
    getActiveUsers: ReturnType<typeof vi.fn>;
    getCurrentPageViews: ReturnType<typeof vi.fn>;
  };

  beforeEach(async () => {
    mockMetricsService = {
      getRealtimeMetrics: vi.fn(),
      getActiveUsers: vi.fn(),
      getCurrentPageViews: vi.fn(),
    };

    const module = await Test.createTestingModule({
      providers: [
        AnalyticsGateway,
        { provide: MetricsService, useValue: mockMetricsService },
      ],
    }).compile();

    gateway = module.get<AnalyticsGateway>(AnalyticsGateway);
    gateway.server = createMockServer() as unknown as Server;
  });

  afterEach(() => {
    vi.clearAllTimers();
  });

  describe('handleConnection', () => {
    it('logs client connection', () => {
      const mockSocket = createMockSocket('client-001');
      const logSpy = vi.spyOn(gateway['logger'], 'log');

      gateway.handleConnection(mockSocket as unknown as Socket);

      expect(logSpy).toHaveBeenCalledWith('Client connected: client-001');
    });

    it('handles multiple simultaneous connections', () => {
      const socket1 = createMockSocket('client-001');
      const socket2 = createMockSocket('client-002');
      const socket3 = createMockSocket('client-003');

      gateway.handleConnection(socket1 as unknown as Socket);
      gateway.handleConnection(socket2 as unknown as Socket);
      gateway.handleConnection(socket3 as unknown as Socket);

      expect(gateway['logger'].log).toHaveBeenCalledTimes(3);
    });
  });

  describe('handleDisconnect', () => {
    it('logs client disconnection and clears subscriptions', () => {
      const mockSocket = createMockSocket('client-001');
      const logSpy = vi.spyOn(gateway['logger'], 'log');
      const clearSpy = vi.spyOn(gateway as any, 'clearSubscriptions');

      gateway.handleDisconnect(mockSocket as unknown as Socket);

      expect(logSpy).toHaveBeenCalledWith('Client disconnected: client-001');
      expect(clearSpy).toHaveBeenCalledWith('client-001');
    });

    it('clears active subscription timer on disconnect', () => {
      vi.useFakeTimers();
      const mockSocket = createMockSocket('client-001');
      const mockMetrics = [
        { metric: 'page_views', value: 100, change: 5, changePercent: 5, timestamp: new Date() },
      ];

      mockMetricsService.getRealtimeMetrics.mockResolvedValue(mockMetrics);

      gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views'],
        interval: 5000,
      });

      expect(gateway['subscriptions'].has('client-001')).toBe(true);

      gateway.handleDisconnect(mockSocket as unknown as Socket);

      expect(gateway['subscriptions'].has('client-001')).toBe(false);

      vi.useRealTimers();
    });
  });

  describe('handleSubscribe', () => {
    it('subscribes client to metrics and sends initial data', async () => {
      const mockSocket = createMockSocket('client-001');
      const mockMetrics = [
        { metric: 'page_views', value: 500, change: 25, changePercent: 5, timestamp: new Date() },
        { metric: 'sessions', value: 150, change: -10, changePercent: -6.25, timestamp: new Date() },
      ];

      mockMetricsService.getRealtimeMetrics.mockResolvedValue(mockMetrics);

      const result = gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views', 'sessions'],
        interval: 3000,
      });

      await vi.waitFor(() => {
        expect(mockMetricsService.getRealtimeMetrics).toHaveBeenCalledWith([
          'page_views',
          'sessions',
        ]);
      });

      await vi.waitFor(() => {
        expect(mockSocket.emit).toHaveBeenCalledWith('metrics', mockMetrics);
      });

      expect(result).toEqual({
        subscribed: ['page_views', 'sessions'],
        interval: 3000,
      });
    });

    it('uses default interval of 5000ms when not specified', () => {
      const mockSocket = createMockSocket('client-001');
      mockMetricsService.getRealtimeMetrics.mockResolvedValue([]);

      const result = gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views'],
      });

      expect(result.interval).toBe(5000);
    });

    it('clears existing subscription before creating new one', () => {
      vi.useFakeTimers();
      const mockSocket = createMockSocket('client-001');
      mockMetricsService.getRealtimeMetrics.mockResolvedValue([]);

      gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views'],
        interval: 5000,
      });

      const firstTimer = gateway['subscriptions'].get('client-001');

      gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['sessions'],
        interval: 3000,
      });

      const secondTimer = gateway['subscriptions'].get('client-001');

      expect(firstTimer).not.toBe(secondTimer);

      vi.useRealTimers();
    });

    it('sends periodic updates at specified interval', async () => {
      vi.useFakeTimers();
      const mockSocket = createMockSocket('client-001');
      const mockMetrics = [
        { metric: 'page_views', value: 100, change: 5, changePercent: 5, timestamp: new Date() },
      ];

      mockMetricsService.getRealtimeMetrics.mockResolvedValue(mockMetrics);

      gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views'],
        interval: 5000,
      });

      await vi.waitFor(() => {
        expect(mockSocket.emit).toHaveBeenCalledWith('metrics', mockMetrics);
      });

      mockSocket.emit.mockClear();

      vi.advanceTimersByTime(5000);

      await vi.waitFor(() => {
        expect(mockSocket.emit).toHaveBeenCalledWith('metrics', mockMetrics);
      });

      mockSocket.emit.mockClear();

      vi.advanceTimersByTime(5000);

      await vi.waitFor(() => {
        expect(mockSocket.emit).toHaveBeenCalledWith('metrics', mockMetrics);
      });

      gateway.handleDisconnect(mockSocket as unknown as Socket);
      vi.useRealTimers();
    });

    it('stores subscription timer in subscriptions map', () => {
      const mockSocket = createMockSocket('client-001');
      mockMetricsService.getRealtimeMetrics.mockResolvedValue([]);

      gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views'],
        interval: 5000,
      });

      expect(gateway['subscriptions'].has('client-001')).toBe(true);
    });

    it('handles errors in metrics service gracefully', async () => {
      const mockSocket = createMockSocket('client-001');
      mockMetricsService.getRealtimeMetrics.mockRejectedValue(
        new Error('Database connection failed'),
      );

      expect(() => {
        gateway.handleSubscribe(mockSocket as unknown as Socket, {
          metrics: ['page_views'],
          interval: 5000,
        });
      }).not.toThrow();

      await vi.waitFor(() => {
        expect(mockMetricsService.getRealtimeMetrics).toHaveBeenCalled();
      });
    });

    it('handles multiple concurrent subscriptions from different clients', () => {
      vi.useFakeTimers();
      const socket1 = createMockSocket('client-001');
      const socket2 = createMockSocket('client-002');
      const socket3 = createMockSocket('client-003');

      mockMetricsService.getRealtimeMetrics.mockResolvedValue([]);

      gateway.handleSubscribe(socket1 as unknown as Socket, {
        metrics: ['page_views'],
        interval: 5000,
      });
      gateway.handleSubscribe(socket2 as unknown as Socket, {
        metrics: ['sessions'],
        interval: 3000,
      });
      gateway.handleSubscribe(socket3 as unknown as Socket, {
        metrics: ['unique_visitors'],
        interval: 10000,
      });

      expect(gateway['subscriptions'].has('client-001')).toBe(true);
      expect(gateway['subscriptions'].has('client-002')).toBe(true);
      expect(gateway['subscriptions'].has('client-003')).toBe(true);

      vi.useRealTimers();
    });
  });

  describe('handleUnsubscribe', () => {
    it('clears subscription and returns confirmation', () => {
      vi.useFakeTimers();
      const mockSocket = createMockSocket('client-001');
      mockMetricsService.getRealtimeMetrics.mockResolvedValue([]);

      gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views'],
        interval: 5000,
      });

      expect(gateway['subscriptions'].has('client-001')).toBe(true);

      const result = gateway.handleUnsubscribe(mockSocket as unknown as Socket);

      expect(gateway['subscriptions'].has('client-001')).toBe(false);
      expect(result).toEqual({ unsubscribed: true });

      vi.useRealTimers();
    });

    it('handles unsubscribe without active subscription', () => {
      const mockSocket = createMockSocket('client-001');

      expect(() => {
        gateway.handleUnsubscribe(mockSocket as unknown as Socket);
      }).not.toThrow();

      const result = gateway.handleUnsubscribe(mockSocket as unknown as Socket);

      expect(result).toEqual({ unsubscribed: true });
    });
  });

  describe('broadcastMetricUpdate', () => {
    it('broadcasts metric update to all connected clients', () => {
      const mockServer = gateway.server as unknown as MockServer;

      gateway.broadcastMetricUpdate('page_views', 1250);

      expect(mockServer.emit).toHaveBeenCalledWith('metric-update', {
        metric: 'page_views',
        value: 1250,
        timestamp: expect.any(Date),
      });
    });

    it('broadcasts multiple metric updates', () => {
      const mockServer = gateway.server as unknown as MockServer;

      gateway.broadcastMetricUpdate('page_views', 1000);
      gateway.broadcastMetricUpdate('sessions', 250);
      gateway.broadcastMetricUpdate('unique_visitors', 180);

      expect(mockServer.emit).toHaveBeenCalledTimes(3);
      expect(mockServer.emit).toHaveBeenNthCalledWith(1, 'metric-update', {
        metric: 'page_views',
        value: 1000,
        timestamp: expect.any(Date),
      });
      expect(mockServer.emit).toHaveBeenNthCalledWith(2, 'metric-update', {
        metric: 'sessions',
        value: 250,
        timestamp: expect.any(Date),
      });
      expect(mockServer.emit).toHaveBeenNthCalledWith(3, 'metric-update', {
        metric: 'unique_visitors',
        value: 180,
        timestamp: expect.any(Date),
      });
    });

    it('includes timestamp in broadcast payload', () => {
      const mockServer = gateway.server as unknown as MockServer;
      const beforeBroadcast = Date.now();

      gateway.broadcastMetricUpdate('revenue', 5000);

      const afterBroadcast = Date.now();

      expect(mockServer.emit).toHaveBeenCalledWith('metric-update', {
        metric: 'revenue',
        value: 5000,
        timestamp: expect.any(Date),
      });

      const callArgs = mockServer.emit.mock.calls[0][1] as {
        metric: string;
        value: number;
        timestamp: Date;
      };
      const timestampValue = callArgs.timestamp.getTime();

      expect(timestampValue).toBeGreaterThanOrEqual(beforeBroadcast);
      expect(timestampValue).toBeLessThanOrEqual(afterBroadcast);
    });
  });

  describe('clearSubscriptions', () => {
    it('clears interval timer and removes from map', () => {
      vi.useFakeTimers();
      const mockSocket = createMockSocket('client-001');
      mockMetricsService.getRealtimeMetrics.mockResolvedValue([]);

      gateway.handleSubscribe(mockSocket as unknown as Socket, {
        metrics: ['page_views'],
        interval: 5000,
      });

      expect(gateway['subscriptions'].has('client-001')).toBe(true);

      gateway['clearSubscriptions']('client-001');

      expect(gateway['subscriptions'].has('client-001')).toBe(false);

      vi.useRealTimers();
    });

    it('handles clearing non-existent subscription', () => {
      expect(() => {
        gateway['clearSubscriptions']('non-existent-client');
      }).not.toThrow();
    });
  });
});
