// Phase 9 §9.4 — vitest scaffold + auth-boundary tests.
//
// Covers shared/auth/middleware.js. Mocks the Postgres pool with a thin
// in-memory fake — no real DB required.

import { describe, it, expect, vi, beforeEach } from 'vitest';
import jwt from 'jsonwebtoken';
import { authenticate, requirePermission } from './middleware.js';

const SECRET = 'test-secret-please-do-not-use-in-prod';

function makeFakePool(users, permissions = {}) {
  const calls = { count: 0 };
  return {
    calls,
    query: vi.fn(async (sql, params) => {
      calls.count += 1;
      if (sql.includes('FROM users WHERE id')) {
        const user = users[params[0]];
        return { rows: user ? [user] : [] };
      }
      if (sql.includes('FROM permissions')) {
        return { rows: (permissions[params[0]] || []).map((code) => ({ code })) };
      }
      return { rows: [] };
    }),
  };
}

function makeReq(authHeader) {
  return { headers: authHeader ? { authorization: authHeader } : {} };
}

function makeRes() {
  const res = {};
  res.status = vi.fn(() => res);
  res.json = vi.fn(() => res);
  return res;
}

describe('authenticate middleware', () => {
  let activeUser;
  let inactiveUser;
  let validToken;

  beforeEach(() => {
    activeUser = { id: 1, username: 'alice', email: 'a@x', is_admin: false, is_active: true };
    inactiveUser = { id: 2, username: 'bob', email: 'b@x', is_admin: false, is_active: false };
    validToken = jwt.sign({ userId: 1 }, SECRET, { expiresIn: '1h' });
  });

  it('returns 401 when no Authorization header is present', async () => {
    const pool = makeFakePool({ 1: activeUser });
    const mw = authenticate({ pool, secret: SECRET });
    const res = makeRes();
    const next = vi.fn();
    await mw(makeReq(), res, next);
    expect(res.status).toHaveBeenCalledWith(401);
    expect(next).not.toHaveBeenCalled();
  });

  it('returns 401 when Authorization is not Bearer', async () => {
    const pool = makeFakePool({ 1: activeUser });
    const mw = authenticate({ pool, secret: SECRET });
    const res = makeRes();
    const next = vi.fn();
    await mw(makeReq('Basic abc'), res, next);
    expect(res.status).toHaveBeenCalledWith(401);
    expect(next).not.toHaveBeenCalled();
  });

  it('returns 401 when token is malformed', async () => {
    const pool = makeFakePool({ 1: activeUser });
    const mw = authenticate({ pool, secret: SECRET });
    const res = makeRes();
    const next = vi.fn();
    await mw(makeReq('Bearer not-a-jwt'), res, next);
    expect(res.status).toHaveBeenCalledWith(401);
    expect(next).not.toHaveBeenCalled();
  });

  it('returns 403 when the user is inactive', async () => {
    const inactiveToken = jwt.sign({ userId: 2 }, SECRET, { expiresIn: '1h' });
    const pool = makeFakePool({ 2: inactiveUser });
    const mw = authenticate({ pool, secret: SECRET });
    const res = makeRes();
    const next = vi.fn();
    await mw(makeReq(`Bearer ${inactiveToken}`), res, next);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(next).not.toHaveBeenCalled();
  });

  it('calls next() and populates req.user for a valid token + active user', async () => {
    const pool = makeFakePool({ 1: activeUser }, { 1: ['products:read'] });
    const mw = authenticate({ pool, secret: SECRET });
    const req = makeReq(`Bearer ${validToken}`);
    const res = makeRes();
    const next = vi.fn();
    await mw(req, res, next);
    expect(next).toHaveBeenCalledOnce();
    expect(req.user).toBeDefined();
    expect(req.user.id).toBe(1);
    expect(req.user.permissions).toEqual(['products:read']);
  });

  it('caches the user lookup — same token within TTL → one DB hit', async () => {
    const pool = makeFakePool({ 1: activeUser }, { 1: [] });
    const mw = authenticate({ pool, secret: SECRET });
    const next = vi.fn();
    await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
    await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
    // Two queries on first hit (user + permissions), zero on the second
    expect(pool.calls.count).toBe(2);
    expect(next).toHaveBeenCalledTimes(2);
  });

  it('refetches after TTL expiry', async () => {
    vi.useFakeTimers();
    const pool = makeFakePool({ 1: activeUser }, { 1: [] });
    const mw = authenticate({ pool, secret: SECRET });
    const next = vi.fn();
    await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
    expect(pool.calls.count).toBe(2);
    vi.advanceTimersByTime(61_000);
    await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
    expect(pool.calls.count).toBe(4);
    vi.useRealTimers();
  });

  describe('KIOSK_IPS bypass', () => {
    it('bypasses token check and mints a synthetic kiosk user when req.ip matches', async () => {
      const pool = makeFakePool({});
      const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
      const req = { headers: {}, ip: '203.0.113.7' };
      const res = makeRes();
      const next = vi.fn();
      await mw(req, res, next);
      expect(next).toHaveBeenCalledOnce();
      expect(req.user).toEqual({
        id: 'kiosk',
        username: 'kiosk',
        is_admin: false,
        is_active: true,
        permissions: [],
        is_kiosk: true,
      });
      expect(pool.calls.count).toBe(0);
    });

    it('falls through to normal Bearer auth when req.ip is not in KIOSK_IPS', async () => {
      const pool = makeFakePool({ 1: activeUser }, { 1: [] });
      const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
      const req = { headers: { authorization: `Bearer ${validToken}` }, ip: '198.51.100.1' };
      const res = makeRes();
      const next = vi.fn();
      await mw(req, res, next);
      expect(next).toHaveBeenCalledOnce();
      expect(req.user.id).toBe(1);
      expect(req.user.is_kiosk).toBeUndefined();
    });

    it('does NOT bypass when a Bearer token is present, even from a kiosk IP', async () => {
      // A real user logged in from the same NAT'd network as the kiosk must
      // keep their actual identity — otherwise the bypass silently strips
      // their permissions and they 403 on gated routes.
      const pool = makeFakePool({ 1: activeUser }, { 1: ['product_import'] });
      const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
      const req = {
        headers: { authorization: `Bearer ${validToken}` },
        ip: '203.0.113.7',
      };
      const res = makeRes();
      const next = vi.fn();
      await mw(req, res, next);
      expect(next).toHaveBeenCalledOnce();
      expect(req.user.id).toBe(1);
      expect(req.user.is_kiosk).toBeUndefined();
      expect(req.user.permissions).toEqual(['product_import']);
    });

    it('does not bypass when KIOSK_IPS is empty, even if req.ip is undefined', async () => {
      const pool = makeFakePool({ 1: activeUser });
      const mw = authenticate({ pool, secret: SECRET, kioskIps: '' });
      const req = { headers: {} };
      const res = makeRes();
      const next = vi.fn();
      await mw(req, res, next);
      expect(res.status).toHaveBeenCalledWith(401);
      expect(next).not.toHaveBeenCalled();
    });

    it('supports multiple comma-separated IPs', async () => {
      const pool = makeFakePool({});
      const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7, 203.0.113.8 ,203.0.113.9' });
      const next = vi.fn();
      for (const ip of ['203.0.113.7', '203.0.113.8', '203.0.113.9']) {
        const req = { headers: {}, ip };
        await mw(req, makeRes(), next);
        expect(req.user?.is_kiosk).toBe(true);
      }
      expect(next).toHaveBeenCalledTimes(3);
    });
  });
});

describe('requirePermission middleware', () => {
  it('returns 401 when req.user is missing', () => {
    const mw = requirePermission('products:write');
    const res = makeRes();
    const next = vi.fn();
    mw({}, res, next);
    expect(res.status).toHaveBeenCalledWith(401);
    expect(next).not.toHaveBeenCalled();
  });

  it('calls next() for admin users regardless of code', () => {
    const mw = requirePermission('products:write');
    const next = vi.fn();
    mw({ user: { is_admin: true, permissions: [] } }, makeRes(), next);
    expect(next).toHaveBeenCalledOnce();
  });

  it('calls next() when user has the required permission', () => {
    const mw = requirePermission('products:write');
    const next = vi.fn();
    mw({ user: { is_admin: false, permissions: ['products:write'] } }, makeRes(), next);
    expect(next).toHaveBeenCalledOnce();
  });

  it('returns 403 when user lacks the required permission', () => {
    const mw = requirePermission('products:write');
    const res = makeRes();
    const next = vi.fn();
    mw({ user: { is_admin: false, permissions: ['products:read'] } }, res, next);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(next).not.toHaveBeenCalled();
  });
});
