// CSP phase 1: the tightened policy rides on Content-Security-Policy-Report-Only for
// one release before it is enforced (docs/website/API_V2_PLAN.md § Phase 1). The whole
// point of that soak is that the live policy is untouched, so the first test here
// pins the enforced header verbatim — if a future edit to config/csp.js changes what
// users are actually protected by, it fails rather than shipping quietly.
//
// Dead-port DB before requiring the app: it pulls in every model, which builds a
// mariadb pool at require time. No route touched here queries anything.
process.env.DB_HOST = '127.0.0.1'
process.env.DB_PORT = '59999'

const { test, after } = require('node:test')
const assert = require('node:assert/strict')

const app = require('../src/app')
const csp = require('../src/config/csp')
const cspReport = require('../src/router/cspReport.controller')
const db = require('../src/utils/db')

after(() => db.close())

// The exact header served before this phase. Not a restatement of config/csp.js —
// captured from the running app on main, so it also catches helmet changing its
// `useDefaults` set underneath us (which is how `form-action` got here in the first
// place: it was already being emitted, and is now pinned explicitly rather than
// inherited from a dependency's defaults).
const ENFORCED_BASELINE =
  "default-src 'self';script-src 'self';" +
  "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
  "font-src 'self' https://fonts.gstatic.com;img-src 'self' data: https:;" +
  "connect-src 'self';frame-ancestors 'self';object-src 'none';base-uri 'self';" +
  "form-action 'self';script-src-attr 'none'"

async function withServer(fn) {
  const server = await new Promise((resolve) => {
    const s = app.listen(0, '127.0.0.1', () => resolve(s))
  })
  try {
    return await fn(`http://127.0.0.1:${server.address().port}`)
  } finally {
    await new Promise((resolve) => server.close(resolve))
  }
}

test('the enforced policy is byte-for-byte what it was before the soak', async () => {
  const res = await withServer((base) => fetch(`${base}/api/health`))
  assert.equal(
    res.headers.get('content-security-policy'),
    ENFORCED_BASELINE,
    'Report-only phase must not change what is actually enforced.',
  )
})

test('the report-only policy differs from the enforced one only by frame-ancestors', async () => {
  const res = await withServer((base) => fetch(`${base}/api/health`))
  const reportOnly = res.headers.get('content-security-policy-report-only')
  assert.ok(reportOnly, 'Content-Security-Policy-Report-Only must be served')

  assert.match(reportOnly, /frame-ancestors 'none'/)
  assert.doesNotMatch(reportOnly, /frame-ancestors 'self'/)

  // Strip the report plumbing and the one tightened directive; what is left must be
  // the live policy. Anything else appearing here would be silently soaking an
  // untracked change.
  const stripped = reportOnly
    .split(';')
    .filter((d) => !d.startsWith('report-to') && !d.startsWith('report-uri'))
    .map((d) => (d.startsWith('frame-ancestors') ? "frame-ancestors 'self'" : d))
    .join(';')
  assert.equal(stripped, ENFORCED_BASELINE)
})

test('report-to is backed by a Reporting-Endpoints header, and report-uri by the same path', async () => {
  // Without the header the report-to directive is inert and only Firefox/Safari's
  // deprecated report-uri would ever fire — i.e. a soak that silently half-works.
  const res = await withServer((base) => fetch(`${base}/api/health`))
  assert.equal(res.headers.get('reporting-endpoints'), `${csp.REPORT_GROUP}="${csp.REPORT_PATH}"`)

  const reportOnly = res.headers.get('content-security-policy-report-only')
  assert.ok(reportOnly.includes(`report-to ${csp.REPORT_GROUP}`))
  assert.ok(reportOnly.includes(`report-uri ${csp.REPORT_PATH}`))
})

test('the sink is same-origin — reports never leave this host', () => {
  assert.ok(csp.REPORT_PATH.startsWith('/'), 'a third-party collector would receive attack data')
})

test('the sink accepts both wire formats and always answers 204', async () => {
  const statuses = await withServer(async (base) => {
    const post = (type, body) =>
      fetch(`${base}${csp.REPORT_PATH}`, {
        method: 'POST',
        headers: { 'Content-Type': type },
        body,
      }).then((r) => r.status)

    return {
      // Firefox / Safari
      reportUri: await post(
        'application/csp-report',
        JSON.stringify({ 'csp-report': { 'effective-directive': 'frame-ancestors' } }),
      ),
      // Chrome's Reporting API
      reportsJson: await post(
        'application/reports+json',
        JSON.stringify([{ type: 'csp-violation', body: { effectiveDirective: 'frame-ancestors' } }]),
      ),
      // Junk must not produce a 4xx + an ERROR log line, or the open endpoint becomes
      // a log-flood primitive.
      malformed: await post('application/csp-report', 'not json'),
      oversized: await post(
        'application/csp-report',
        JSON.stringify({ 'csp-report': { 'script-sample': 'x'.repeat(40000) } }),
      ),
      empty: await post('application/csp-report', ''),
    }
  })

  assert.deepEqual(statuses, {
    reportUri: 204,
    reportsJson: 204,
    malformed: 204,
    oversized: 204,
    empty: 204,
  })
})

test('the sink is POST-only', async () => {
  const res = await withServer((base) => fetch(`${base}${csp.REPORT_PATH}`))
  assert.equal(res.status, 404)
})

test('normalize() reads the hyphenated report-uri body', () => {
  const [v] = cspReport.normalize({
    'csp-report': {
      'document-uri': 'https://site/page',
      'effective-directive': 'frame-ancestors',
      'blocked-uri': 'https://evil/',
      'script-sample': 'alert(1)',
      disposition: 'report',
    },
  })
  assert.deepEqual(v, {
    documentUrl: 'https://site/page',
    directive: 'frame-ancestors',
    blockedUrl: 'https://evil/',
    disposition: 'report',
    sample: 'alert(1)',
  })
})

test('normalize() reads the camelCase Reporting-API envelope and drops other report types', () => {
  const out = cspReport.normalize([
    {
      type: 'csp-violation',
      url: 'https://site/page',
      body: { effectiveDirective: 'form-action', blockedURL: 'https://evil/' },
    },
    // Browsers deliver deprecation/intervention reports to the same group.
    { type: 'deprecation', url: 'https://site/page', body: { id: 'x' } },
  ])
  assert.equal(out.length, 1)
  assert.equal(out[0].directive, 'form-action')
  assert.equal(out[0].documentUrl, 'https://site/page')
})

test('normalize() shrugs at anything else', () => {
  assert.deepEqual(cspReport.normalize(undefined), [])
  assert.deepEqual(cspReport.normalize({}), [])
  assert.deepEqual(cspReport.normalize('nonsense'), [])
})
