// src/services/analyticsService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AnalyticsService } from './analyticsService.server';
import { logger } from './logger.server';
import type { Job } from 'bullmq';
import type { AnalyticsJobData, WeeklyAnalyticsJobData } from '../types/job-data';

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

describe('AnalyticsService', () => {
  let service: AnalyticsService;
  let mockLoggerInstance: any;

  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();

    // Setup mock logger instance returned by child()
    mockLoggerInstance = {
      info: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
      debug: vi.fn(),
    };
    vi.mocked(logger.child).mockReturnValue(mockLoggerInstance);

    service = new AnalyticsService();
  });

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

  const createMockJob = <T>(data: T): Job<T> =>
    ({
      id: 'job-123',
      name: 'analytics-job',
      data,
      attemptsMade: 1,
      updateProgress: vi.fn(),
    } as unknown as Job<T>);

  describe('processDailyReportJob', () => {
    it('should process successfully', async () => {
      const job = createMockJob<AnalyticsJobData>({ reportDate: '2023-10-27' } as AnalyticsJobData);

      const promise = service.processDailyReportJob(job);

      // Fast-forward time to bypass the 10s delay
      await vi.advanceTimersByTimeAsync(10000);

      const result = await promise;

      expect(result).toEqual({ status: 'success', reportDate: '2023-10-27' });
      expect(logger.child).toHaveBeenCalledWith(
        expect.objectContaining({
          jobId: 'job-123',
          reportDate: '2023-10-27',
        }),
      );
      expect(mockLoggerInstance.info).toHaveBeenCalledWith('Picked up daily analytics job.');
      expect(mockLoggerInstance.info).toHaveBeenCalledWith(
        'Successfully generated report for 2023-10-27.',
      );
    });

    it('should handle failure when reportDate is FAIL', async () => {
      const job = createMockJob<AnalyticsJobData>({ reportDate: 'FAIL' } as AnalyticsJobData);

      const promise = service.processDailyReportJob(job);

      await expect(promise).rejects.toThrow('This is a test failure for the analytics job.');

      expect(mockLoggerInstance.error).toHaveBeenCalledWith(
        expect.objectContaining({
          err: expect.any(Error),
          attemptsMade: 1,
        }),
        'Daily analytics job failed.',
      );
    });

    it('should handle non-Error objects thrown during processing', async () => {
      const job = createMockJob<AnalyticsJobData>({ reportDate: '2023-10-27' } as AnalyticsJobData);

      mockLoggerInstance.info
        .mockImplementationOnce(() => {}) // "Picked up..."
        .mockImplementationOnce(() => {
          throw 'A string error';
        });

      const promise = service.processDailyReportJob(job);

      // Capture the expectation promise BEFORE triggering the rejection via timer advancement.
      const expectation = expect(promise).rejects.toThrow('A string error');

      await vi.advanceTimersByTimeAsync(10000);

      await expectation;

      expect(mockLoggerInstance.error).toHaveBeenCalledWith(
        expect.objectContaining({
          err: expect.objectContaining({ message: 'A string error' }),
          attemptsMade: 1,
        }),
        'Daily analytics job failed.',
      );
    });
  });

  describe('processWeeklyReportJob', () => {
    it('should process successfully', async () => {
      const job = createMockJob<WeeklyAnalyticsJobData>({
        reportYear: 2023,
        reportWeek: 43,
      } as WeeklyAnalyticsJobData);

      const promise = service.processWeeklyReportJob(job);

      await vi.advanceTimersByTimeAsync(30000);

      const result = await promise;

      expect(result).toEqual({ status: 'success', reportYear: 2023, reportWeek: 43 });
      expect(logger.child).toHaveBeenCalledWith(
        expect.objectContaining({
          jobId: 'job-123',
          reportYear: 2023,
          reportWeek: 43,
        }),
      );
      expect(mockLoggerInstance.info).toHaveBeenCalledWith('Picked up weekly analytics job.');
      expect(mockLoggerInstance.info).toHaveBeenCalledWith(
        'Successfully generated weekly report for week 43, 2023.',
      );
    });

    it('should handle errors during processing', async () => {
      const job = createMockJob<WeeklyAnalyticsJobData>({
        reportYear: 2023,
        reportWeek: 43,
      } as WeeklyAnalyticsJobData);

      // Make the second info call throw to simulate an error inside the try block
      mockLoggerInstance.info
        .mockImplementationOnce(() => {}) // "Picked up..."
        .mockImplementationOnce(() => {
          throw new Error('Processing failed');
        }); // "Successfully generated..."

      // Get the promise from the service method.
      const promise = service.processWeeklyReportJob(job);

      // Capture the expectation promise BEFORE triggering the rejection.
      const expectation = expect(promise).rejects.toThrow('Processing failed');

      // Advance timers to trigger the part of the code that throws.
      await vi.advanceTimersByTimeAsync(30000);

      // Await the expectation to ensure assertions ran.
      await expectation;

      // Verify the side effect (error logging) after the rejection is confirmed.
      expect(mockLoggerInstance.error).toHaveBeenCalledWith(
        expect.objectContaining({
          err: expect.any(Error),
          attemptsMade: 1,
        }),
        'Weekly analytics job failed.',
      );
    });

    it('should handle non-Error objects thrown during processing', async () => {
      const job = createMockJob<WeeklyAnalyticsJobData>({
        reportYear: 2023,
        reportWeek: 43,
      } as WeeklyAnalyticsJobData);

      mockLoggerInstance.info
        .mockImplementationOnce(() => {}) // "Picked up..."
        .mockImplementationOnce(() => {
          throw 'A string error';
        });

      const promise = service.processWeeklyReportJob(job);

      // Capture the expectation promise BEFORE triggering the rejection via timer advancement.
      const expectation = expect(promise).rejects.toThrow('A string error');

      await vi.advanceTimersByTimeAsync(30000);

      await expectation;

      expect(mockLoggerInstance.error).toHaveBeenCalledWith(
        expect.objectContaining({
          err: expect.objectContaining({ message: 'A string error' }),
          attemptsMade: 1,
        }),
        'Weekly analytics job failed.',
      );
    });
  });
});