// PERF-1 (Session 7d): /api/analyze caches via Redis. Second identical
// call must hit cache (returns _cache:'HIT', does not re-invoke the
// analyzer). Different keys still miss.
// ARCH-1 (Session 7g): the analyzer target moved from propAnalyzer to
// the unified analyzeViaEngine1. Mock target updated to follow; the
// caching behavior under test is unchanged.

const mockAnalyze = jest.fn();
jest.mock('../../src/services/intelligence/analyzeViaEngine1', () => ({
  analyzeViaEngine1: (...args) => mockAnalyze(...args),
}));

const mockStore = new Map();
jest.mock('../../src/utils/redis', () => ({
  cacheGet: async (k) => mockStore.get(k) ?? null,
  cacheSet: async (k, v) => { mockStore.set(k, v); return true; },
  cacheDel: async (k) => { mockStore.delete(k); return true; },
  isDegraded: () => false,
}));

// Session 7h: scan-limit middleware now mounts on /api/analyze. Reset
// between tests so the cumulative anonymous-IP quota doesn't leak
// across cases and 429 the cache assertions.
const { __internals: mockScanLimitInternals } = require('../../src/middleware/scanLimit');
beforeEach(() => mockScanLimitInternals.resetForTests());

const express = require('express');
const analyze = require('../../src/routes/analyze');

function makeApp() {
  const app = express();
  app.use(express.json());
  app.use('/api/analyze', analyze);
  return app;
}

function call(app, path, body, ip = '10.0.0.1') {
  return new Promise((resolve) => {
    const http = require('http');
    const server = app.listen(0, '127.0.0.1', () => {
      const port = server.address().port;
      const data = JSON.stringify(body);
      const req = http.request({
        host: '127.0.0.1', port, path, method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(data),
          'X-Forwarded-For': ip,
        },
      }, (res) => {
        const chunks = [];
        res.on('data', (c) => chunks.push(c));
        res.on('end', () => {
          server.close();
          const raw = Buffer.concat(chunks).toString('utf8');
          let parsed; try { parsed = JSON.parse(raw); } catch { parsed = raw; }
          resolve({ status: res.statusCode, body: parsed });
        });
      });
      req.write(data);
      req.end();
    });
  });
}

beforeEach(() => {
  mockAnalyze.mockReset();
  mockStore.clear();
});

describe('/api/analyze caching (PERF-1)', () => {
  test('first call is a MISS, second identical call is a HIT', async () => {
    mockAnalyze.mockResolvedValue({
      player: 'Jalen Brunson', stat_type: 'points', line: 26.5, direction: 'over',
      grade: 'A-', confidence: 0.78,
    });
    const app = makeApp();
    const body = { player: 'Jalen Brunson', stat_type: 'points', line: 26.5, direction: 'over' };

    const first = await call(app, '/api/analyze/prop', body, '11.11.11.11');
    expect(first.status).toBe(200);
    expect(first.body._cache).toBe('MISS');

    const second = await call(app, '/api/analyze/prop', body, '11.11.11.11');
    expect(second.status).toBe(200);
    expect(second.body._cache).toBe('HIT');

    // analyzeProp was invoked exactly once — the second call hit cache.
    expect(mockAnalyze).toHaveBeenCalledTimes(1);
  });

  test('different prop = different cache key = both MISS', async () => {
    mockAnalyze.mockResolvedValue({ grade: 'B+' });
    const app = makeApp();
    const a = await call(app, '/api/analyze/prop',
      { player: 'A', stat_type: 'points', line: 20, direction: 'over' }, '12.12.12.12');
    const b = await call(app, '/api/analyze/prop',
      { player: 'B', stat_type: 'points', line: 20, direction: 'over' }, '12.12.12.12');
    expect(a.body._cache).toBe('MISS');
    expect(b.body._cache).toBe('MISS');
    expect(mockAnalyze).toHaveBeenCalledTimes(2);
  });

  test('cache key normalizes player-name case (refresh/typo proof)', async () => {
    // stat_type + direction are constrained to a lowercase enum by the
    // validator, but the player name is free-form. The cache key
    // normalizer should treat 'OG Anunoby' and 'og anunoby' as identical.
    mockAnalyze.mockResolvedValue({ grade: 'A' });
    const app = makeApp();
    await call(app, '/api/analyze/prop',
      { player: 'OG Anunoby', stat_type: 'points', line: 12.5, direction: 'over' }, '13.13.13.13');
    const second = await call(app, '/api/analyze/prop',
      { player: 'og anunoby', stat_type: 'points', line: 12.5, direction: 'over' }, '13.13.13.13');
    expect(second.body._cache).toBe('HIT');
    expect(mockAnalyze).toHaveBeenCalledTimes(1);
  });
});
