import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import type { Repository, SelectQueryBuilder } from 'typeorm';
import { MetricsService } from './metrics.service';
import { AggregatedMetric, MetricType, TimeGranularity } from '../entities/aggregated-metric.entity';

interface MockRepository<T> {
  findOne: ReturnType<typeof vi.fn>;
  createQueryBuilder: ReturnType<typeof vi.fn>;
}

interface MockQueryBuilder {
  select: ReturnType<typeof vi.fn>;
  where: ReturnType<typeof vi.fn>;
  andWhere: ReturnType<typeof vi.fn>;
  getRawOne: ReturnType<typeof vi.fn>;
}

function createMockRepository<T>(): MockRepository<T> {
  return {
    findOne: vi.fn(),
    createQueryBuilder: vi.fn(),
  };
}

function createMockQueryBuilder(): MockQueryBuilder {
  const builder: MockQueryBuilder = {
    select: vi.fn(),
    where: vi.fn(),
    andWhere: vi.fn(),
    getRawOne: vi.fn().mockResolvedValue(null),
  };

  // Make chainable methods return builder
  builder.select.mockReturnValue(builder);
  builder.where.mockReturnValue(builder);
  builder.andWhere.mockReturnValue(builder);

  return builder;
}

describe('MetricsService', () => {
  let service: MetricsService;
  let mockRepository: MockRepository<AggregatedMetric>;

  beforeEach(async () => {
    mockRepository = createMockRepository<AggregatedMetric>();

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

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

  describe('getRealtimeMetrics', () => {
    it('returns realtime metrics with hour-over-hour comparison', async () => {
      const currentHourMetric = {
        metricType: MetricType.PAGE_VIEWS,
        granularity: TimeGranularity.HOUR,
        timestamp: new Date('2026-01-29T10:00:00Z'),
        value: 500,
        count: 500,
      };

      const previousHourMetric = {
        metricType: MetricType.PAGE_VIEWS,
        granularity: TimeGranularity.HOUR,
        timestamp: new Date('2026-01-29T09:00:00Z'),
        value: 400,
        count: 400,
      };

      mockRepository.findOne
        .mockResolvedValueOnce(currentHourMetric)
        .mockResolvedValueOnce(previousHourMetric);

      const result = await service.getRealtimeMetrics(['page_views']);

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        metric: 'page_views',
        value: 500,
        change: 100,
        changePercent: 25,
        timestamp: expect.any(Date),
      });

      expect(mockRepository.findOne).toHaveBeenCalledTimes(2);
    });

    it('handles multiple metrics concurrently', async () => {
      mockRepository.findOne
        .mockResolvedValueOnce({ value: 100, count: 100 })
        .mockResolvedValueOnce({ value: 80, count: 80 })
        .mockResolvedValueOnce({ value: 250, count: 250 })
        .mockResolvedValueOnce({ value: 200, count: 200 });

      const result = await service.getRealtimeMetrics(['page_views', 'sessions']);

      expect(result).toHaveLength(2);
      expect(result[0].metric).toBe('page_views');
      expect(result[0].value).toBe(100);
      expect(result[0].change).toBe(20);
      expect(result[0].changePercent).toBe(25);

      expect(result[1].metric).toBe('sessions');
      expect(result[1].value).toBe(250);
      expect(result[1].change).toBe(50);
      expect(result[1].changePercent).toBe(25);
    });

    it('returns zero values when no current data exists', async () => {
      mockRepository.findOne
        .mockResolvedValueOnce(null)
        .mockResolvedValueOnce(null);

      const result = await service.getRealtimeMetrics(['page_views']);

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        metric: 'page_views',
        value: 0,
        change: 0,
        changePercent: 0,
        timestamp: expect.any(Date),
      });
    });

    it('handles missing previous data (returns change based on zero)', async () => {
      const currentHourMetric = {
        value: 300,
        count: 300,
      };

      mockRepository.findOne
        .mockResolvedValueOnce(currentHourMetric)
        .mockResolvedValueOnce(null);

      const result = await service.getRealtimeMetrics(['sessions']);

      expect(result[0].value).toBe(300);
      expect(result[0].change).toBe(300);
      expect(result[0].changePercent).toBe(0);
    });

    it('calculates negative change correctly', async () => {
      const currentHourMetric = { value: 150, count: 150 };
      const previousHourMetric = { value: 200, count: 200 };

      mockRepository.findOne
        .mockResolvedValueOnce(currentHourMetric)
        .mockResolvedValueOnce(previousHourMetric);

      const result = await service.getRealtimeMetrics(['unique_visitors']);

      expect(result[0].value).toBe(150);
      expect(result[0].change).toBe(-50);
      expect(result[0].changePercent).toBe(-25);
    });

    it('queries correct time ranges for hourly metrics', async () => {
      mockRepository.findOne.mockResolvedValue(null);

      await service.getRealtimeMetrics(['page_views']);

      expect(mockRepository.findOne).toHaveBeenNthCalledWith(1, {
        where: {
          metricType: 'page_views',
          granularity: TimeGranularity.HOUR,
          timestamp: expect.any(Object),
        },
        order: { timestamp: 'DESC' },
      });

      expect(mockRepository.findOne).toHaveBeenNthCalledWith(2, {
        where: {
          metricType: 'page_views',
          granularity: TimeGranularity.HOUR,
          timestamp: expect.any(Object),
        },
        order: { timestamp: 'DESC' },
      });
    });

    it('handles empty metric names array', async () => {
      const result = await service.getRealtimeMetrics([]);

      expect(result).toEqual([]);
      expect(mockRepository.findOne).not.toHaveBeenCalled();
    });
  });

  describe('getActiveUsers', () => {
    it('returns total active sessions from last 5 minutes', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue({ total: '45' });

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getActiveUsers();

      expect(result).toBe(45);
      expect(mockQueryBuilder.select).toHaveBeenCalledWith('SUM(m.count)', 'total');
      expect(mockQueryBuilder.where).toHaveBeenCalledWith('m.metricType = :type', {
        type: MetricType.SESSIONS,
      });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('m.granularity = :gran', {
        gran: TimeGranularity.MINUTE,
      });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('m.timestamp > :since', {
        since: expect.any(Date),
      });
    });

    it('returns zero when no active sessions exist', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue(null);

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getActiveUsers();

      expect(result).toBe(0);
    });

    it('returns zero when total is null in result', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue({ total: null });

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getActiveUsers();

      expect(result).toBe(0);
    });

    it('converts string total to number correctly', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue({ total: '123' });

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getActiveUsers();

      expect(result).toBe(123);
      expect(typeof result).toBe('number');
    });
  });

  describe('getCurrentPageViews', () => {
    it('returns total page views from last 5 minutes', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue({ total: '832' });

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getCurrentPageViews();

      expect(result).toBe(832);
      expect(mockQueryBuilder.select).toHaveBeenCalledWith('SUM(m.count)', 'total');
      expect(mockQueryBuilder.where).toHaveBeenCalledWith('m.metricType = :type', {
        type: MetricType.PAGE_VIEWS,
      });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('m.granularity = :gran', {
        gran: TimeGranularity.MINUTE,
      });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('m.timestamp > :since', {
        since: expect.any(Date),
      });
    });

    it('returns zero when no page views exist', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue(null);

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getCurrentPageViews();

      expect(result).toBe(0);
    });

    it('returns zero when total is undefined', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue({ total: undefined });

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getCurrentPageViews();

      expect(result).toBe(0);
    });

    it('handles large page view counts', async () => {
      const mockQueryBuilder = createMockQueryBuilder();
      mockQueryBuilder.getRawOne.mockResolvedValue({ total: '9999999' });

      mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await service.getCurrentPageViews();

      expect(result).toBe(9999999);
    });
  });
});
