// src/services/monitoringService.server.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Job } from 'bullmq';
import { NotFoundError, ValidationError } from './db/errors.db';
import { logger } from './logger.server';

// --- Hoisted Mocks ---
const mocks = vi.hoisted(() => {
  const createMockWorker = (name: string) => ({
    name,
    isRunning: vi.fn().mockReturnValue(true),
  });

  const createMockQueue = (name: string) => ({
    name,
    getJobCounts: vi.fn().mockResolvedValue({
      waiting: 0,
      active: 0,
      completed: 0,
      failed: 0,
      delayed: 0,
      paused: 0,
    }),
    getJob: vi.fn(),
  });

  return {
    flyerWorker: createMockWorker('flyer-processing'),
    emailWorker: createMockWorker('email-sending'),
    analyticsWorker: createMockWorker('analytics-reporting'),
    cleanupWorker: createMockWorker('file-cleanup'),
    weeklyAnalyticsWorker: createMockWorker('weekly-analytics-reporting'),
    tokenCleanupWorker: createMockWorker('token-cleanup'),

    flyerQueue: createMockQueue('flyer-processing'),
    emailQueue: createMockQueue('email-sending'),
    analyticsQueue: createMockQueue('analytics-reporting'),
    cleanupQueue: createMockQueue('file-cleanup'),
    weeklyAnalyticsQueue: createMockQueue('weekly-analytics-reporting'),
    tokenCleanupQueue: createMockQueue('token-cleanup'),
  };
});

// --- Mock Modules ---
vi.mock('./queues.server', () => ({
  flyerQueue: mocks.flyerQueue,
  emailQueue: mocks.emailQueue,
  analyticsQueue: mocks.analyticsQueue,
  cleanupQueue: mocks.cleanupQueue,
  weeklyAnalyticsQueue: mocks.weeklyAnalyticsQueue,
  tokenCleanupQueue: mocks.tokenCleanupQueue,
}));

vi.mock('./workers.server', () => ({
  flyerWorker: mocks.flyerWorker,
  emailWorker: mocks.emailWorker,
  analyticsWorker: mocks.analyticsWorker,
  cleanupWorker: mocks.cleanupWorker,
  weeklyAnalyticsWorker: mocks.weeklyAnalyticsWorker,
  tokenCleanupWorker: mocks.tokenCleanupWorker,
  flyerProcessingService: {},
}));

vi.mock('./db/errors.db', () => ({
  NotFoundError: class NotFoundError extends Error {
    constructor(message: string) {
      super(message);
      this.name = 'NotFoundError';
    }
  },
  ValidationError: class ValidationError extends Error {
    constructor(issues: [], message: string) {
      super(message);
      this.name = 'ValidationError';
    }
  },
}));

vi.mock('./logger.server', () => ({
  logger: {
    info: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
  },
}));

// Import the service to be tested AFTER all mocks are set up.
import { monitoringService } from './monitoringService.server';

describe('MonitoringService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getWorkerStatuses', () => {
    it('should return the running status of all workers', async () => {
      // Arrange: one worker is not running
      mocks.emailWorker.isRunning.mockReturnValue(false);

      // Act
      const statuses = await monitoringService.getWorkerStatuses();

      // Assert
      expect(statuses).toEqual([
        { name: 'flyer-processing', isRunning: true },
        { name: 'email-sending', isRunning: false },
        { name: 'analytics-reporting', isRunning: true },
        { name: 'file-cleanup', isRunning: true },
        { name: 'weekly-analytics-reporting', isRunning: true },
        { name: 'token-cleanup', isRunning: true },
      ]);
      expect(mocks.flyerWorker.isRunning).toHaveBeenCalledTimes(1);
      expect(mocks.emailWorker.isRunning).toHaveBeenCalledTimes(1);
    });
  });

  describe('getQueueStatuses', () => {
    it('should return job counts for all queues', async () => {
      const defaultCounts = {
        waiting: 0,
        active: 0,
        completed: 0,
        failed: 0,
        delayed: 0,
        paused: 0,
      };

      // Arrange - override specific queue counts
      mocks.flyerQueue.getJobCounts.mockResolvedValue({ ...defaultCounts, active: 1, failed: 2 });
      mocks.emailQueue.getJobCounts.mockResolvedValue({
        ...defaultCounts,
        completed: 10,
        waiting: 5,
      });

      // Act
      const statuses = await monitoringService.getQueueStatuses();

      // Assert
      expect(statuses).toEqual(
        expect.arrayContaining([
          { name: 'flyer-processing', counts: { ...defaultCounts, active: 1, failed: 2 } },
          { name: 'email-sending', counts: { ...defaultCounts, completed: 10, waiting: 5 } },
          { name: 'analytics-reporting', counts: defaultCounts },
          { name: 'file-cleanup', counts: defaultCounts },
          { name: 'weekly-analytics-reporting', counts: defaultCounts },
          { name: 'token-cleanup', counts: defaultCounts },
        ]),
      );
      expect(mocks.flyerQueue.getJobCounts).toHaveBeenCalledTimes(1);
      expect(mocks.emailQueue.getJobCounts).toHaveBeenCalledTimes(1);
    });
  });

  describe('retryFailedJob', () => {
    const userId = 'admin-user';
    const jobId = 'failed-job-1';

    it('should throw NotFoundError for an unknown queue name', async () => {
      await expect(
        monitoringService.retryFailedJob('unknown-queue', jobId, userId),
      ).rejects.toThrow(new NotFoundError(`Queue 'unknown-queue' not found.`));
    });

    it('should throw NotFoundError if the job does not exist in the queue', async () => {
      mocks.flyerQueue.getJob.mockResolvedValue(null);

      await expect(
        monitoringService.retryFailedJob('flyer-processing', jobId, userId),
      ).rejects.toThrow(
        new NotFoundError(`Job with ID '${jobId}' not found in queue 'flyer-processing'.`),
      );
    });

    it("should throw ValidationError if the job is not in a 'failed' state", async () => {
      const mockJob = {
        id: jobId,
        getState: vi.fn().mockResolvedValue('completed'),
        retry: vi.fn(),
      } as unknown as Job;
      mocks.flyerQueue.getJob.mockResolvedValue(mockJob);

      await expect(
        monitoringService.retryFailedJob('flyer-processing', jobId, userId),
      ).rejects.toThrow(
        new ValidationError([], `Job is not in a 'failed' state. Current state: completed.`),
      );
    });

    it("should call job.retry() and log if the job is in a 'failed' state", async () => {
      const mockJob = {
        id: jobId,
        getState: vi.fn().mockResolvedValue('failed'),
        retry: vi.fn().mockResolvedValue(undefined),
      } as unknown as Job;
      mocks.flyerQueue.getJob.mockResolvedValue(mockJob);

      await monitoringService.retryFailedJob('flyer-processing', jobId, userId);

      expect(mockJob.retry).toHaveBeenCalledTimes(1);
      expect(logger.info).toHaveBeenCalledWith(
        `[Admin] User ${userId} manually retried job ${jobId} in queue flyer-processing.`,
      );
    });
  });

  describe('getFlyerJobStatus', () => {
    const jobId = 'flyer-job-123';

    it('should throw NotFoundError if the job is not found', async () => {
      mocks.flyerQueue.getJob.mockResolvedValue(null);

      await expect(monitoringService.getFlyerJobStatus(jobId)).rejects.toThrow(
        new NotFoundError('Job not found.'),
      );
    });

    it('should return the job status object if the job is found', async () => {
      const mockJob = {
        id: jobId,
        getState: vi.fn().mockResolvedValue('completed'),
        progress: 100,
        returnvalue: { flyerId: 99 },
        failedReason: null,
      } as unknown as Job;
      mocks.flyerQueue.getJob.mockResolvedValue(mockJob);

      const status = await monitoringService.getFlyerJobStatus(jobId);

      expect(status).toEqual({
        id: jobId,
        state: 'completed',
        progress: 100,
        returnValue: { flyerId: 99 },
        failedReason: null,
      });
    });
  });
});
