// Unit: PropLine adapter (Session 30). 3-key rotation + normalization.

const mockAxiosGet = jest.fn();
jest.mock('axios', () => ({ get: (...a) => mockAxiosGet(...a) }));

// Gateway is a pass-through in tests (quota allowed).
jest.mock('../../src/services/providerGateway', () => ({
  fetch: jest.fn(async (_id, cb) => cb('propline')),
}));

const mockRedisStore = {};
jest.mock('../../src/utils/redis', () => ({
  getRedisClient: () => ({
    get: async (k) => (k in mockRedisStore ? String(mockRedisStore[k]) : null),
    incr: async (k) => { mockRedisStore[k] = (mockRedisStore[k] || 0) + 1; return mockRedisStore[k]; },
    expire: async () => 1,
  }),
  isDegraded: () => false,
}));

const adapter = require('../../src/services/adapters/proplineAdapter');

const KEYS = { PROPLINE_API_KEY_1: 'k1', PROPLINE_API_KEY_2: 'k2', PROPLINE_API_KEY_3: 'k3' };

beforeEach(() => {
  mockAxiosGet.mockReset();
  for (const k of Object.keys(mockRedisStore)) delete mockRedisStore[k];
  for (const k of Object.keys(adapter.__internals.memUsage)) delete adapter.__internals.memUsage[k];
  Object.assign(process.env, KEYS);
});
afterAll(() => {
  delete process.env.PROPLINE_API_KEY_1;
  delete process.env.PROPLINE_API_KEY_2;
  delete process.env.PROPLINE_API_KEY_3;
});

const SAMPLE = [{
  id: '43866',
  sport_key: 'baseball_mlb',
  home_team: 'Cincinnati Reds',
  away_team: 'Arizona Diamondbacks',
  commence_time: '2026-06-14T02:05:00Z',
  bookmakers: [{
    key: 'betmgm', title: 'BetMGM', last_update: '2026-06-14T00:45:30Z',
    markets: [{
      key: 'batter_hits', last_update: '2026-06-13T15:12:46Z',
      outcomes: [
        { name: 'Over', description: 'Braxton Fulford', price: -200, point: 0.5 },
        { name: 'Under', description: 'Braxton Fulford', price: 160, point: 0.5 },
      ],
    }],
  }],
}];

describe('proplineAdapter — config + URL', () => {
  test('hasKeys true when any key set', () => {
    expect(adapter.hasKeys()).toBe(true);
  });

  test('builds the odds URL with apiKey query param + markets', async () => {
    mockAxiosGet.mockResolvedValue({ data: SAMPLE });
    await adapter.fetchRaw('mlb');
    const [url, opts] = mockAxiosGet.mock.calls[0];
    expect(url).toBe('https://api.prop-line.com/v1/sports/baseball_mlb/odds');
    expect(opts.params.apiKey).toMatch(/^k[123]$/);
    expect(opts.params.markets).toContain('batter_hits');
  });

  test('unsupported sport → null without calling axios', async () => {
    expect(await adapter.fetchRaw('cricket')).toBeNull();
    expect(mockAxiosGet).not.toHaveBeenCalled();
  });
});

describe('proplineAdapter — normalization (Odds-API-compatible)', () => {
  test('getProps normalizes into VYNDR prop shape (MLB market mapped)', async () => {
    mockAxiosGet.mockResolvedValue({ data: SAMPLE });
    const out = await adapter.getProps('mlb');
    expect(out.source).toBe('propline');
    expect(out.props).toHaveLength(1);
    const p = out.props[0];
    expect(p.player).toBe('Braxton Fulford');
    expect(p.stat_type).toBe('hits');        // batter_hits → hits via MARKET_MAP
    expect(p.line).toBe(0.5);
    expect(p.over_odds).toBe(-200);
    expect(p.under_odds).toBe(160);
    expect(p.book).toBe('betmgm');
  });

  test('tolerates { data: [...] } wrapper', async () => {
    mockAxiosGet.mockResolvedValue({ data: { data: SAMPLE } });
    const raw = await adapter.fetchRaw('mlb');
    expect(raw).toHaveLength(1);
  });

  test('error → getProps returns null (caller falls back)', async () => {
    mockAxiosGet.mockRejectedValue(new Error('upstream 500'));
    expect(await adapter.getProps('mlb')).toBeNull();
  });
});

describe('proplineAdapter — 3-key rotation', () => {
  test('picks the least-used key', async () => {
    const { incrUsage, usageKey } = adapter.__internals;
    // Make key 0 heavily used, key 1 lightly, key 2 medium.
    mockRedisStore[usageKey(0)] = 500;
    mockRedisStore[usageKey(2)] = 100;
    const picked = await adapter.pickKey(adapter.__internals.getKeys());
    expect(picked.index).toBe(1); // unused → most remaining
  });

  test('rotates OFF a key once it crosses the 900 threshold', async () => {
    const { usageKey } = adapter.__internals;
    mockRedisStore[usageKey(0)] = 950; // over threshold
    mockRedisStore[usageKey(1)] = 950; // over threshold
    mockRedisStore[usageKey(2)] = 10;  // healthy
    const picked = await adapter.pickKey(adapter.__internals.getKeys());
    expect(picked.index).toBe(2);
  });

  test('all keys exhausted (>=1000) → pickKey null, fetchRaw null', async () => {
    const { usageKey } = adapter.__internals;
    mockRedisStore[usageKey(0)] = 1000;
    mockRedisStore[usageKey(1)] = 1000;
    mockRedisStore[usageKey(2)] = 1000;
    expect(await adapter.pickKey(adapter.__internals.getKeys())).toBeNull();
    mockAxiosGet.mockResolvedValue({ data: SAMPLE });
    expect(await adapter.fetchRaw('mlb')).toBeNull();
    expect(mockAxiosGet).not.toHaveBeenCalled();
  });

  test('increments usage on a successful call', async () => {
    mockAxiosGet.mockResolvedValue({ data: SAMPLE });
    await adapter.fetchRaw('mlb');
    const total = Object.entries(mockRedisStore)
      .filter(([k]) => k.startsWith('propline:usage:'))
      .reduce((s, [, v]) => s + v, 0);
    expect(total).toBe(1);
  });

  test('no keys configured → fetchRaw null', async () => {
    delete process.env.PROPLINE_API_KEY_1;
    delete process.env.PROPLINE_API_KEY_2;
    delete process.env.PROPLINE_API_KEY_3;
    expect(adapter.hasKeys()).toBe(false);
    expect(await adapter.fetchRaw('mlb')).toBeNull();
  });
});
