/**
 * DC-072: WebSocket exec scope-based authorization + containerId charset
 * hardening.
 *
 * Bug class under test:
 *   1. Pre-fix `routes/exec.js` captured `auth.scope` (line 39/46) but
 *      NEVER enforced it. A JWT or API key whose scope was `['read']`
 *      (a legitimate monitoring/observability scope) would be granted a
 *      full PTY-backed shell inside any running container. Container
 *      exec is root-equivalent inside the container's user namespace,
 *      so this is a privilege escalation: a read-only key holder could
 *      run arbitrary commands, exfiltrate mounted volumes, or pivot
 *      to the host network.
 *
 *   2. Pre-fix `containerId` regex `/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/`
 *      accepted mixed case, `_`, `-`, `.`, and any length up to 128.
 *      Docker container IDs are exactly 64 lowercase hex (or 12-char
 *      short form). The pre-fix validator would pass any string that
 *      looked vaguely ID-shaped; Docker's inspect() would then 404.
 *
 * Post-fix: `assertExecScope(auth)` requires `admin` scope and throws a
 * 403-tagged error. `isValidContainerId(id)` accepts only 12 or 64
 * lowercase hex chars. Both helpers are exported via `__test`.
 */

const { __test } = require('../../routes/exec');
const { assertExecScope, isValidContainerId } = __test;

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

describe('DC-072: exec WebSocket scope-based authorization', () => {
  describe('assertExecScope — admin required', () => {
    test('admin scope passes', () => {
      // Should not throw
      assertExecScope({ type: 'jwt', scope: ['admin'] });
      assertExecScope({ type: 'apikey', scope: ['admin', 'read'] });
    });

    test('read-only scope rejected with DC-072_INSUFFICIENT_SCOPE', () => {
      let caught = null;
      try {
        assertExecScope({ type: 'apikey', scope: ['read'] });
      } catch (e) {
        caught = e;
      }
      check(caught !== null, 'expected assertExecScope to throw on read-only scope');
      check(caught.code === 'DC-072_INSUFFICIENT_SCOPE', `expected code DC-072_INSUFFICIENT_SCOPE, got ${caught.code}`);
      check(caught.statusCode === 403, `expected statusCode 403, got ${caught.statusCode}`);
      check(caught.requiredScope === 'admin', `expected requiredScope=admin, got ${caught.requiredScope}`);
      check(Array.isArray(caught.actualScope) && caught.actualScope[0] === 'read', `expected actualScope=['read'], got ${JSON.stringify(caught.actualScope)}`);
    });

    test('write-only scope rejected (write ≠ admin)', () => {
      let caught = null;
      try {
        assertExecScope({ type: 'jwt', scope: ['write'] });
      } catch (e) {
        caught = e;
      }
      check(caught !== null, 'expected assertExecScope to throw on write-only scope');
      check(caught.code === 'DC-072_INSUFFICIENT_SCOPE', `expected code DC-072_INSUFFICIENT_SCOPE, got ${caught.code}`);
      check(caught.statusCode === 403, `expected statusCode 403, got ${caught.statusCode}`);
    });

    test('empty scope rejected', () => {
      let caught = null;
      try {
        assertExecScope({ type: 'apikey', scope: [] });
      } catch (e) {
        caught = e;
      }
      check(caught !== null, 'expected assertExecScope to throw on empty scope');
      check(caught.code === 'DC-072_INSUFFICIENT_SCOPE', 'expected DC-072_INSUFFICIENT_SCOPE code');
    });

    test('undefined scope rejected (null-safety)', () => {
      let caught = null;
      try {
        assertExecScope({ type: 'jwt' }); // no scope field
      } catch (e) {
        caught = e;
      }
      check(caught !== null, 'expected assertExecScope to throw on undefined scope');
      check(caught.code === 'DC-072_INSUFFICIENT_SCOPE', 'expected DC-072_INSUFFICIENT_SCOPE code');
    });

    test('null auth rejected', () => {
      let caught = null;
      try {
        assertExecScope(null);
      } catch (e) {
        caught = e;
      }
      check(caught !== null, 'expected assertExecScope to throw on null auth');
      check(caught.code === 'DC-072_INSUFFICIENT_SCOPE', 'expected DC-072_INSUFFICIENT_SCOPE code');
    });

    test('non-array scope rejected (defensive)', () => {
      let caught = null;
      try {
        assertExecScope({ type: 'apikey', scope: 'admin' }); // string, not array
      } catch (e) {
        caught = e;
      }
      check(caught !== null, 'expected assertExecScope to throw on non-array scope');
      check(caught.code === 'DC-072_INSUFFICIENT_SCOPE', 'expected DC-072_INSUFFICIENT_SCOPE code');
    });

    test('error envelope carries operator-actionable fields', () => {
      let caught = null;
      try {
        assertExecScope({ type: 'apikey', keyId: 'k_test', scope: ['read'] });
      } catch (e) {
        caught = e;
      }
      check(caught.message === 'Container exec requires admin scope', `expected canonical message, got ${caught.message}`);
      check(typeof caught.requiredScope === 'string' && caught.requiredScope === 'admin', 'requiredScope present');
      check(Array.isArray(caught.actualScope), 'actualScope is array');
    });
  });

  describe('isValidContainerId — Docker charset (12 or 64 lowercase hex)', () => {
    test('64-char lowercase hex accepted (full Docker ID)', () => {
      // Real-world example: dashcaddy-api container ID
      check(isValidContainerId('abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789') === true, '64-char hex should pass');
    });

    test('12-char lowercase hex accepted (short form)', () => {
      check(isValidContainerId('abcdef012345') === true, '12-char hex should pass');
    });

    test('uppercase hex rejected (Docker IDs are lowercase)', () => {
      check(isValidContainerId('ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789') === false, 'uppercase 64-char should fail');
      check(isValidContainerId('ABCDEF012345') === false, 'uppercase 12-char should fail');
    });

    test('mixed case rejected', () => {
      check(isValidContainerId('Abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789') === false, 'mixed case 64-char should fail');
    });

    test('non-hex chars rejected', () => {
      check(isValidContainerId('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz') === false, 'g-z hex should fail');
      check(isValidContainerId('abc!@#$%^&*()_+-=[]{}|\\:;\'",.<>/?0123456789012345678901234567890123') === false, 'special chars should fail');
    });

    test('underscore / dot / dash rejected (pre-fix allowed these)', () => {
      // Pre-fix regex accepted `_`, `-`, `.` — all are non-Docker
      check(isValidContainerId('my_container_1') === false, 'underscore should fail');
      check(isValidContainerId('my.container.1') === false, 'dot should fail');
      check(isValidContainerId('my-container-1') === false, 'dash should fail');
    });

    test('wrong length rejected', () => {
      check(isValidContainerId('abcdef0123456') === false, '13-char should fail');     // 12 + 1
      check(isValidContainerId('abcdef01234567') === false, '14-char should fail');   // 12 + 2
      check(isValidContainerId('abcdef0123456789a') === false, '65-char should fail'); // 64 + 1
    });

    test('empty string rejected', () => {
      check(isValidContainerId('') === false, 'empty string should fail');
    });

    test('null / undefined / non-string rejected (defensive)', () => {
      check(isValidContainerId(null) === false, 'null should fail');
      check(isValidContainerId(undefined) === false, 'undefined should fail');
      check(isValidContainerId(12345) === false, 'number should fail');
      check(isValidContainerId({}) === false, 'object should fail');
      check(isValidContainerId([]) === false, 'array should fail');
    });

    test('whitespace / padding rejected', () => {
      check(isValidContainerId('  abcdef012345  ') === false, 'padded should fail');
      check(isValidContainerId('\nabcdef012345\n') === false, 'CRLF-padded should fail');
    });

    test('CRLF injection rejected (defensive against pre-fix attack class)', () => {
      // Pre-fix regex accepted 128 chars with dots; a payload like
      // `aa.bb.cc.dd\r\nSet-Cookie:...` would have passed. Post-fix
      // the LF + non-hex + wrong-length combo fails on every axis.
      check(isValidContainerId('aa\r\nbb') === false, 'CRLF payload should fail');
    });
  });

  describe('__test exports shape', () => {
    test('exports assertExecScope and isValidContainerId', () => {
      check(typeof __test.assertExecScope === 'function', 'assertExecScope is a function');
      check(typeof __test.isValidContainerId === 'function', 'isValidContainerId is a function');
    });
  });
});
