/**
 * DC-062: errorResponse arg-order regression test + caddy-upstreams JSON
 * response guarantees.
 *
 * Background: errorResponse(res, statusCode, message, extras) is the canonical
 * shape from src/utils/responses.js. Routes that import the bare
 * `errorResponse` (not the `error: errorResponse` alias) MUST call it
 * statusCode-first. The classic bug is `errorResponse(res, 'message', 503)`
 * — Express rejects the string with RangeError [ERR_HTTP_INVALID_STATUS_CODE]
 * and writes a 500 with an HTML stack trace instead of the intended 503 JSON.
 *
 * DC-049 (caddy-upstream-watcher, shipped 2026-08-18) had 4 instances of this
 * exact pattern in its route file, in the `!caddyUpstreamWatcher` defensive
 * branch. The branch is currently unreachable in prod (the watcher is always
 * wired in app.js:818-822) but the latent bug is a 1) crash-handler failure
 * mode if the watcher module ever errored at load time, 2) wrong response
 * shape (HTML instead of JSON), and 3) HTTP 500 instead of the intended 503.
 *
 * Two layers of fix:
 *   1. routes/caddy-upstreams.js — swap the 4 callsites to (res, 503, msg).
 *   2. src/utils/responses.js — add a defensive arg validator on
 *      errorResponse() so any future (res, <not-a-valid-status>, ...)
 *      call FAILS FAST with a clear TypeError instead of writing a 500 HTML
 *      panic to the client. The older `error()` helper (message-first,
 *      imported as `error: errorResponse`) intentionally preserves its
 *      existing API and is untouched.
 *
 * This test exercises both fixes.
 */

const express = require('express');
const http = require('http');
const path = require('path');

// Use the repo's deps so the test fails under exactly the same module
// resolution as production code (otherwise symlink/path differences can
// mask validator-install gaps).
//   __dirname = /opt/dashcaddy/dashcaddy-api/__tests__
//   __dirname/../src/utils/responses = the file under test
const repoRoot = path.join(__dirname, '..');

const { errorResponse, error: legacyError } = require(path.join(repoRoot, 'src/utils/responses'));

function get(port, urlPath) {
  return new Promise((resolve, reject) => {
    const req = http.get(`http://localhost:${port}${urlPath}`, (resp) => {
      let body = '';
      resp.on('data', (c) => { body += c; });
      resp.on('end', () => resolve({ status: resp.statusCode, headers: resp.headers, body }));
    });
    req.on('error', reject);
  });
}

describe('errorResponse canonical arg-order + type guard (DC-062)', () => {
  test('correct order — (res, 503, msg) returns 503 JSON', () => {
    const mockRes = {
      status(code) { mockRes._code = code; return this; },
      json(body) { mockRes._body = body; return this; },
    };
    errorResponse(mockRes, 503, 'Caddy upstream watcher not initialized');
    expect(mockRes._code).toBe(503);
    expect(mockRes._body).toEqual({ success: false, error: 'Caddy upstream watcher not initialized' });
  });

  test('swapped order — (res, msg, statusCode) throws TypeError instead of writing a 500 HTML panic', () => {
    // Before DC-062: errorResponse would call res.status('string-msg'),
    // Express throws RangeError, error middleware catches it, writes 500 HTML.
    // After DC-062: errorResponse itself rejects the call with a clear
    //   TypeError, naming the wrong arg.
    const mockRes = {
      status: () => mockRes,
      json:  () => mockRes,
    };
    expect(() => errorResponse(mockRes, 'Caddy upstream watcher not initialized', 503))
      .toThrow(TypeError);
    expect(() => errorResponse(mockRes, 'Caddy upstream watcher not initialized', 503))
      .toThrow(/statusCode must be an integer HTTP status/);
  });

  test.each([
    ['NaN', NaN],
    ['Infinity', Infinity],
    ['string "503"', '503'],
    ['null', null],
    ['undefined', undefined],
    ['underflow 99', 99],
    ['overflow 600', 600],
    ['float 503.5', 503.5],
    ['object', { code: 503 }],
    ['array', [503]],
  ])('rejects invalid statusCode %s', (_name, badStatus) => {
    const mockRes = {
      status: () => mockRes,
      json:  () => mockRes,
    };
    expect(() => errorResponse(mockRes, badStatus, 'msg')).toThrow(TypeError);
  });

  test('rejects non-string message', () => {
    const mockRes = {
      status: () => mockRes,
      json:  () => mockRes,
    };
    expect(() => errorResponse(mockRes, 503, 123)).toThrow(TypeError);
    expect(() => errorResponse(mockRes, 503, null)).toThrow(TypeError);
    expect(() => errorResponse(mockRes, 503, undefined)).toThrow(TypeError);
    expect(() => errorResponse(mockRes, 503, { msg: 'x' })).toThrow(TypeError);
  });

  test('preserves correct callers (DC-086 extras.code propagation still works)', () => {
    const mockRes = {
      status: () => mockRes,
      json:  (b) => { mockRes._lastBody = b; return mockRes; },
    };
    errorResponse(mockRes, 409, 'Conflict', { code: 'DC-CONF-1', extra: 'detail' });
    expect(mockRes._lastBody).toEqual({
      success: false,
      error: 'Conflict',
      code: 'DC-CONF-1',
      extra: 'detail',
    });
  });

  test('legacy `error()` helper (message, status) is UNCHANGED — still works', () => {
    // Regression guard for alias-style importers (dns.js, services.js,
    // ssl-monitor.js, license.js, dependencies.js, errorlogs.js, etc.).
    // The legacy helper takes (res, message, statusCode) order. Make sure
    // the validator we added to `errorResponse` doesn't bleed into
    // `error()`.
    const mockRes = {
      status(code) { mockRes._code = code; return this; },
      json(body) { mockRes._body = body; return this; },
    };
    legacyError(mockRes, 'service unavailable', 503);
    expect(mockRes._code).toBe(503);
    expect(mockRes._body).toEqual({ success: false, error: 'service unavailable' });
  });

  test('regression: an Express response with res.status(string) emits HTML 500 — proves the bug pre-fix', async () => {
    // This is the failure mode DC-062 prevents. We still need this to
    // be true to prove the guard's value: if a call site ever slipped past
    // the validator (e.g. by sending a non-number disguised as code 0),
    // the server still doesn't return the intended status as JSON.
    const server = await new Promise((resolve) => {
      const app = express();
      app.get('/probe', (req, res) => {
        try {
          res.status('not a status').json({ ok: false });
        } catch (_) {
          res.end();
        }
      });
      const s = app.listen(0, () => resolve({
        port: s.address().port,
        close: () => new Promise((r) => s.close(r)),
      }));
    });
    try {
      const resp = await get(server.port, '/probe');
      expect(resp.status).toBe(500);
      // Express renders an HTML error page (not JSON) — this is the bug
      // class DC-062 prevents at the helper layer.
      expect(resp.headers['content-type'] || '').toMatch(/text\/html/);
    } finally {
      await server.close();
    }
  });
});

// Mount the real route module and inject a null watcher — proves the
// the four `!caddyUpstreamWatcher` paths now respond with the intended
// 503 JSON shape, not a 500 HTML panic.
describe('caddy-upstreams JSON response shape (route file literal fix)', () => {
  // The real route module exports a factory `function({ asyncHandler, caddyUpstreamWatcher, healthChecker })`.
  // We need to provide an asyncHandler shim since the route file uses it.
  function asyncHandlerShim(fn) { return fn; }
  // The factory also depends on the asyncHandler resolving rejected
  // promises to errors. Define a simple one that just calls next(err).
  function asyncHandler(fn) {
    return (req, res, next) => {
      Promise.resolve(fn(req, res, next)).catch(next);
    };
  }

  function mountRouter(router) {
    return new Promise((resolve) => {
      const app = express();
      app.use('/api/v1', router);
      const server = app.listen(0, () => resolve({
        port: server.address().port,
        close: () => new Promise((r) => server.close(r)),
      }));
    });
  }

  function loadRoute(deps) {
    return require(path.join(repoRoot, 'routes/caddy-upstreams'))(deps);
  }

  test('GET /caddy/upstreams with null watcher — 503 JSON (regression for swap bug)', async () => {
    const router = loadRoute({
      asyncHandler,
      caddyUpstreamWatcher: null,
      healthChecker: null,
    });
    const server = await mountRouter(router);
    try {
      const resp = await get(server.port, '/api/v1/caddy/upstreams');
      expect(resp.status).toBe(503);
      expect(resp.body).toContain('"success":false');
      expect(resp.body).toContain('Caddy upstream watcher not initialized');
      expect(resp.headers['content-type'] || '').toMatch(/application\/json/);
    } finally {
      await server.close();
    }
  });

  test('POST /caddy/upstreams/:host/mute with null watcher — 503 JSON', async () => {
    const router = loadRoute({
      asyncHandler,
      caddyUpstreamWatcher: null,
      healthChecker: null,
    });
    const server = await mountRouter(router);
    try {
      const req = http.request({
        hostname: 'localhost',
        port: server.port,
        method: 'POST',
        path: '/api/v1/caddy/upstreams/100.74.102.61:8080/mute',
      }, (res) => {
        let body = '';
        res.on('data', (c) => { body += c; });
        res.on('end', () => {
          expect(res.statusCode).toBe(503);
          expect(body).toContain('"success":false');
          expect(body).toContain('Caddy upstream watcher not initialized');
          expect(res.headers['content-type'] || '').toMatch(/application\/json/);
          server.close();
        });
      });
      req.on('error', (e) => { throw e; });
      req.end();
    } finally {
      // server.close() will run via res.on('end') — defensively guard too.
      // (Don't double-close if test already returned.)
    }
  });

  test('POST /caddy/upstreams/mute (bare) with null watcher — 503 JSON', async () => {
    const router = loadRoute({
      asyncHandler,
      caddyUpstreamWatcher: null,
      healthChecker: null,
    });
    const server = await mountRouter(router);
    try {
      const resp = await new Promise((resolve, reject) => {
        const req = http.request({
          hostname: 'localhost',
          port: server.port,
          method: 'POST',
          path: '/api/v1/caddy/upstreams/mute',
          headers: { 'Content-Type': 'application/json' },
        }, (res) => {
          let body = '';
          res.on('data', (c) => { body += c; });
          res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
        });
        req.on('error', reject);
        req.end('{"host":"x","muted":true}');
      });
      expect(resp.status).toBe(503);
      expect(resp.body).toContain('Caddy upstream watcher not initialized');
      expect(resp.headers['content-type'] || '').toMatch(/application\/json/);
    } finally {
      await server.close();
    }
  });

  test('POST /caddy/upstreams/:host/unmute with null watcher — 503 JSON', async () => {
    const router = loadRoute({
      asyncHandler,
      caddyUpstreamWatcher: null,
      healthChecker: null,
    });
    const server = await mountRouter(router);
    try {
      const resp = await new Promise((resolve, reject) => {
        const req = http.request({
          hostname: 'localhost',
          port: server.port,
          method: 'POST',
          path: '/api/v1/caddy/upstreams/100.74.102.61:8080/unmute',
        }, (res) => {
          let body = '';
          res.on('data', (c) => { body += c; });
          res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
        });
        req.on('error', reject);
        req.end();
      });
      expect(resp.status).toBe(503);
      expect(resp.body).toContain('Caddy upstream watcher not initialized');
      expect(resp.headers['content-type'] || '').toMatch(/application\/json/);
    } finally {
      await server.close();
    }
  });

  test('route file source: no swapped-order patterns remain', () => {
    // Static scan of the post-fix route file: confirms the 4 swapped calls
    // are gone. If a future refactor re-introduces the pattern, this scan
    // catches it at test-time (before it ever lands in prod).
    const fs = require('fs');
    const src = fs.readFileSync(
      path.join(repoRoot, 'routes/caddy-upstreams.js'),
      'utf8'
    );
    // Match `errorResponse(res, <quote-or-backtick>, <int>)` — the
    // swapped-order shape (string literal in the 2nd arg position).
    const swappedRe = /errorResponse\(res,\s*['"`]/;
    expect(src).not.toMatch(swappedRe);
    // And confirm the corrected shape appears at least four times
    // (the four `!caddyUpstreamWatcher` guards).
    const canonicalRe = /errorResponse\(res,\s*503,\s*['"]Caddy upstream watcher not initialized['"]/g;
    const matches = src.match(canonicalRe) || [];
    expect(matches.length).toBe(4);
  });
});
