/**
 * Unit tests for TabApiClient
 * Following TDD approach - tests written before implementation
 */

import { TabApiClient } from '../tab-api-client';
import axios, { AxiosInstance } from 'axios';
import { GetMeetingsOptions } from '../types';

// Mock dependencies
jest.mock('axios', () => ({
  __esModule: true,
  default: {
    create: jest.fn(),
    isAxiosError: jest.fn((error) => error && (error.response !== undefined || error.code !== undefined)),
  },
  isAxiosError: jest.fn((error) => error && (error.response !== undefined || error.code !== undefined)),
}));
jest.mock('axios-retry', () => ({
  __esModule: true,
  default: jest.fn(),
  exponentialDelay: jest.fn(() => 1000),
  isNetworkOrIdempotentRequestError: jest.fn(() => true),
}));
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,
  },
}));
jest.mock('bottleneck', () => {
  return jest.fn().mockImplementation(() => ({
    schedule: jest.fn((fn) => Promise.resolve().then(() => fn())),
    counts: jest.fn(() => ({
      RUNNING: 0,
      QUEUED: 0,
    })),
  }));
});

const mockedAxios = axios as jest.Mocked<typeof axios> & { default: jest.Mocked<typeof axios> };

describe('TabApiClient', () => {
  let client: TabApiClient;
  let mockAxiosInstance: jest.Mocked<AxiosInstance>;

  beforeEach(() => {
    // Reset mocks before each test
    jest.clearAllMocks();

    // Create mock axios instance
    mockAxiosInstance = {
      get: jest.fn(),
      post: jest.fn(),
      defaults: { headers: { common: {} } },
    } as any;

    (mockedAxios.default?.create ?? mockedAxios.create).mockReturnValue(mockAxiosInstance);

    // Create client instance
    client = new TabApiClient({
      baseUrl: 'https://api.tab.com.au',
      timeout: 5000,
      maxRetries: 3,
      retryDelay: 1000,
      rateLimitPerMinute: 100,
    });
  });

  describe('constructor', () => {
    it('should create axios instance with correct config', () => {
      expect(mockedAxios.create).toHaveBeenCalledWith(
        expect.objectContaining({
          baseURL: 'https://api.tab.com.au',
          timeout: 5000,
        })
      );
    });

    it('should use default config when not provided', () => {
      const defaultClient = new TabApiClient({
        baseUrl: 'https://api.tab.com.au',
      });

      expect(defaultClient).toBeDefined();
    });
  });

  describe('getMeetings', () => {
    const mockMeetingsResponse = {
      header: {
        title: 'Race Meetings',
        generated_time: '2026-01-13T09:11:46.722887528Z',
        url: '/affiliates/v1/racing/meetings',
      },
      params: {
        enc: 'json',
        ids: null,
        date_from: '2026-01-13T00:00:00Z',
        date_to: '2026-01-13T00:00:00Z',
        type: 'T',
        country: 'AUS',
        limit: 100,
        offset: 0,
        futures: false,
        meeting_numbers: null,
      },
      data: {
        meetings: [
          {
            meeting: '6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef',
            name: 'Yarra Valley',
            date: '2026-01-13T00:00:00Z',
            track_condition: 'Good',
            category: 'T' as const,
            category_name: 'Thoroughbred Horse Racing',
            country: 'AUS',
            state: 'VIC',
            races: [
              {
                id: 'cb9d4f39-c7ce-4778-a0be-f3899d86f1bd',
                race_number: 1,
                name: 'Bet365 Top Finishes Maiden Plate',
                start_time: '2026-01-13T02:30:00Z',
                tote_start_time: '15:30:00',
                track_condition: 'Good',
                distance: 1000,
                weather: 'OCAST',
                country: 'AUS',
                state: 'VIC',
                status: 'Final',
              },
            ],
          },
        ],
      },
    };

    it('should successfully fetch meetings with default options', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockMeetingsResponse });

      const result = await client.getMeetings();

      expect(mockAxiosInstance.get).toHaveBeenCalledWith(
        '/affiliates/v1/racing/meetings',
        expect.objectContaining({
          params: expect.objectContaining({
            enc: 'json',
          }),
        })
      );
      expect(result.data.meetings).toHaveLength(1);
      expect(result.data.meetings[0].name).toBe('Yarra Valley');
    });

    it('should fetch meetings with custom options', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockMeetingsResponse });

      const options: GetMeetingsOptions = {
        category: 'T',
        country: 'AUS',
        dateFrom: '2026-01-13',
        dateTo: '2026-01-13',
        futures: false,
        limit: 50,
        offset: 0,
      };

      await client.getMeetings(options);

      expect(mockAxiosInstance.get).toHaveBeenCalledWith(
        '/affiliates/v1/racing/meetings',
        expect.objectContaining({
          params: expect.objectContaining({
            category: 'T',
            country: 'AUS',
            date_from: '2026-01-13',
            date_to: '2026-01-13',
            futures: false,
            limit: 50,
            offset: 0,
          }),
        })
      );
    });

    it('should validate response with Zod schema', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockMeetingsResponse });

      const result = await client.getMeetings();

      // If validation passes, result should have correct type
      expect(result.data.meetings[0].meeting).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
      );
    });

    it('should throw error when response validation fails', async () => {
      const invalidResponse = {
        ...mockMeetingsResponse,
        data: { meetings: [{ invalid: 'data' }] },
      };

      mockAxiosInstance.get.mockResolvedValue({ data: invalidResponse });

      await expect(client.getMeetings()).rejects.toThrow();
    });

    it('should handle 404 error gracefully', async () => {
      const error = {
        response: {
          status: 404,
          data: {
            error: {
              code: 'NOT_FOUND',
              message: 'No meetings found',
            },
          },
        },
      };

      mockAxiosInstance.get.mockRejectedValue(error);

      await expect(client.getMeetings()).rejects.toThrow('No meetings found');
    });

    it('should handle 500 error with retry', async () => {
      const error = {
        response: {
          status: 500,
          data: {
            error: {
              code: 'INTERNAL_ERROR',
              message: 'Server error',
            },
          },
        },
      };

      mockAxiosInstance.get.mockRejectedValue(error);

      await expect(client.getMeetings()).rejects.toThrow();
    });

    it('should handle network timeout', async () => {
      const error = {
        code: 'ECONNABORTED',
        message: 'timeout of 5000ms exceeded',
        request: {}, // Indicates request was made but no response received
      };

      mockAxiosInstance.get.mockRejectedValue(error);

      await expect(client.getMeetings()).rejects.toThrow('timeout');
    });
  });

  describe('getMeetingById', () => {
    const mockMeetingResponse = {
      header: {
        title: 'Get meeting by id',
        generated_time: '2026-01-13T09:12:36.571668056Z',
        url: '/affiliates/v1/racing/meetings/6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef',
      },
      params: {
        enc: 'json',
        id: '6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef',
      },
      data: {
        meetings: [
          {
            meeting: '6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef',
            name: 'Yarra Valley',
            date: '2026-01-13T00:00:00Z',
            track_condition: 'Good',
            category: 'T' as const,
            category_name: 'Thoroughbred Horse Racing',
            country: 'AUS',
            state: 'VIC',
            races: [],
          },
        ],
      },
    };

    it('should fetch meeting by ID successfully', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockMeetingResponse });

      const meetingId = '6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef';
      const result = await client.getMeetingById(meetingId);

      expect(mockAxiosInstance.get).toHaveBeenCalledWith(
        `/affiliates/v1/racing/meetings/${meetingId}`,
        expect.objectContaining({
          params: { enc: 'json' },
        })
      );
      expect(result.data.meetings).toHaveLength(1);
      expect(result.data.meetings[0].meeting).toBe(meetingId);
    });

    it('should throw error for invalid UUID', async () => {
      await expect(client.getMeetingById('invalid-uuid')).rejects.toThrow();
    });

    it('should handle meeting not found', async () => {
      const error = {
        response: {
          status: 404,
          data: {
            error: {
              code: 'NOT_FOUND',
              message: 'Meeting not found',
            },
          },
        },
      };

      mockAxiosInstance.get.mockRejectedValue(error);

      await expect(
        client.getMeetingById('6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef')
      ).rejects.toThrow('Meeting not found');
    });
  });

  describe('rate limiting', () => {
    it('should respect rate limit configuration', async () => {
      const mockResponse = {
        header: { title: 'Test', generated_time: '2026-01-13T09:11:46Z', url: '/' },
        params: {
          enc: 'json',
          date_from: '2026-01-13T00:00:00Z',
          date_to: '2026-01-13T00:00:00Z',
        },
        data: { meetings: [] },
      };

      mockAxiosInstance.get.mockResolvedValue({ data: mockResponse });

      // Make multiple rapid requests
      const requests = Array.from({ length: 5 }, () => client.getMeetings());

      await Promise.all(requests);

      // Should be rate limited (implementation will control this)
      expect(mockAxiosInstance.get).toHaveBeenCalledTimes(5);
    });
  });

  describe('error handling', () => {
    it('should include request context in error logs', async () => {
      const error = new Error('Network error');
      mockAxiosInstance.get.mockRejectedValue(error);

      try {
        await client.getMeetings({ category: 'T', country: 'AUS' });
      } catch (e) {
        // Error should be thrown with context
        expect(e).toBeDefined();
      }
    });

    it('should handle malformed response data', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: null });

      await expect(client.getMeetings()).rejects.toThrow();
    });
  });

  describe('metrics and observability', () => {
    it('should emit request duration metrics', async () => {
      const mockResponse = {
        header: { title: 'Test', generated_time: '2026-01-13T09:11:46Z', url: '/' },
        params: {
          enc: 'json',
          date_from: '2026-01-13T00:00:00Z',
          date_to: '2026-01-13T00:00:00Z',
        },
        data: { meetings: [] },
      };

      mockAxiosInstance.get.mockResolvedValue({ data: mockResponse });

      await client.getMeetings();

      // Metrics should be recorded (checked via mock)
      // Implementation will handle actual metrics
      expect(mockAxiosInstance.get).toHaveBeenCalled();
    });

    it('should emit error count metrics', async () => {
      const error = new Error('API error');
      mockAxiosInstance.get.mockRejectedValue(error);

      try {
        await client.getMeetings();
      } catch (e) {
        // Error metric should be incremented
        expect(e).toBeDefined();
      }
    });
  });

  describe('getRaceById', () => {
    const mockRaceResponse = {
      header: {
        title: 'Event Details: cb9d4f39-c7ce-4778-a0be-f3899d86f1bd',
        generated_time: '2026-01-13T09:13:13.837898925Z',
        url: '/affiliates/v1/racing/events/cb9d4f39-c7ce-4778-a0be-f3899d86f1bd?enc=json',
      },
      params: {
        enc: 'json',
        id: 'cb9d4f39-c7ce-4778-a0be-f3899d86f1bd',
      },
      data: {
        race: {
          event_id: 'cb9d4f39-c7ce-4778-a0be-f3899d86f1bd',
          meeting_name: 'Yarra Valley',
          meeting_id: '6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef',
          status: 'Final',
          description: 'Bet365 Top Finishes Maiden Plate',
          advertised_start: 1768271400,
          actual_start: 1768271560,
          race_number: 1,
          type: 'T',
          country: 'AUS',
          state: 'VIC',
          distance: 1000,
          weather: 'overcast',
          track_condition: 'Good',
          track_direction: 'Left',
          entrant_count: 9,
          field_size: 6,
          positions_paid: 2,
        },
        runners: [
          {
            entrant_id: '86d775ee-9961-478e-848a-4b947edb639a',
            name: 'Koko',
            is_scratched: false,
            scratch_time: 0,
            barrier: 5,
            runner_number: 7,
            jockey: 'Damien Thornton',
            trainer_name: 'David & Ben Hayes & Tom Dabernig',
            odds: {
              fixed_win: 2.9,
              fixed_place: 1.4,
            },
          },
        ],
        results: [
          {
            position: 1,
            name: "Artie's Storm",
            barrier: 2,
            runner_number: 2,
            margin_length: 0,
            entrant_id: '37d27dcb-4234-4bff-b2c0-a29079ff366a',
          },
        ],
        dividends: [
          {
            id: 'a1b2c3d4-e5f6-7a8b-9c0d-e1f2a3b4c5d6',
            tote: 'VIC',
            product_name: 'Win',
            status: 'final',
            dividend: 6.5,
            pool_size: 5000,
            positions: [{ runner_number: 2, position: 1 }],
          },
        ],
      },
    };

    it('should fetch race by ID successfully', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockRaceResponse });

      const raceId = 'cb9d4f39-c7ce-4778-a0be-f3899d86f1bd';
      const result = await client.getRaceById(raceId);

      expect(mockAxiosInstance.get).toHaveBeenCalledWith(
        `/affiliates/v1/racing/events/${raceId}`,
        expect.objectContaining({
          params: { enc: 'json' },
        })
      );
      expect(result.data.race.event_id).toBe(raceId);
      expect(result.data.runners).toHaveLength(1);
      expect(result.data.results).toHaveLength(1);
    });

    it('should throw error for invalid UUID', async () => {
      await expect(client.getRaceById('invalid-uuid')).rejects.toThrow();
    });

    it('should handle race not found', async () => {
      const error = {
        response: {
          status: 404,
          data: {
            error: {
              code: 'NOT_FOUND',
              message: 'Race not found',
            },
          },
        },
      };

      mockAxiosInstance.get.mockRejectedValue(error);

      await expect(
        client.getRaceById('cb9d4f39-c7ce-4778-a0be-f3899d86f1bd')
      ).rejects.toThrow('Race not found');
    });

    it('should validate response with Zod schema', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockRaceResponse });

      const result = await client.getRaceById('cb9d4f39-c7ce-4778-a0be-f3899d86f1bd');

      // If validation passes, result should have correct structure
      expect(result.data.race.event_id).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
      );
      expect(result.data.runners[0].entrant_id).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
      );
    });

    it('should throw error when response validation fails', async () => {
      const invalidResponse = {
        ...mockRaceResponse,
        data: { race: { invalid: 'data' }, runners: [] },
      };

      mockAxiosInstance.get.mockResolvedValue({ data: invalidResponse });

      await expect(
        client.getRaceById('cb9d4f39-c7ce-4778-a0be-f3899d86f1bd')
      ).rejects.toThrow();
    });

    it('should include runner details in response', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockRaceResponse });

      const result = await client.getRaceById('cb9d4f39-c7ce-4778-a0be-f3899d86f1bd');

      const runner = result.data.runners[0];
      expect(runner.name).toBe('Koko');
      expect(runner.jockey).toBe('Damien Thornton');
      expect(runner.odds?.fixed_win).toBe(2.9);
    });

    it('should include dividends when race is final', async () => {
      mockAxiosInstance.get.mockResolvedValue({ data: mockRaceResponse });

      const result = await client.getRaceById('cb9d4f39-c7ce-4778-a0be-f3899d86f1bd');

      expect(result.data.dividends).toBeDefined();
      expect(result.data.dividends![0].dividend).toBe(6.5);
    });
  });
});
