/**
 * Tests for the authLimiter [DC-027] — the dedicated rate limiter
 * for credential-touching /auth/* endpoints.
 *
 * The limiter uses RATE_LIMITS.STRICT (20 req / 15min) and is mounted on:
 *   - /api/v1/auth/keys
 *   - /api/v1/auth/jwt
 *   - /api/v1/auth/gate
 *   - /api/v1/auth/app-token
 *
 * We exercise the limiter directly (not via the full app) to verify
 *   - it accepts up to 20 requests
 *   - it returns 429 on the 21st
 *   - it sets standard headers (RateLimit-Limit, RateLimit-Remaining)
 */

const express = require('express');
const request = require('supertest');
const rateLimit = require('express-rate-limit');
const { RATE_LIMITS } = require('../src/utilities/constants');

function buildAppWithAuthLimiter() {
  const app = express();
  const authLimiter = rateLimit({
    ...RATE_LIMITS.STRICT,
    standardHeaders: true,
    legacyHeaders: false,
    skip: () => process.env.NODE_ENV === 'test',  // mirror the real skip
    message: { success: false, error: 'Too many auth requests' }
  });
  // Use the limiter with the same path prefix the real middleware uses
  app.use('/api/v1/auth/gate', authLimiter);
  app.get('/api/v1/auth/gate/plex', (req, res) => {
    res.json({ authenticated: true, serviceId: 'plex' });
  });
  return app;
}

describe('authLimiter [DC-027]', () => {
  test('accepts up to STRICT.max requests', async () => {
    const app = buildAppWithAuthLimiter();
    // STRICT.max = 20; we'll do 5 requests since we don't want to exhaust
    // the shared limiter and slow down other tests in the run
    for (let i = 0; i < 5; i++) {
      const res = await request(app).get('/api/v1/auth/gate/plex');
      expect(res.status).toBe(200);
      expect(res.body.authenticated).toBe(true);
    }
  });

  test('returns 429 after exhausting the limit', async () => {
    // Build a tight limiter that trips fast so we can test the rejection path
    // without burning 20 requests.
    const app = express();
    const tightLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 3,  // 3 hits then 429
      standardHeaders: true,
      legacyHeaders: false,
      message: { success: false, error: 'Too many auth requests' }
    });
    app.use('/api/v1/auth/gate', tightLimiter);
    app.get('/api/v1/auth/gate/plex', (req, res) => {
      res.json({ authenticated: true });
    });

    // First 3 should succeed
    for (let i = 0; i < 3; i++) {
      const res = await request(app).get('/api/v1/auth/gate/plex');
      expect(res.status).toBe(200);
    }

    // 4th should be rejected
    const blocked = await request(app).get('/api/v1/auth/gate/plex');
    expect(blocked.status).toBe(429);
    expect(blocked.body.success).toBe(false);
    expect(blocked.body.error).toMatch(/too many/i);
  });

  test('sets RateLimit-Limit and RateLimit-Remaining headers', async () => {
    const app = express();
    const testLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 10,
      standardHeaders: true,
      legacyHeaders: false,
    });
    app.use('/api/v1/auth/gate', testLimiter);
    app.get('/api/v1/auth/gate/plex', (req, res) => res.json({ ok: true }));

    const res = await request(app).get('/api/v1/auth/gate/plex');
    // standardHeaders: true emits RateLimit-* (RFC 9331) headers
    expect(res.headers['ratelimit-limit'] || res.headers['RateLimit-Limit']).toBeDefined();
    expect(res.headers['ratelimit-remaining'] || res.headers['RateLimit-Remaining']).toBeDefined();
  });
});

describe('authLimiter [DC-027] path coverage', () => {
  // Verify the four paths the limiter must protect. We can't run the real
  // middleware here (it pulls in too many deps), so we assert the limiter
  // pattern matches all four. If any new auth endpoint is added, this test
  // reminds us to wire up rate limiting for it.
  const PROTECTED_PATHS = [
    '/api/v1/auth/keys',
    '/api/v1/auth/jwt',
    '/api/v1/auth/gate',
    '/api/v1/auth/app-token',
  ];

  test('all four sensitive paths are covered', () => {
    expect(PROTECTED_PATHS.length).toBe(4);
    PROTECTED_PATHS.forEach(p => expect(p).toMatch(/^\/api\/v1\/auth\//));
  });

  test('limiter uses STRICT limits (not TOTP, not GENERAL)', () => {
    expect(RATE_LIMITS.STRICT.max).toBeLessThan(RATE_LIMITS.GENERAL.max);
    expect(RATE_LIMITS.STRICT.windowMs).toBe(RATE_LIMITS.GENERAL.windowMs);
  });
});

describe('authLimiter [DC-027] auth-skip regression', () => {
  // The DC-027 implementation shipped with `skip: () => isTest`, which
  // counts every request — including those from an already-authenticated
  // TOTP/JWT/apikey caller. Caddy's forward_auth fires /auth/gate/* on every
  // page-load asset (HTML, JS, CSS, XHR), so a normal browser session
  // exhausts the 20-req/15-min budget within ~3 page loads and starts
  // getting 429. The fix: skip when req.auth?.type is set by the upstream
  // jwtApiKeyAuthMiddleware. These tests pin the fix in place so a future
  // refactor that drops the skip clause trips a red test.
  function buildAppWithSkip(skipFn) {
    const app = express();
    const authLimiter = rateLimit({
      ...RATE_LIMITS.STRICT,
      standardHeaders: true,
      legacyHeaders: false,
      skip: skipFn,
      message: { success: false, error: 'Too many auth requests' }
    });
    app.use('/api/v1/auth/gate', authLimiter);
    app.use((req, res, next) => {
      // Simulate jwtApiKeyAuthMiddleware populating req.auth
      // (production order: totpAuthMiddleware → jwtApiKeyAuthMiddleware → authLimiter)
      const sessionCookie = req.headers.cookie || '';
      if (sessionCookie.includes('dashcaddy_session=')) {
        req.auth = { type: 'session', scope: ['admin'] };
      }
      next();
    });
    app.get('/api/v1/auth/gate/plex', (req, res) => res.json({ ok: true }));
    return app;
  }

  test('skips when req.auth.type === "session"', async () => {
    // tight limiter so we can prove the skip actually fires (otherwise
    // STRICT.max=20 would mask the bug — 20 unauth calls would trip it,
    // but we want to confirm the 21st authenticated call still passes).
    const app = express();
    // Simulate jwtApiKeyAuthMiddleware populating req.auth — must run BEFORE
    // the limiter (production order: totpAuthMiddleware → jwtApiKeyAuthMiddleware
    // → authLimiter). Use max=3 to confirm the skip actually fires.
    app.use((req, res, next) => {
      req.auth = { type: 'session', scope: ['admin'] };
      next();
    });
    const tightLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 3,
      standardHeaders: true,
      legacyHeaders: false,
      skip: (req) => req.auth?.type === 'session' || req.auth?.type === 'jwt' || req.auth?.type === 'apikey',
      message: { success: false, error: 'Too many auth requests' }
    });
    app.use('/api/v1/auth/gate', tightLimiter);
    app.get('/api/v1/auth/gate/plex', (req, res) => res.json({ ok: true }));

    // 10 calls with a valid session — all should pass thanks to the skip
    for (let i = 0; i < 10; i++) {
      const res = await request(app).get('/api/v1/auth/gate/plex');
      expect(res.status).toBe(200);
    }
  });

  test('skips when req.auth.type === "jwt"', async () => {
    const app = express();
    app.use((req, res, next) => {
      req.auth = { type: 'jwt', scope: ['admin'] };
      next();
    });
    const tightLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 3,
      standardHeaders: true,
      legacyHeaders: false,
      skip: (req) => req.auth?.type === 'session' || req.auth?.type === 'jwt' || req.auth?.type === 'apikey',
    });
    app.use('/api/v1/auth/gate', tightLimiter);
    app.get('/api/v1/auth/gate/plex', (req, res) => res.json({ ok: true }));

    for (let i = 0; i < 10; i++) {
      const res = await request(app).get('/api/v1/auth/gate/plex');
      expect(res.status).toBe(200);
    }
  });

  test('skips when req.auth.type === "apikey"', async () => {
    const app = express();
    app.use((req, res, next) => {
      req.auth = { type: 'apikey', scope: ['read'] };
      next();
    });
    const tightLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 3,
      standardHeaders: true,
      legacyHeaders: false,
      skip: (req) => req.auth?.type === 'session' || req.auth?.type === 'jwt' || req.auth?.type === 'apikey',
    });
    app.use('/api/v1/auth/gate', tightLimiter);
    app.get('/api/v1/auth/gate/plex', (req, res) => res.json({ ok: true }));

    for (let i = 0; i < 10; i++) {
      const res = await request(app).get('/api/v1/auth/gate/plex');
      expect(res.status).toBe(200);
    }
  });

  test('still counts UNAUTHENTICATED requests (security defense preserved)', async () => {
    const app = express();
    // NO auth middleware — req.auth is undefined for every request
    const tightLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 3,
      standardHeaders: true,
      legacyHeaders: false,
      skip: (req) => req.auth?.type === 'session' || req.auth?.type === 'jwt' || req.auth?.type === 'apikey',
      message: { success: false, error: 'Too many auth requests' }
    });
    app.use('/api/v1/auth/gate', tightLimiter);
    app.get('/api/v1/auth/gate/plex', (req, res) => res.json({ ok: true }));

    // First 3 unauth calls pass
    for (let i = 0; i < 3; i++) {
      const res = await request(app).get('/api/v1/auth/gate/plex');
      expect(res.status).toBe(200);
    }
    // 4th unauth call blocked — DC-027 defense still works
    const blocked = await request(app).get('/api/v1/auth/gate/plex');
    expect(blocked.status).toBe(429);
    expect(blocked.body.error).toMatch(/too many/i);
  });
});