import { describe, it, expect } from 'vitest';
import { isMaintenanceExempt, evaluateMaintenance, MAINTENANCE_RENDER_PATH, runMaintenanceGate } from './maintenance.js';

describe('isMaintenanceExempt', () => {
  it('exempts the admin area, the API, and the render target (operator escape + loop break)', () => {
    expect(isMaintenanceExempt('/admin')).toBe(true);
    expect(isMaintenanceExempt('/admin/login')).toBe(true);
    expect(isMaintenanceExempt('/admin/maintenance')).toBe(true);
    expect(isMaintenanceExempt('/api/admin/settings')).toBe(true);
    expect(isMaintenanceExempt(MAINTENANCE_RENDER_PATH)).toBe(true);
  });
  it('does NOT exempt public pages', () => {
    expect(isMaintenanceExempt('/')).toBe(false);
    expect(isMaintenanceExempt('/post/hello')).toBe(false);
    expect(isMaintenanceExempt('/blueprint/cms')).toBe(false);
    // must not be prefix-fooled by a public path that merely contains "admin"
    expect(isMaintenanceExempt('/posts/admin-guide')).toBe(false);
  });
});

describe('evaluateMaintenance', () => {
  const on = { enabled: true, retryAfterSec: 120 };
  it('never blocks when disabled', () => {
    expect(evaluateMaintenance({ enabled: false }, null, '/').blocked).toBe(false);
  });
  it('never blocks an exempt path even when enabled + anonymous', () => {
    expect(evaluateMaintenance(on, null, '/admin/login').blocked).toBe(false);
    expect(evaluateMaintenance(on, null, MAINTENANCE_RENDER_PATH).blocked).toBe(false);
  });
  it('blocks an anonymous public request when enabled, surfacing retryAfterSec', () => {
    const v = evaluateMaintenance(on, null, '/');
    expect(v.blocked).toBe(true);
    expect(v.retryAfterSec).toBe(120);
  });
  it('NEVER blocks admin/owner (operator escape hard floor)', () => {
    expect(evaluateMaintenance(on, ['admin'], '/').blocked).toBe(false);
    expect(evaluateMaintenance(on, ['owner'], '/').blocked).toBe(false);
  });
  it('lets configured allowRoles through, blocks others', () => {
    expect(evaluateMaintenance({ ...on, allowRoles: ['editor'] }, ['editor'], '/').blocked).toBe(false);
    expect(evaluateMaintenance({ ...on, allowRoles: ['editor'] }, ['subscriber'], '/').blocked).toBe(true);
  });
  it('defaults retryAfterSec to the guard default when unset', () => {
    expect(evaluateMaintenance({ enabled: true }, null, '/').retryAfterSec).toBe(3600);
  });
});

describe('runMaintenanceGate', () => {
  const ctx = (pathname: string, rewrite = async () =>
    new Response('<html>maintenance</html>', { status: 200, headers: { 'content-type': 'text/html' } })) =>
    ({ url: new URL(`https://x.test${pathname}`), rewrite });
  const deps = (cfg: any, roles: string[] | null = null) => ({
    loadConfig: async () => cfg,
    resolveRoles: async () => roles,
  });

  it('returns null (pass) when disabled — never reads roles', async () => {
    let rolesRead = false;
    const d = { loadConfig: async () => ({ enabled: false }), resolveRoles: async () => { rolesRead = true; return null; } };
    expect(await runMaintenanceGate(ctx('/'), d)).toBeNull();
    expect(rolesRead).toBe(false);
  });
  it('returns null for an exempt path even when enabled — never reads config', async () => {
    let cfgRead = false;
    const d = { loadConfig: async () => { cfgRead = true; return { enabled: true }; }, resolveRoles: async () => null };
    expect(await runMaintenanceGate(ctx('/admin/login'), d)).toBeNull();
    expect(cfgRead).toBe(false);
  });
  it('returns null for admin (operator escape)', async () => {
    expect(await runMaintenanceGate(ctx('/'), deps({ enabled: true }, ['admin']))).toBeNull();
  });
  it('blocks anonymous with a themed 503 carrying Retry-After + noindex', async () => {
    const res = await runMaintenanceGate(ctx('/'), deps({ enabled: true, retryAfterSec: 120 }, null));
    expect(res).not.toBeNull();
    expect(res!.status).toBe(503);
    expect(res!.headers.get('Retry-After')).toBe('120');
    expect(res!.headers.get('X-Robots-Tag')).toBe('noindex');
    expect(res!.headers.get('content-type')).toContain('text/html');
    expect(await res!.text()).toContain('maintenance');
  });
  it('rewrites to the render target exactly once (no loop)', async () => {
    let calls = 0;
    const rw = async () => { calls++; return new Response('m', { status: 200, headers: { 'content-type': 'text/html' } }); };
    await runMaintenanceGate(ctx('/', rw), deps({ enabled: true }, null));
    expect(calls).toBe(1);
  });
  it('passes any Set-Cookie from the rendered page through to the 503', async () => {
    const rw = async () => new Response('m', { status: 200, headers: { 'content-type': 'text/html', 'set-cookie': 'a=b' } });
    const res = await runMaintenanceGate(ctx('/', rw), deps({ enabled: true }, null));
    expect(res!.headers.get('set-cookie')).toBe('a=b');
  });
  it('falls back to 3600 Retry-After when retryAfterSec unset', async () => {
    const res = await runMaintenanceGate(ctx('/'), deps({ enabled: true }, null));
    expect(res!.headers.get('Retry-After')).toBe('3600');
  });
});
