// ABOUTME: Unit tests for PostRaceScheduler
// ABOUTME: Tests race result fetching and retry logic

import { Race } from '@prisma/client';
import { PostRaceScheduler } from '../post-race-scheduler';
import { TabApiClient } from '../../api/tab';
import { RaceService } from '../../services/race-service';
import { DateTime } from 'luxon';

// Mock dependencies
jest.mock('../../utils/logger', () => ({
  __esModule: true,
  default: {
    info: jest.fn(),
    debug: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
  },
}));

jest.mock('prom-client', () => ({
  Counter: jest.fn().mockImplementation(() => ({
    inc: jest.fn(),
    labels: jest.fn().mockReturnThis(),
  })),
  Histogram: jest.fn().mockImplementation(() => ({
    observe: jest.fn(),
    labels: jest.fn().mockReturnThis(),
  })),
  Gauge: jest.fn().mockImplementation(() => ({
    set: jest.fn(),
    labels: jest.fn().mockReturnThis(),
  })),
}));

jest.mock('@opentelemetry/api', () => ({
  trace: {
    getTracer: jest.fn(() => ({
      startSpan: jest.fn(() => ({
        setAttributes: jest.fn(),
        setAttribute: jest.fn(),
        setStatus: jest.fn(),
        end: jest.fn(),
      })),
    })),
  },
  SpanStatusCode: {
    OK: 1,
    ERROR: 2,
  },
}));

jest.mock('node-cron', () => ({
  schedule: jest.fn(() => ({
    stop: jest.fn(),
  })),
}));

// Mock RaceService
jest.mock('../../services/race-service');

describe('PostRaceScheduler', () => {
  let scheduler: PostRaceScheduler;
  let mockPrisma: any;
  let mockApiClient: jest.Mocked<TabApiClient>;
  let mockRaceService: jest.Mocked<RaceService>;
  let mockFindMany: jest.Mock;

  const mockSuccessResultWithResults = {
    success: true,
    raceId: 'race-1',
    runnersProcessed: 5,
    oddsSnapshotsCaptured: 5,
    resultsProcessed: 3,
    dividendsProcessed: 4,
    errors: [],
    durationMs: 100,
  };

  const mockSuccessResultNoResults = {
    success: true,
    raceId: 'race-1',
    runnersProcessed: 5,
    oddsSnapshotsCaptured: 5,
    resultsProcessed: 0,
    dividendsProcessed: 0,
    errors: [],
    durationMs: 100,
  };

  beforeEach(() => {
    jest.clearAllMocks();

    mockFindMany = jest.fn();

    // Create mock Prisma client
    mockPrisma = {
      race: {
        findMany: mockFindMany,
        findUnique: jest.fn(),
      },
      jobRun: {
        create: jest.fn().mockResolvedValue({ id: 'test-job-run-id' }),
        update: jest.fn().mockResolvedValue({}),
      },
    };

    // Create mock API client
    mockApiClient = {} as jest.Mocked<TabApiClient>;

    // Get the mocked RaceService constructor
    const MockedRaceService = RaceService as jest.MockedClass<typeof RaceService>;
    MockedRaceService.mockClear();

    // Create scheduler
    scheduler = new PostRaceScheduler(mockApiClient, mockPrisma);

    // Get the mock instance
    mockRaceService = MockedRaceService.mock.instances[0] as jest.Mocked<RaceService>;
    mockRaceService.fetchAndStoreRaceDetails = jest.fn().mockResolvedValue(mockSuccessResultWithResults);
  });

  describe('race result fetching', () => {
    it('should fetch race details for each recently completed race', async () => {
      const now = DateTime.now().toUTC();
      // Races that started 10 minutes ago (within the 5-20 minute window)
      const completedTime = now.minus({ minutes: 10 });

      const mockRaces: Partial<Race>[] = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: completedTime.toJSDate(), status: 'OPEN' },
        { id: 'race-2', meetingId: 'meeting-A', raceNumber: 2, startTime: completedTime.plus({ minutes: 5 }).toJSDate(), status: 'OPEN' },
      ];

      mockFindMany.mockResolvedValue(mockRaces as Race[]);

      // Act
      const result = await scheduler.triggerManually();

      // Assert: fetchAndStoreRaceDetails called for each race with 'post_race' type
      expect(mockRaceService.fetchAndStoreRaceDetails).toHaveBeenCalledTimes(2);
      expect(mockRaceService.fetchAndStoreRaceDetails).toHaveBeenCalledWith('race-1', 'post_race');
      expect(mockRaceService.fetchAndStoreRaceDetails).toHaveBeenCalledWith('race-2', 'post_race');

      expect(result.itemsProcessed).toBe(2);
      expect(result.metadata?.racesUpdated).toBe(2);
    });

    it('should track results and dividends processed', async () => {
      const now = DateTime.now().toUTC();
      const completedTime = now.minus({ minutes: 10 });

      const mockRaces: Partial<Race>[] = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: completedTime.toJSDate(), status: 'OPEN' },
        { id: 'race-2', meetingId: 'meeting-A', raceNumber: 2, startTime: completedTime.toJSDate(), status: 'OPEN' },
      ];

      mockFindMany.mockResolvedValue(mockRaces as Race[]);

      // Act
      const result = await scheduler.triggerManually();

      // Assert: metadata includes results and dividends counts
      expect(result.metadata?.resultsProcessed).toBe(6); // 3 per race x 2 races
      expect(result.metadata?.dividendsProcessed).toBe(8); // 4 per race x 2 races
    });

    it('should track confirmed results when results are processed', async () => {
      const now = DateTime.now().toUTC();
      const completedTime = now.minus({ minutes: 10 });

      const mockRaces: Partial<Race>[] = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: completedTime.toJSDate(), status: 'OPEN' },
      ];

      mockFindMany.mockResolvedValue(mockRaces as Race[]);

      // Act
      const result = await scheduler.triggerManually();

      // Assert: confirmed results tracked
      expect(result.metadata?.confirmedResults).toHaveLength(1);
      expect(result.metadata?.confirmedResults[0]).toEqual({
        raceId: 'race-1',
        meetingId: 'meeting-A',
        raceNumber: 1,
      });
    });

    it('should track provisional results when no results processed yet', async () => {
      const now = DateTime.now().toUTC();
      const completedTime = now.minus({ minutes: 10 });

      const mockRaces: Partial<Race>[] = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: completedTime.toJSDate(), status: 'OPEN' },
      ];

      mockFindMany.mockResolvedValue(mockRaces as Race[]);
      mockRaceService.fetchAndStoreRaceDetails.mockResolvedValue(mockSuccessResultNoResults);

      // Act
      const result = await scheduler.triggerManually();

      // Assert: provisional results tracked
      expect(result.metadata?.provisionalResults).toHaveLength(1);
      expect(result.metadata?.provisionalResults[0]).toEqual({
        raceId: 'race-1',
        meetingId: 'meeting-A',
        raceNumber: 1,
        status: 'pending',
      });
    });

    it('should not call fetchAndStoreRaceDetails when no races are in window', async () => {
      mockFindMany.mockResolvedValue([]);

      // Act
      const result = await scheduler.triggerManually();

      // Assert
      expect(mockRaceService.fetchAndStoreRaceDetails).not.toHaveBeenCalled();
      expect(result.itemsProcessed).toBe(0);
    });
  });

  describe('error handling', () => {
    it('should continue processing other races when one fails', async () => {
      const now = DateTime.now().toUTC();
      const completedTime = now.minus({ minutes: 10 });

      const mockRaces: Partial<Race>[] = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: completedTime.toJSDate(), status: 'OPEN' },
        { id: 'race-2', meetingId: 'meeting-B', raceNumber: 1, startTime: completedTime.toJSDate(), status: 'OPEN' },
        { id: 'race-3', meetingId: 'meeting-C', raceNumber: 1, startTime: completedTime.toJSDate(), status: 'OPEN' },
      ];

      mockFindMany.mockResolvedValue(mockRaces as Race[]);

      // race-2 fails
      mockRaceService.fetchAndStoreRaceDetails
        .mockResolvedValueOnce(mockSuccessResultWithResults) // race-1 succeeds
        .mockResolvedValueOnce({ ...mockSuccessResultWithResults, success: false, errors: ['API error'] }) // race-2 fails
        .mockResolvedValueOnce(mockSuccessResultWithResults); // race-3 succeeds

      // Act
      const result = await scheduler.triggerManually();

      // Assert: all 3 races attempted, 1 failed
      expect(mockRaceService.fetchAndStoreRaceDetails).toHaveBeenCalledTimes(3);
      expect(result.errors.length).toBe(1);
      expect(result.itemsProcessed).toBe(2); // 2 succeeded
    });
  });
});
