const { createRateLimit } = require('../../src/middleware/rateLimit');

function mockReqRes(ip = '1.2.3.4') {
  const req = { ip, headers: {}, socket: { remoteAddress: ip } };
  const headers = {};
  const res = {
    statusCode: 200,
    body: null,
    set(k, v) { headers[k] = v; return res; },
    status(c) { res.statusCode = c; return res; },
    json(b) { res.body = b; return res; },
  };
  return { req, res, headers };
}

describe('rateLimit middleware', () => {
  test('allows under-limit calls', () => {
    const mw = createRateLimit({ windowMs: 60_000, max: 3 });
    const next = jest.fn();
    for (let i = 0; i < 3; i += 1) {
      const { req, res } = mockReqRes();
      mw(req, res, next);
    }
    expect(next).toHaveBeenCalledTimes(3);
  });

  test('429s on the (max+1)th call within the window', () => {
    const mw = createRateLimit({ windowMs: 60_000, max: 2 });
    const next = jest.fn();
    let lastRes;
    for (let i = 0; i < 3; i += 1) {
      const { req, res } = mockReqRes('5.5.5.5');
      mw(req, res, next);
      lastRes = res;
    }
    expect(next).toHaveBeenCalledTimes(2);
    expect(lastRes.statusCode).toBe(429);
    expect(lastRes.body).toEqual({ error: 'Too many requests' });
  });

  test('429 response carries Retry-After header', () => {
    const mw = createRateLimit({ windowMs: 60_000, max: 1 });
    const next = jest.fn();
    let lastHeaders;
    let lastRes;
    for (let i = 0; i < 2; i += 1) {
      const { req, res, headers } = mockReqRes('6.6.6.6');
      mw(req, res, next);
      lastRes = res; lastHeaders = headers;
    }
    expect(lastRes.statusCode).toBe(429);
    expect(lastHeaders['Retry-After']).toBeDefined();
    expect(parseInt(lastHeaders['Retry-After'], 10)).toBeGreaterThan(0);
  });

  test('different IPs each get their own quota', () => {
    const mw = createRateLimit({ windowMs: 60_000, max: 1 });
    const next = jest.fn();
    const r1 = mockReqRes('7.7.7.7');
    const r2 = mockReqRes('8.8.8.8');
    mw(r1.req, r1.res, next);
    mw(r2.req, r2.res, next);
    expect(next).toHaveBeenCalledTimes(2);
    expect(r1.res.statusCode).toBe(200);
    expect(r2.res.statusCode).toBe(200);
  });

  test('window expiry releases the quota', async () => {
    const mw = createRateLimit({ windowMs: 60, max: 1 });
    const next = jest.fn();
    const a = mockReqRes('9.9.9.9');
    mw(a.req, a.res, next); // counts as 1
    const b = mockReqRes('9.9.9.9');
    mw(b.req, b.res, next); // 429
    expect(b.res.statusCode).toBe(429);
    await new Promise((r) => setTimeout(r, 90));
    const c = mockReqRes('9.9.9.9');
    mw(c.req, c.res, next); // back to OK
    expect(next).toHaveBeenCalledTimes(2);
  });

  test('eviction trims the IP table when capacity exceeded', () => {
    const { __internals } = require('../../src/middleware/rateLimit');
    // We can't directly read the internal Map, so just confirm the
    // module exposes MAX_TRACKED_IPS as a sanity floor.
    expect(__internals.MAX_TRACKED_IPS).toBeGreaterThanOrEqual(1000);
  });
});

describe('analyze routes wired to the middleware', () => {
  test('hits a 429 on the 11th identical-IP call', async () => {
    const express = require('express');
    process.env.NBA_STATS_URL = 'http://localhost:9999'; // unreachable, but
    // the rate limit fires before we ever try to call upstream.

    const analyze = require('../../src/routes/analyze');
    const app = express();
    app.use(express.json());
    app.use('/api/analyze', analyze);

    const http = require('http');
    const server = await new Promise((resolve) => {
      const s = app.listen(0, '127.0.0.1', () => resolve(s));
    });
    const port = server.address().port;

    function postOnce(path = '/api/analyze/prop') {
      return new Promise((resolve) => {
        const req = http.request({
          host: '127.0.0.1', port, path, method: 'POST',
          headers: { 'Content-Type': 'application/json' },
        }, (res) => {
          const chunks = [];
          res.on('data', (c) => chunks.push(c));
          res.on('end', () => resolve({ status: res.statusCode }));
        });
        // Empty body — validator will 400 within the window, but the
        // rate-limit middleware runs first and counts the hit.
        req.end('{}');
      });
    }

    const statuses = [];
    for (let i = 0; i < 11; i += 1) {
      // eslint-disable-next-line no-await-in-loop
      statuses.push((await postOnce()).status);
    }
    server.close();

    const tooMany = statuses.filter((s) => s === 429).length;
    expect(tooMany).toBeGreaterThanOrEqual(1);
  });
});
