import { test, expect } from '@playwright/test';

/**
 * Security headers E2E tests — runs against the local dev server (localhost:3000).
 *
 * Verifies that Next.js emits the required security headers on all response types.
 * Traefik adds additional headers in production; this test covers the Next.js layer.
 *
 * Related: GitHub issue #194 (T-10 security headers audit)
 */

const REQUIRED_HEADERS = [
  { name: 'x-frame-options', pattern: /SAMEORIGIN/i },
  { name: 'x-content-type-options', value: 'nosniff' },
  { name: 'referrer-policy', value: 'strict-origin-when-cross-origin' },
  { name: 'strict-transport-security', pattern: /max-age=63072000/ },
  { name: 'content-security-policy', pattern: /default-src/ },
  { name: 'permissions-policy', pattern: /camera=\(\)/ },
  { name: 'cross-origin-opener-policy', value: 'same-origin' },
] as const;

const PUBLIC_PATHS = ['/', '/pricing', '/auth/login', '/auth/register', '/docs'];

test.describe('Security Headers — Next.js layer', () => {
  for (const path of PUBLIC_PATHS) {
    test(`${path} has required security headers`, async ({ request }) => {
      const response = await request.get(path);
      expect(response.status(), `${path} should be reachable`).toBeLessThan(400);

      const headers = response.headers();

      for (const h of REQUIRED_HEADERS) {
        const actual = headers[h.name];
        const label = `${h.name} on ${path}`;

        expect(actual, `${label} must be present`).toBeTruthy();

        if ('value' in h) {
          expect(actual, label).toBe(h.value);
        } else {
          expect(actual, label).toMatch(h.pattern);
        }
      }
    });
  }

  test('CSP blocks dangerous sources', async ({ request }) => {
    const response = await request.get('/');
    const csp = response.headers()['content-security-policy'] ?? '';

    // base-uri and form-action restrict injection vectors
    expect(csp, 'base-uri must be self').toContain("base-uri 'self'");
    expect(csp, 'form-action must be self').toContain("form-action 'self'");
    expect(csp, 'upgrade-insecure-requests must be set').toContain('upgrade-insecure-requests');
    // frame-ancestors restricts clickjacking (defence-in-depth alongside X-Frame-Options)
    expect(csp, 'frame-ancestors must be set').toContain('frame-ancestors');
  });

  test('HSTS requires 2-year max-age and includeSubDomains', async ({ request }) => {
    const response = await request.get('/');
    const hsts = response.headers()['strict-transport-security'] ?? '';

    expect(hsts, 'HSTS max-age must be ≥63072000 (2 years)').toMatch(/max-age=63072000/);
    expect(hsts, 'HSTS must include subdomains').toContain('includeSubDomains');
    expect(hsts, 'HSTS must have preload flag').toContain('preload');
  });

  test('Permissions-Policy covers privacy-sensitive APIs', async ({ request }) => {
    const response = await request.get('/');
    const policy = response.headers()['permissions-policy'] ?? '';

    const restricted = ['camera=()', 'microphone=()', 'geolocation=()', 'payment=()'];
    for (const directive of restricted) {
      expect(policy, `Permissions-Policy must restrict ${directive}`).toContain(directive);
    }
  });

  test('no server fingerprinting headers', async ({ request }) => {
    const response = await request.get('/');
    const headers = response.headers();

    // x-powered-by should not be present (Next.js sets it by default, we strip it via Traefik)
    // In dev (no Traefik), Next.js may still set it — we document this as a known gap
    // and assert only that it's not advertising a specific technology version.
    const xpb = headers['x-powered-by'] ?? '';
    expect(xpb, 'X-Powered-By must not expose version info').not.toMatch(/\d+\.\d+/);
  });
});
