import type { Job, Queue, QueueEvents } from 'bullmq';
import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test';
import { QueueMetricsCollector } from '../src/queue-metrics';

describe('QueueMetricsCollector', () => {
  let metrics: QueueMetricsCollector;
  let mockQueue: {
    name: string;
    getWaitingCount: Mock<() => Promise<number>>;
    getActiveCount: Mock<() => Promise<number>>;
    getCompletedCount: Mock<() => Promise<number>>;
    getFailedCount: Mock<() => Promise<number>>;
    getDelayedCount: Mock<() => Promise<number>>;
    isPaused: Mock<() => Promise<boolean>>;
    getWaiting: Mock<() => Promise<Job[]>>;
  };
  let mockQueueEvents: {
    on: Mock<(event: string, handler: Function) => void>;
  };

  beforeEach(() => {
    mockQueue = {
      name: 'test-queue',
      getWaitingCount: mock(() => Promise.resolve(0)),
      getActiveCount: mock(() => Promise.resolve(0)),
      getCompletedCount: mock(() => Promise.resolve(0)),
      getFailedCount: mock(() => Promise.resolve(0)),
      getDelayedCount: mock(() => Promise.resolve(0)),
      isPaused: mock(() => Promise.resolve(false)),
      getWaiting: mock(() => Promise.resolve([])),
    };

    mockQueueEvents = {
      on: mock(() => {}),
    };

    metrics = new QueueMetricsCollector(
      mockQueue as unknown as Queue,
      mockQueueEvents as unknown as QueueEvents
    );
  });

  describe('collect metrics', () => {
    it('should collect current metrics', async () => {
      mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(5));
      mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(2));
      mockQueue.getCompletedCount.mockImplementation(() => Promise.resolve(100));
      mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(3));
      mockQueue.getDelayedCount.mockImplementation(() => Promise.resolve(1));

      // Add some completed timestamps to avoid 100% failure rate
      const completedHandler = mockQueueEvents.on.mock.calls.find(
        call => call[0] === 'completed'
      )?.[1];
      if (completedHandler) {
        for (let i = 0; i < 50; i++) {
          completedHandler();
        }
      }

      const result = await metrics.collect();

      expect(result.waiting).toBe(5);
      expect(result.active).toBe(2);
      expect(result.completed).toBe(100);
      expect(result.failed).toBe(3);
      expect(result.delayed).toBe(1);
      expect(result.isHealthy).toBe(true);
    });

    it('should detect health issues', async () => {
      mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(2000)); // High backlog
      mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(150)); // High active
      mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(50));

      const result = await metrics.collect();

      expect(result.isHealthy).toBe(false);
      expect(result.healthIssues.length).toBeGreaterThan(0);
      expect(result.healthIssues.some(issue => issue.includes('queue backlog'))).toBe(true);
      expect(result.healthIssues.some(issue => issue.includes('active jobs'))).toBe(true);
    });

    it('should handle paused queue', async () => {
      mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(10));
      mockQueue.isPaused.mockImplementation(() => Promise.resolve(true));

      const result = await metrics.collect();

      expect(result.paused).toBe(10);
    });
  });

  describe('processing time metrics', () => {
    it('should calculate processing time metrics', async () => {
      // Access private property for testing
      const metricsWithPrivate = metrics as QueueMetricsCollector & {
        processingTimes: number[];
      };
      metricsWithPrivate.processingTimes = [1000, 2000, 3000, 4000, 5000];

      const result = await metrics.collect();

      expect(result.processingTime.avg).toBe(3000);
      expect(result.processingTime.min).toBe(1000);
      expect(result.processingTime.max).toBe(5000);
      expect(result.processingTime.p95).toBe(5000);
    });

    it('should handle no processing times', async () => {
      const result = await metrics.collect();

      expect(result.processingTime.avg).toBe(0);
      expect(result.processingTime.min).toBe(0);
      expect(result.processingTime.max).toBe(0);
    });
  });

  describe('throughput metrics', () => {
    it('should calculate throughput', async () => {
      // Access private properties for testing
      const metricsWithPrivate = metrics as QueueMetricsCollector & {
        completedTimestamps: number[];
        failedTimestamps: number[];
      };

      const now = Date.now();
      metricsWithPrivate.completedTimestamps = [
        now - 30000, // 30 seconds ago
        now - 20000,
        now - 10000,
      ];
      metricsWithPrivate.failedTimestamps = [now - 25000, now - 5000];

      const result = await metrics.collect();

      expect(result.throughput.completedPerMinute).toBe(3);
      expect(result.throughput.failedPerMinute).toBe(2);
      expect(result.throughput.totalPerMinute).toBe(5);
    });
  });

  describe('getReport', () => {
    it('should generate formatted report', async () => {
      mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(5));
      mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(2));
      mockQueue.getCompletedCount.mockImplementation(() => Promise.resolve(100));
      mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(3));

      // Add some completed timestamps to make it healthy
      const completedHandler = mockQueueEvents.on.mock.calls.find(
        call => call[0] === 'completed'
      )?.[1];
      if (completedHandler) {
        for (let i = 0; i < 50; i++) {
          completedHandler();
        }
      }

      const report = await metrics.getReport();

      expect(report).toContain('Queue Metrics Report');
      expect(report).toContain('✅ Healthy');
      expect(report).toContain('Waiting: 5');
      expect(report).toContain('Active: 2');
      expect(report).toContain('Completed: 100');
      expect(report).toContain('Failed: 3');
    });
  });

  describe('getPrometheusMetrics', () => {
    it('should generate Prometheus formatted metrics', async () => {
      mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(5));
      mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(2));
      mockQueue.getCompletedCount.mockImplementation(() => Promise.resolve(100));
      mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(3));

      const prometheusMetrics = await metrics.getPrometheusMetrics();

      expect(prometheusMetrics).toContain('# HELP queue_jobs_total');
      expect(prometheusMetrics).toContain(
        'queue_jobs_total{queue="test-queue",status="waiting"} 5'
      );
      expect(prometheusMetrics).toContain('queue_jobs_total{queue="test-queue",status="active"} 2');
      expect(prometheusMetrics).toContain(
        'queue_jobs_total{queue="test-queue",status="completed"} 100'
      );
      expect(prometheusMetrics).toContain('# HELP queue_processing_time_seconds');
      expect(prometheusMetrics).toContain('# HELP queue_throughput_per_minute');
      expect(prometheusMetrics).toContain('# HELP queue_health');
    });
  });

  describe('event listeners', () => {
    it('should setup event listeners on construction', () => {
      const newMockQueueEvents = {
        on: mock<(event: string, handler: Function) => void>(() => {}),
      };

      new QueueMetricsCollector(
        mockQueue as unknown as Queue,
        newMockQueueEvents as unknown as QueueEvents
      );

      expect(newMockQueueEvents.on).toHaveBeenCalledWith('completed', expect.any(Function));
      expect(newMockQueueEvents.on).toHaveBeenCalledWith('failed', expect.any(Function));
      expect(newMockQueueEvents.on).toHaveBeenCalledWith('active', expect.any(Function));
    });
  });

  describe('oldest waiting job', () => {
    it('should get oldest waiting job date', async () => {
      const oldJob = {
        timestamp: Date.now() - 60000, // 1 minute ago
      } as Job;

      mockQueue.getWaiting.mockImplementation(() => Promise.resolve([oldJob]));

      const result = await metrics.collect();

      expect(result.oldestWaitingJob).toBeDefined();
      expect(result.oldestWaitingJob).toBeInstanceOf(Date);
    });

    it('should return null when no waiting jobs', async () => {
      mockQueue.getWaiting.mockImplementation(() => Promise.resolve([]));

      const result = await metrics.collect();

      expect(result.oldestWaitingJob).toBeNull();
    });
  });
});
