/**
 * Tests for AAO Gateway Phase 2a key format helpers.
 *
 * We don't measure entropy directly (impossible against PRNG output in
 * a unit test) but we do verify shape, prefix, uniqueness over a few
 * thousand draws, and that hash + prefix are derived consistently.
 */
import { describe, it, expect } from 'vitest';
import {
  bytesToBase62,
  deriveKeyPrefix,
  generateVirtualKey,
  hashKey,
  KEY_BODY_LEN,
  KEY_PREFIX_HEAD,
  KEY_TOTAL_LEN,
} from './key-format.js';

describe('gateway/key-format', () => {
  describe('bytesToBase62', () => {
    it('emits stable length even for all-zero input', () => {
      expect(bytesToBase62(Buffer.alloc(8), 16)).toHaveLength(16);
      expect(bytesToBase62(Buffer.alloc(8), 16)).toMatch(/^0+$/);
    });

    it('uses only the base62 alphabet', () => {
      for (let i = 0; i < 50; i++) {
        const out = bytesToBase62(Buffer.from([i, i ^ 0xff, (i * 7) & 0xff]), 8);
        expect(out).toMatch(/^[0-9A-Za-z]+$/);
        expect(out).toHaveLength(8);
      }
    });

    it('throws when the input cannot fit in outLen (regression guard against truncation bias)', () => {
      // 33 bytes can encode to ~45 base62 chars; asking for 32 must fail
      // rather than silently slicing off the most-significant chars,
      // which would bias the leading character of the output.
      expect(() => bytesToBase62(Buffer.alloc(33, 0xff), 32)).toThrow(/exceeds outLen/);
    });
  });

  describe('generateVirtualKey', () => {
    it('returns a well-shaped key', () => {
      const k = generateVirtualKey();
      expect(k.raw.startsWith(KEY_PREFIX_HEAD)).toBe(true);
      expect(k.raw).toHaveLength(KEY_TOTAL_LEN);
      // Body uses base62 only.
      expect(k.raw.slice(KEY_PREFIX_HEAD.length)).toMatch(/^[0-9A-Za-z]+$/);
      expect(k.raw.slice(KEY_PREFIX_HEAD.length)).toHaveLength(KEY_BODY_LEN);
    });

    it('hash and prefix are consistent with helpers', () => {
      const k = generateVirtualKey();
      expect(k.hash).toBe(hashKey(k.raw));
      expect(k.prefix).toBe(deriveKeyPrefix(k.raw));
      // SHA-256 hex = 64 chars
      expect(k.hash).toHaveLength(64);
    });

    it('produces unique keys across many draws', () => {
      const seen = new Set<string>();
      for (let i = 0; i < 5000; i++) {
        const k = generateVirtualKey();
        expect(seen.has(k.raw)).toBe(false);
        seen.add(k.raw);
      }
      expect(seen.size).toBe(5000);
    });

    it('always emits exactly KEY_BODY_LEN body characters (no truncation, no overflow)', () => {
      // Stress-test: padding/truncation bugs typically show up
      // probabilistically when the encoded big-int hits the upper bound.
      // 1000 fresh draws is enough to surface any off-by-one in the
      // KEY_BODY_BYTES↔KEY_BODY_LEN sizing.
      for (let i = 0; i < 1000; i++) {
        const k = generateVirtualKey();
        expect(k.raw).toHaveLength(KEY_TOTAL_LEN);
        const body = k.raw.slice(KEY_PREFIX_HEAD.length);
        expect(body).toHaveLength(KEY_BODY_LEN);
        expect(body).toMatch(/^[0-9A-Za-z]+$/);
      }
    });

    it('spreads entropy across the variable-width portion of the body', () => {
      // 23 bytes of entropy always encode to ≤ 31 base62 chars (the
      // first body slot is therefore a deterministic '0' pad — that's
      // OK; entropy lives in positions 1..31). The pre-fix path used 24
      // bytes and silently truncated when the encoding hit 33 chars,
      // which biased the leading data char (only values that "shortened"
      // the encoding could survive the slice). Assert the first
      // post-pad character lands across most of base62 over many draws.
      const seen = new Set<string>();
      for (let i = 0; i < 1000; i++) {
        const k = generateVirtualKey();
        const body = k.raw.slice(KEY_PREFIX_HEAD.length);
        // Skip the deterministic leading pad ('0'); look at the first
        // entropy-bearing slot.
        seen.add(body[1]!);
      }
      // base62 has 62 chars; over 1000 draws we expect to see most of
      // them. Set a conservative lower bound (≥ 40) so the test isn't
      // flaky on a slow CI machine.
      expect(seen.size).toBeGreaterThanOrEqual(40);
    });
  });

  describe('deriveKeyPrefix', () => {
    it('returns sk-aao- + 6 chars for an AAO key', () => {
      const out = deriveKeyPrefix('sk-aao-ABCDEF0123456');
      expect(out).toBe('sk-aao-ABCDEF');
    });

    it('returns empty string for a non-AAO key', () => {
      expect(deriveKeyPrefix('sk-litellm-abcdef')).toBe('');
      expect(deriveKeyPrefix('not-a-key')).toBe('');
      expect(deriveKeyPrefix('')).toBe('');
    });

    it('handles non-string input defensively', () => {
      expect(deriveKeyPrefix(undefined as unknown as string)).toBe('');
      expect(deriveKeyPrefix(null as unknown as string)).toBe('');
    });
  });

  describe('hashKey', () => {
    it('is deterministic and 64 hex chars', () => {
      const a = hashKey('sk-aao-test');
      const b = hashKey('sk-aao-test');
      expect(a).toBe(b);
      expect(a).toMatch(/^[0-9a-f]{64}$/);
    });

    it('differs on any single character change', () => {
      const a = hashKey('sk-aao-AAAAAA');
      const b = hashKey('sk-aao-AAAAAB');
      expect(a).not.toBe(b);
    });
  });
});
