// ABOUTME: Unit tests for MorningScrapeScheduler
// ABOUTME: Tests meeting discovery and race detail fetching

import { MorningScrapeScheduler } from '../morning-scrape-scheduler';
import { TabApiClient } from '../../api/tab';
import { MeetingService } from '../../services/meeting-service';
import { RaceService } from '../../services/race-service';

// 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 services
jest.mock('../../services/meeting-service');
jest.mock('../../services/race-service');

describe('MorningScrapeScheduler', () => {
  let scheduler: MorningScrapeScheduler;
  let mockPrisma: any;
  let mockApiClient: jest.Mocked<TabApiClient>;
  let mockMeetingService: jest.Mocked<MeetingService>;
  let mockRaceService: jest.Mocked<RaceService>;
  let mockRaceFindMany: jest.Mock;

  const mockFetchMeetingsResult = {
    meetingsProcessed: 2,
    racesProcessed: 10,
    errors: [],
  };

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

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

    mockRaceFindMany = jest.fn();

    // Create mock Prisma client
    mockPrisma = {
      race: {
        findMany: mockRaceFindMany,
      },
      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 service constructors
    const MockedMeetingService = MeetingService as jest.MockedClass<typeof MeetingService>;
    const MockedRaceService = RaceService as jest.MockedClass<typeof RaceService>;
    MockedMeetingService.mockClear();
    MockedRaceService.mockClear();

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

    // Get the mock instances
    mockMeetingService = MockedMeetingService.mock.instances[0] as jest.Mocked<MeetingService>;
    mockMeetingService.fetchAndStoreMeetings = jest.fn().mockResolvedValue(mockFetchMeetingsResult);

    mockRaceService = MockedRaceService.mock.instances[0] as jest.Mocked<RaceService>;
    mockRaceService.fetchAndStoreRaceDetails = jest.fn().mockResolvedValue(mockRaceDetailResult);

    // Default: no races to fetch details for
    mockRaceFindMany.mockResolvedValue([]);
  });

  describe('meeting discovery (Phase 1)', () => {
    it('should fetch meetings for all category/country/date combinations', async () => {
      // Act
      const result = await scheduler.triggerManually();

      // Assert: fetchAndStoreMeetings called for each combination
      // MORNING_SCRAPE_CONFIG has categories: ['T', 'H'], countries: ['AUS', 'NZL'], daysToFetch: 2
      // That's 2 categories * 2 countries * 2 days = 8 calls
      expect(mockMeetingService.fetchAndStoreMeetings).toHaveBeenCalledTimes(8);

      // Verify result structure
      expect(result.success).toBe(true);
      expect(result.itemsProcessed).toBeGreaterThan(0);
    });

    it('should track meetings and races by day', async () => {
      // Act
      const result = await scheduler.triggerManually();

      // Assert: metadata includes counts by day
      expect(result.metadata?.meetingsByDay).toBeDefined();
      expect(result.metadata?.racesByDay).toBeDefined();
    });

    it('should continue with other combinations when one fails', async () => {
      // Fail on second call
      mockMeetingService.fetchAndStoreMeetings
        .mockResolvedValueOnce(mockFetchMeetingsResult)
        .mockRejectedValueOnce(new Error('API error'))
        .mockResolvedValue(mockFetchMeetingsResult);

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

      // Assert: all combinations attempted
      expect(mockMeetingService.fetchAndStoreMeetings).toHaveBeenCalledTimes(8);
      expect(result.errors.length).toBeGreaterThan(0);
    });
  });

  describe('race detail fetching (Phase 2)', () => {
    it('should fetch detailed data for all races after meetings are discovered', async () => {
      // Setup: 3 races exist
      const mockRaces = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: new Date() },
        { id: 'race-2', meetingId: 'meeting-A', raceNumber: 2, startTime: new Date() },
        { id: 'race-3', meetingId: 'meeting-B', raceNumber: 1, startTime: new Date() },
      ];
      mockRaceFindMany.mockResolvedValue(mockRaces);

      // Act
      await scheduler.triggerManually();

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

    it('should track runners and odds snapshots processed', async () => {
      // Setup: 2 races
      const mockRaces = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: new Date() },
        { id: 'race-2', meetingId: 'meeting-A', raceNumber: 2, startTime: new Date() },
      ];
      mockRaceFindMany.mockResolvedValue(mockRaces);

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

      // Assert: metadata includes runner and odds counts
      expect(result.metadata?.runnersProcessed).toBe(10); // 5 per race x 2 races
      expect(result.metadata?.oddsSnapshotsCaptured).toBe(10);
    });

    it('should handle race detail fetch errors gracefully', async () => {
      // Setup: 2 races, second one fails
      const mockRaces = [
        { id: 'race-1', meetingId: 'meeting-A', raceNumber: 1, startTime: new Date() },
        { id: 'race-2', meetingId: 'meeting-A', raceNumber: 2, startTime: new Date() },
      ];
      mockRaceFindMany.mockResolvedValue(mockRaces);

      mockRaceService.fetchAndStoreRaceDetails
        .mockResolvedValueOnce(mockRaceDetailResult)
        .mockResolvedValueOnce({ ...mockRaceDetailResult, success: false, errors: ['API error'] });

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

      // Assert: both races attempted, errors captured
      expect(mockRaceService.fetchAndStoreRaceDetails).toHaveBeenCalledTimes(2);
      expect(jobResult.errors).toContain('race-2: API error');
    });
  });
});
