/**
 * DC-070: Caddycode config sanitization — validate the structural config
 * that flows into generateSiteBlock(), and confirm that the post-fix
 * generation does NOT interpolate raw user input into Caddyfile text.
 *
 * The endpoint /caddycode/generate was, pre-fix, the single most exposed
 * surface in the Caddy-as-code path: every JSON field flowed verbatim into
 * the Caddyfile text that /caddycode→POST /load feeds to Caddy.
 *
 * Bug class under test:
 *   1. CRLF / newline in `domain` → close the block and inject a new site
 *   2. `"` (quote) in a header value → break out of the quoted-string
 *      context and append arbitrary directives
 *   3. `}` in `tls`, `authService`, `stripPrefix`, or `upstream` →
 *      prematurely close the parent block (or open a new one)
 *   4. `://` or `;` in `upstream` → header injection / path smuggling
 *
 * Post-fix: validateGenerationConfig rejects every one of these at the
 * route layer with 400 + enumerable errors; the helper-level tests here
 * pin the rejection rules independent of the route.
 */

const { __test } = require('../../routes/caddycode');
const { validateGenerationConfig, escapeCaddyQuotedString, generateSiteBlock } = __test;

const BASE_OK = {
  domain: 'app.example.com',
  upstream: 'localhost:8080',
};

function check(cond, msg) {
  if (!cond) throw new Error('assertion failed: ' + msg);
}

describe('DC-070: caddycode config sanitization', () => {
  describe('validateGenerationConfig — happy paths', () => {
    test('minimal valid config passes', () => {
      const r = validateGenerationConfig(BASE_OK);
      check(r.valid === true, `expected valid=true, got errors=${JSON.stringify(r.errors)}`);
      check(Array.isArray(r.errors) && r.errors.length === 0, 'expected no errors');
    });

    test('full valid config (auth + headers + stripPrefix + tls CA) passes', () => {
      const r = validateGenerationConfig({
        domain: 'chat.example.com',
        upstream: 'localhost:8096',
        tls: 'letsencrypt',
        auth: true,
        authService: 'chat',
        upstreamProtocol: 'https',
        headers: {
          'X-Frame-Options': 'DENY',
          'X-Content-Type-Options': 'nosniff',
          'Strict-Transport-Security': 'max-age=63072000',
        },
        stripPrefix: '/api/v1',
      });
      check(r.valid === true, `expected valid, got errors=${JSON.stringify(r.errors)}`);
    });

    test('IPv6 bracket-form upstream accepted', () => {
      const r = validateGenerationConfig({ domain: 'dns.example.com', upstream: '[::1]:5380' });
      check(r.valid === true, `IPv6 bracket should pass: ${JSON.stringify(r.errors)}`);
    });

    test('bare host without :port rejected (DC-070 round 2)', () => {
      // Round-1 polish: Caddy reverse_proxy requires an explicit :port
      // segment. A bare `localhost` would produce a Caddyfile that
      // either fails to reload or silently picks a default port.
      const r = validateGenerationConfig({ domain: 'app.example.com', upstream: 'localhost' });
      check(r.valid === false, `bare host should reject: ${JSON.stringify(r.errors)}`);
    });

    test('upstream with non-numeric port rejected', () => {
      const r = validateGenerationConfig({ domain: 'app.example.com', upstream: 'localhost:abc' });
      check(r.valid === false, `non-numeric port should reject: ${JSON.stringify(r.errors)}`);
    });
  });

  describe('validateGenerationConfig — injection rejection', () => {
    test('CRLF in domain rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, domain: 'evil.com\nnew.site.example.com {' });
      check(r.valid === false, 'CRLF should reject');
      check(r.errors.some((e) => /domain/.test(e)), `expected error to mention domain, got ${JSON.stringify(r.errors)}`);
    });

    test('brace in domain rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, domain: 'evil} malicious' });
      check(r.valid === false, 'brace should reject');
    });

    test('"://" in upstream rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, upstream: 'http://evil.tld/x' });
      check(r.valid === false, ':// should reject');
    });

    test('space + brace in upstream rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, upstream: 'localhost:8080 } evil {' });
      check(r.valid === false, 'whitespace+brace in upstream should reject');
    });

    test('CRLF in header value rejected', () => {
      const r = validateGenerationConfig({
        ...BASE_OK,
        headers: { 'X-Custom': 'innocent\r\nHost: evil.tld' },
      });
      check(r.valid === false, 'CRLF in header value should reject');
      check(r.errors.some((e) => /CR or LF/i.test(e)), `expected CR/LF error: ${JSON.stringify(r.errors)}`);
    });

    test('bad header key charset rejected', () => {
      const r = validateGenerationConfig({
        ...BASE_OK,
        headers: { 'X Bad Key': 'innocent' },
      });
      check(r.valid === false, 'space in header key should reject');
    });

    test('non-string tls rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, tls: 'evil directive' });
      check(r.valid === false, 'whitespace+word tls should reject');
    });

    test('empty authService when auth=true rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, auth: true });
      check(r.valid === false, 'auth=true requires authService');
    });

    test('upstreamProtocol other than http/https rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, upstreamProtocol: 'javascript' });
      check(r.valid === false, 'non-http protocol should reject');
    });

    test('stripPrefix without leading slash rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, stripPrefix: 'app/v1' });
      check(r.valid === false, 'stripPrefix without leading slash should reject');
    });

    test('stripPrefix with brace rejected', () => {
      const r = validateGenerationConfig({ ...BASE_OK, stripPrefix: '/api/{evil}' });
      check(r.valid === false, 'stripPrefix with brace should reject');
    });

    test('multiple errors returned together (enumerable)', () => {
      const r = validateGenerationConfig({
        domain: 'evil }',
        upstream: 'localhost:8080 } malicious {',
        tls: 'bad tls',
        auth: true,
        headers: { 'X B': 'oops' },
      });
      check(r.valid === false, 'should reject');
      check(r.errors.length >= 4, `expected multiple errors, got ${r.errors.length}: ${JSON.stringify(r.errors)}`);
    });
  });

  describe('escapeCaddyQuotedString', () => {
    test('escapes backslash and quote', () => {
      check(escapeCaddyQuotedString('a"b\\c') === 'a\\"b\\\\c', 'should escape both');
    });

    test('safe string passes through verbatim', () => {
      check(escapeCaddyQuotedString('hello') === 'hello', 'safe string unchanged');
    });

    test('empty string survives', () => {
      check(escapeCaddyQuotedString('') === '', 'empty string survives');
    });
  });

  describe('generateSiteBlock — quote-breakout defence-in-depth', () => {
    test('post-validation, header value with " is properly escaped', () => {
      // The validator REJECTS this upstream (CRLF + quote) but the
      // generator must also escape `"` even if a future code path bypasses
      // validation. This test pins the dual-defence.
      const cfg = {
        domain: 'app.example.com',
        upstream: 'localhost:8080',
        headers: { 'X-Custom': 'a"b' },
      };
      // The validator rejects CRLF + chars outside the charset, but a bare
      // `"` is technically allowed by /[\r\n]/ (only CR/LF). However the
      // GENERATOR must still escape it. Verify by calling generateSiteBlock
      // directly with a manually-validated config.
      const out = generateSiteBlock(cfg);
      // The header line should appear as:   X-Custom "a\"b"
      // i.e. the raw `"` in the value MUST be escaped, otherwise the Caddyfile
      // line breaks out of the quoted context.
      check(out.includes('X-Custom "a\\"b"'), `expected escaped quote, got: ${out}`);
    });
  });

  describe('route integration — /caddycode/generate wires validation', () => {
    const express = require('express');
    const request = require('supertest');
    const routes = require('../../routes/caddycode');

    function buildApp() {
      const app = express();
      app.use(express.json());
      const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
      return { app, wrap };
    }

    test('valid config → 200 + caddyfile', async () => {
      const { app, wrap } = buildApp();
      app.use('/api/v1', routes({ asyncHandler: wrap }));
      const res = await request(app)
        .post('/api/v1/caddycode/generate')
        .send({ domain: 'app.example.com', upstream: 'localhost:8080' });
      check(res.status === 200, `expected 200, got ${res.status}`);
      check(typeof res.body.caddyfile === 'string', 'expected caddyfile string');
      check(res.body.caddyfile.includes('app.example.com'), 'caddyfile should include domain');
    });

    test('CRLF in domain → 400 + enumerable errors', async () => {
      const { app, wrap } = buildApp();
      app.use('/api/v1', routes({ asyncHandler: wrap }));
      const res = await request(app)
        .post('/api/v1/caddycode/generate')
        .send({ domain: 'evil.com\nnew block', upstream: 'localhost:8080' });
      check(res.status === 400, `expected 400, got ${res.status}: ${JSON.stringify(res.body)}`);
      check(res.body.success === false, 'success should be false');
      check(Array.isArray(res.body.errors), `expected enumerable errors array, got body=${JSON.stringify(res.body)}`);
      check(res.body.errors.length >= 1, 'at least one error');
    });

    test('"://" in upstream → 400', async () => {
      const { app, wrap } = buildApp();
      app.use('/api/v1', routes({ asyncHandler: wrap }));
      const res = await request(app)
        .post('/api/v1/caddycode/generate')
        .send({ domain: 'app.example.com', upstream: 'http://evil.tld/x' });
      check(res.status === 400, `expected 400, got ${res.status}`);
    });

    test('header with CRLF → 400 + specific error', async () => {
      const { app, wrap } = buildApp();
      app.use('/api/v1', routes({ asyncHandler: wrap }));
      const res = await request(app)
        .post('/api/v1/caddycode/generate')
        .send({
          domain: 'app.example.com',
          upstream: 'localhost:8080',
          headers: { 'X-Bad': 'oops\r\nHost: evil.tld' },
        });
      check(res.status === 400, `expected 400, got ${res.status}`);
      check(res.body.errors.some((e) => /CR or LF/i.test(e)), `expected CR/LF mention: ${JSON.stringify(res.body.errors)}`);
    });

    test('end-to-end: header value with quote + backslash round-trips through generator', async () => {
      // DC-070 round-2 polish (per GLM-5.3 review): the unit tests pin the
      // escape helper and the route reject path independently, but nothing
      // asserts the GENERATED Caddyfile is well-formed when a header value
      // contains BOTH " and \. Verify the generator escapes both so the
      // resulting line parses as a Caddyfile quoted string.
      const { app, wrap } = buildApp();
      app.use('/api/v1', routes({ asyncHandler: wrap }));
      const res = await request(app)
        .post('/api/v1/caddycode/generate')
        .send({
          domain: 'app.example.com',
          upstream: 'localhost:8080',
          headers: { 'X-Custom': 'a"b\\c' },
        });
      check(res.status === 200, `expected 200, got ${res.status}: ${JSON.stringify(res.body)}`);
      const out = res.body.caddyfile;
      check(typeof out === 'string', 'expected caddyfile string');
      // The header line should be EXACTLY:  X-Custom "a\"b\\c"
      // i.e. the raw `"` and `\` in the value MUST be escaped.
      check(
        /X-Custom "a\\"b\\\\c"/.test(out),
        `expected escaped quote+backslash in generated Caddyfile, got: ${out}`
      );
    });
  });
});
