// internalAuth middleware (Session 10) — tests both the new short-form
// header (x-internal-key) and the legacy long form
// (X-VYNDR-Internal-Key), the timing-safe compare, and the optional
// loopback restriction.

const { requireInternalAuth, __internals } = require('../../src/middleware/internalAuth');

function fakeReqRes({ headers = {}, ip = '127.0.0.1' } = {}) {
  const req = {
    headers: Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])),
    ip,
    socket: { remoteAddress: ip },
    get(name) {
      return this.headers[name.toLowerCase()];
    },
  };
  const res = {
    statusCode: null,
    body: null,
    status(code) { this.statusCode = code; return this; },
    json(body) { this.body = body; return this; },
  };
  return { req, res };
}

function run(mw, req, res) {
  return new Promise((resolve) => {
    mw(req, res, () => resolve('next'));
    if (res.statusCode != null) resolve('blocked');
  });
}

beforeEach(() => {
  delete process.env.VYNDR_INTERNAL_KEY;
});

describe('requireInternalAuth', () => {
  describe('configuration guard', () => {
    test('returns 503 when VYNDR_INTERNAL_KEY env var is unset', async () => {
      const mw = requireInternalAuth();
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'anything' } });
      const out = await run(mw, req, res);
      expect(out).toBe('blocked');
      expect(res.statusCode).toBe(503);
      expect(res.body.error).toMatch(/not configured/);
    });
  });

  describe('header compatibility', () => {
    beforeEach(() => { process.env.VYNDR_INTERNAL_KEY = 'real-key-12345'; });

    test('accepts the new short-form header (x-internal-key)', async () => {
      const mw = requireInternalAuth();
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' } });
      const out = await run(mw, req, res);
      expect(out).toBe('next');
    });

    test('accepts the legacy long-form header (X-VYNDR-Internal-Key)', async () => {
      const mw = requireInternalAuth();
      const { req, res } = fakeReqRes({ headers: { 'x-vyndr-internal-key': 'real-key-12345' } });
      const out = await run(mw, req, res);
      expect(out).toBe('next');
    });

    test('returns 401 on missing header', async () => {
      const mw = requireInternalAuth();
      const { req, res } = fakeReqRes({});
      const out = await run(mw, req, res);
      expect(out).toBe('blocked');
      expect(res.statusCode).toBe(401);
    });

    test('returns 401 on wrong key (timing-safe)', async () => {
      const mw = requireInternalAuth();
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'wrong-key' } });
      const out = await run(mw, req, res);
      expect(out).toBe('blocked');
      expect(res.statusCode).toBe(401);
    });

    test('returns 401 even when prefix matches but suffix differs', async () => {
      // Guards against early-exit string compare leaking timing info.
      const mw = requireInternalAuth();
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-1234X' } });
      const out = await run(mw, req, res);
      expect(out).toBe('blocked');
      expect(res.statusCode).toBe(401);
    });

    test('returns 401 on length mismatch (avoids any timingSafeEqual throw)', async () => {
      const mw = requireInternalAuth();
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'short' } });
      const out = await run(mw, req, res);
      expect(out).toBe('blocked');
      expect(res.statusCode).toBe(401);
    });
  });

  describe('loopbackOnly option', () => {
    beforeEach(() => { process.env.VYNDR_INTERNAL_KEY = 'real-key-12345'; });

    test('passes from 127.0.0.1', async () => {
      const mw = requireInternalAuth({ loopbackOnly: true });
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '127.0.0.1' });
      const out = await run(mw, req, res);
      expect(out).toBe('next');
    });

    test('passes from IPv6 loopback', async () => {
      const mw = requireInternalAuth({ loopbackOnly: true });
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '::1' });
      const out = await run(mw, req, res);
      expect(out).toBe('next');
    });

    test('passes from IPv4-mapped IPv6 loopback', async () => {
      const mw = requireInternalAuth({ loopbackOnly: true });
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '::ffff:127.0.0.1' });
      const out = await run(mw, req, res);
      expect(out).toBe('next');
    });

    test('returns 403 from off-host IP', async () => {
      const mw = requireInternalAuth({ loopbackOnly: true });
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '10.0.0.42' });
      const out = await run(mw, req, res);
      expect(out).toBe('blocked');
      expect(res.statusCode).toBe(403);
      expect(res.body.error).toMatch(/Origin not permitted/);
    });

    test('WITHOUT loopbackOnly, off-host IPs are accepted (n8n case)', async () => {
      const mw = requireInternalAuth({ loopbackOnly: false });
      const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '172.18.0.3' });
      const out = await run(mw, req, res);
      expect(out).toBe('next');
    });
  });

  describe('timingSafeStringEqual', () => {
    const { timingSafeStringEqual } = __internals;

    test('equal strings → true', () => {
      expect(timingSafeStringEqual('abc123', 'abc123')).toBe(true);
    });
    test('different strings same length → false', () => {
      expect(timingSafeStringEqual('abc123', 'abc124')).toBe(false);
    });
    test('different lengths → false (does not throw)', () => {
      expect(() => timingSafeStringEqual('a', 'abc')).not.toThrow();
      expect(timingSafeStringEqual('a', 'abc')).toBe(false);
    });
    test('non-string inputs → false', () => {
      expect(timingSafeStringEqual(null, 'a')).toBe(false);
      expect(timingSafeStringEqual('a', undefined)).toBe(false);
      expect(timingSafeStringEqual(123, '123')).toBe(false);
    });
  });
});
