// ABOUTME: Unit tests for RaceService
// ABOUTME: Tests race detail fetching, runner/odds/results/dividend processing

import { RaceService } from '../race-service';
import { TabApiClient } from '../../api/tab';

// 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(),
  })),
}));

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,
  },
}));

describe('RaceService', () => {
  let raceService: RaceService;
  let mockApiClient: jest.Mocked<TabApiClient>;
  let mockPrisma: any;

  const mockRaceResponse = {
    header: {
      title: 'Event Details',
      generated_time: '2026-01-13T09:13:13Z',
      url: '/affiliates/v1/racing/events/race-id-1',
    },
    data: {
      race: {
        event_id: 'race-id-1',
        meeting_name: 'Yarra Valley',
        meeting_id: 'meeting-id-1',
        status: 'Final',
        description: 'Test Race',
        advertised_start: 1768271400,
        race_number: 1,
        type: 'T',
        country: 'AUS',
        state: 'VIC',
        distance: 1000,
        weather: 'Fine',
        track_condition: 'Good',
        positions_paid: 3,
        prize_monies: { '1st': 20000, '2nd': 5000, '3rd': 2500 },
      },
      runners: [
        {
          entrant_id: 'entrant-1',
          name: 'Fast Horse',
          is_scratched: false,
          scratch_time: 0,
          barrier: 1,
          runner_number: 1,
          jockey: 'J Smith',
          trainer_name: 'T Jones',
          trainer_location: 'Melbourne',
          age: 4,
          sex: 'Gelding',
          colour: 'Bay',
          sire: 'Big Sire',
          dam: 'Fast Dam',
          dam_sire: 'Old Sire',
          silk_colours: 'Red and White',
          last_twenty_starts: 'x1x2x3',
          odds: {
            fixed_win: 3.5,
            fixed_place: 1.6,
            pool_win: 3.8,
            pool_place: 1.7,
          },
        },
        {
          entrant_id: 'entrant-2',
          name: 'Slow Horse',
          is_scratched: true,
          scratch_time: 1768270000,
          barrier: 2,
          runner_number: 2,
          jockey: 'J Brown',
          trainer_name: 'T White',
          odds: {
            fixed_win: 10.0,
            fixed_place: 3.0,
          },
        },
      ],
      results: [
        {
          position: 1,
          name: 'Fast Horse',
          barrier: 1,
          runner_number: 1,
          margin_length: 0,
          entrant_id: 'entrant-1',
        },
      ],
      dividends: [
        {
          id: 'div-id-1',
          tote: 'VIC',
          product_name: 'Win',
          status: 'final',
          dividend: 3.5,
          pool_size: 50000,
          positions: [{ runner_number: 1, position: 1 }],
        },
        {
          id: 'div-id-2',
          tote: 'VIC',
          product_name: 'Place',
          status: 'final',
          dividend: 1.6,
          pool_size: 30000,
          positions: [{ runner_number: 1, position: 1 }],
        },
      ],
    },
  };

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

    // Create mock API client
    mockApiClient = {
      getRaceById: jest.fn().mockResolvedValue(mockRaceResponse),
    } as any;

    // Create mock Prisma client
    mockPrisma = {
      $transaction: jest.fn((fn) => fn(mockPrisma)),
      runner: {
        upsert: jest.fn().mockResolvedValue({ id: 'runner-db-id' }),
        findFirst: jest.fn().mockResolvedValue({ id: 'runner-db-id' }),
      },
      result: {
        upsert: jest.fn().mockResolvedValue({}),
      },
      dividend: {
        upsert: jest.fn().mockResolvedValue({}),
      },
      oddsSnapshot: {
        upsert: jest.fn().mockResolvedValue({}),
      },
      race: {
        findUnique: jest.fn().mockResolvedValue({ id: 'race-id-1', meetingId: 'meeting-id-1' }),
        update: jest.fn().mockResolvedValue({}),
      },
    };

    raceService = new RaceService(mockApiClient, mockPrisma);
  });

  describe('fetchAndStoreRaceDetails', () => {
    it('should fetch race details from API and process all data', async () => {
      const result = await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      expect(mockApiClient.getRaceById).toHaveBeenCalledWith('race-id-1');
      expect(result.success).toBe(true);
      expect(result.runnersProcessed).toBe(2);
    });

    it('should capture odds snapshot with correct type', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      // Should capture odds for non-scratched runners
      expect(mockPrisma.oddsSnapshot.upsert).toHaveBeenCalled();
      const upsertCall = mockPrisma.oddsSnapshot.upsert.mock.calls[0][0];
      expect(upsertCall.create.snapshotType).toBe('t60');
    });

    it('should process results when race status is Final', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'post_race');

      expect(mockPrisma.result.upsert).toHaveBeenCalled();
    });

    it('should process dividends when race status is Final', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'post_race');

      expect(mockPrisma.dividend.upsert).toHaveBeenCalledTimes(2); // Win and Place
    });

    it('should not process results/dividends when race is not Final', async () => {
      mockApiClient.getRaceById.mockResolvedValue({
        ...mockRaceResponse,
        data: {
          ...mockRaceResponse.data,
          race: { ...mockRaceResponse.data.race, status: 'Open' },
        },
      });

      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      expect(mockPrisma.result.upsert).not.toHaveBeenCalled();
      expect(mockPrisma.dividend.upsert).not.toHaveBeenCalled();
    });

    it('should handle API errors gracefully', async () => {
      mockApiClient.getRaceById.mockRejectedValue(new Error('API Error'));

      const result = await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      expect(result.success).toBe(false);
      expect(result.errors).toContain('API Error');
    });
  });

  describe('processRunners', () => {
    it('should upsert all runners with correct data', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      expect(mockPrisma.runner.upsert).toHaveBeenCalledTimes(2);

      // Check first runner upsert
      const firstCall = mockPrisma.runner.upsert.mock.calls[0][0];
      expect(firstCall.create.horseName).toBe('Fast Horse');
      expect(firstCall.create.entrantId).toBe('entrant-1');
      expect(firstCall.create.age).toBe(4);
      expect(firstCall.create.sire).toBe('Big Sire');
    });

    it('should mark scratched runners correctly', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      const scratchedRunnerCall = mockPrisma.runner.upsert.mock.calls[1][0];
      expect(scratchedRunnerCall.create.scratched).toBe(true);
    });

    it('should update currentOdds on runner', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      const firstCall = mockPrisma.runner.upsert.mock.calls[0][0];
      expect(firstCall.update.currentOdds).toBe(3.5);
    });
  });

  describe('captureOddsSnapshot', () => {
    it('should map scrape type to snapshot type correctly', async () => {
      // Test morning scrape -> morning
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'morning_scrape');
      let call = mockPrisma.oddsSnapshot.upsert.mock.calls[0][0];
      expect(call.create.snapshotType).toBe('morning');

      jest.clearAllMocks();

      // Test pre_race_t15 -> t15
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t15');
      call = mockPrisma.oddsSnapshot.upsert.mock.calls[0][0];
      expect(call.create.snapshotType).toBe('t15');

      jest.clearAllMocks();

      // Test post_race -> final
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'post_race');
      call = mockPrisma.oddsSnapshot.upsert.mock.calls[0][0];
      expect(call.create.snapshotType).toBe('final');
    });

    it('should store all odds types in snapshot', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      const call = mockPrisma.oddsSnapshot.upsert.mock.calls[0][0];
      expect(call.create.fixedWin).toBe(3.5);
      expect(call.create.fixedPlace).toBe(1.6);
      expect(call.create.poolWin).toBe(3.8);
      expect(call.create.poolPlace).toBe(1.7);
    });

    it('should skip scratched runners for odds snapshot', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'pre_race_t60');

      // Only 1 odds snapshot (for non-scratched runner)
      expect(mockPrisma.oddsSnapshot.upsert).toHaveBeenCalledTimes(1);
    });
  });

  describe('processDividends', () => {
    it('should store dividend with correct data', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'post_race');

      const winDividendCall = mockPrisma.dividend.upsert.mock.calls[0][0];
      expect(winDividendCall.create.tote).toBe('VIC');
      expect(winDividendCall.create.productName).toBe('Win');
      expect(winDividendCall.create.dividend).toBe(3.5);
      expect(winDividendCall.create.poolSize).toBe(50000);
    });

    it('should store positions as JSON', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'post_race');

      const call = mockPrisma.dividend.upsert.mock.calls[0][0];
      expect(call.create.positions).toEqual([{ runner_number: 1, position: 1 }]);
    });
  });

  describe('processResults', () => {
    it('should store result with correct position and margin', async () => {
      await raceService.fetchAndStoreRaceDetails('race-id-1', 'post_race');

      expect(mockPrisma.result.upsert).toHaveBeenCalled();
      const call = mockPrisma.result.upsert.mock.calls[0][0];
      expect(call.create.finishPosition).toBe(1);
      expect(call.create.margin).toBe(0);
    });

    it('should link result to correct runner by entrant_id', async () => {
      // Setup: findFirst returns the runner
      mockPrisma.runner.findFirst.mockResolvedValue({ id: 'runner-db-id-for-entrant-1' });

      await raceService.fetchAndStoreRaceDetails('race-id-1', 'post_race');

      expect(mockPrisma.runner.findFirst).toHaveBeenCalledWith({
        where: {
          raceId: 'race-id-1',
          entrantId: 'entrant-1',
        },
      });
    });
  });
});
