import { describe, expect, it, vi } from 'vitest';
import {
  applySecurityHeaders,
  securityHeadersMiddleware,
  HSTS_MAX_AGE_SECONDS,
} from './security-headers.js';
import { type Request, type Response } from 'express';

function fakeRes() {
  const headers: Record<string, string> = {};
  const res = {
    setHeader(name: string, value: string) {
      headers[name] = value;
    },
  } as unknown as Response;
  return { res, headers };
}

describe('applySecurityHeaders', () => {
  it('sets the baseline hardening headers on every response', () => {
    const { res, headers } = fakeRes();
    applySecurityHeaders({ secure: false } as Request, res);
    expect(headers['X-Content-Type-Options']).toBe('nosniff');
    expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
    expect(headers['Referrer-Policy']).toBe('strict-origin-when-cross-origin');
  });

  it('keeps X-Frame-Options at SAMEORIGIN so the app can serve its own iframes', () => {
    // PDF preview and noVNC browser sessions embed same-origin iframes; DENY
    // would break them.
    const { res, headers } = fakeRes();
    applySecurityHeaders({ secure: true } as Request, res);
    expect(headers['X-Frame-Options']).not.toBe('DENY');
    expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
  });

  it('never touches HSTS on a plain-HTTP response, regardless of policy', () => {
    const clear = fakeRes();
    applySecurityHeaders({ secure: false } as Request, clear.res, 'clear');
    expect(clear.headers['Strict-Transport-Security']).toBeUndefined();

    const enable = fakeRes();
    applySecurityHeaders({ secure: false } as Request, enable.res, 'enable');
    expect(enable.headers['Strict-Transport-Security']).toBeUndefined();
  });

  it("emits HSTS max-age (no includeSubDomains) over TLS when policy is 'enable'", () => {
    const tls = fakeRes();
    applySecurityHeaders({ secure: true } as Request, tls.res, 'enable');
    expect(tls.headers['Strict-Transport-Security']).toBe(`max-age=${HSTS_MAX_AGE_SECONDS}`);
    // includeSubDomains is intentionally dropped — it is the most damaging part
    // of a mistaken HSTS rollout (pins every subdomain too).
    expect(tls.headers['Strict-Transport-Security']).not.toContain('includeSubDomains');
  });

  it("actively clears a stale HSTS pin (max-age=0) over TLS when policy is 'clear'", () => {
    // This is the recovery path: a browser pinned by an earlier build loads the
    // app over HTTPS, receives max-age=0, and forgets the pin so plain HTTP works.
    const tls = fakeRes();
    applySecurityHeaders({ secure: true } as Request, tls.res, 'clear');
    expect(tls.headers['Strict-Transport-Security']).toBe('max-age=0');
  });

  it("defaults to the 'clear' policy when none is passed", () => {
    const tls = fakeRes();
    applySecurityHeaders({ secure: true } as Request, tls.res);
    expect(tls.headers['Strict-Transport-Security']).toBe('max-age=0');
  });
});

describe('securityHeadersMiddleware', () => {
  it('applies the headers and calls next()', () => {
    const { res, headers } = fakeRes();
    const next = vi.fn();
    securityHeadersMiddleware()({ secure: false } as Request, res, next);
    expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
    expect(next).toHaveBeenCalledOnce();
  });

  it('reads a thunk policy per request (lazy resolution)', () => {
    let policy: 'enable' | 'clear' = 'clear';
    const mw = securityHeadersMiddleware(() => policy);

    const a = fakeRes();
    mw({ secure: true } as Request, a.res, vi.fn());
    expect(a.headers['Strict-Transport-Security']).toBe('max-age=0');

    // Flip the source after registration — the next request sees the new value.
    policy = 'enable';
    const b = fakeRes();
    mw({ secure: true } as Request, b.res, vi.fn());
    expect(b.headers['Strict-Transport-Security']).toBe(`max-age=${HSTS_MAX_AGE_SECONDS}`);
  });
});
