/**
 * Property-based and fuzz-style tests for the Nobulex SDK.
 *
 * Since fast-check is unavailable, we use manual random generators with
 * crypto.getRandomValues for true randomness, running 20 iterations per
 * property to cover a broad range of inputs.
 *
 * Covers: @nobulex/crypto, @nobulex/ccl, @nobulex/core, @nobulex/identity
 */

import { describe, it, expect } from 'vitest';

import {
  generateKeyPair,
  sign,
  verify,
  signString,
  sha256,
  sha256String,
  sha256Object,
  toHex,
  fromHex,
  base64urlEncode,
  base64urlDecode,
  canonicalizeJson,
  generateNonce,
  generateId,
  constantTimeEqual,
} from '@nobulex/crypto';
import type { KeyPair } from '@nobulex/crypto';

import {
  parse,
  serialize,
  evaluate,
  merge,
  validateNarrowing,
  specificity,
  matchAction,
  matchResource,
  evaluateCondition,
} from '@nobulex/ccl';
import type { CCLDocument, EvaluationContext } from '@nobulex/ccl';

import {
  buildCovenant,
  verifyCovenant,
  canonicalForm,
  computeId,
  countersignCovenant,
  resignCovenant,
  serializeCovenant,
  deserializeCovenant,
  MAX_CHAIN_DEPTH,
} from '@nobulex/core';
import type { CovenantDocument, CovenantBuilderOptions } from '@nobulex/core';

import {
  createIdentity,
  evolveIdentity,
  verifyIdentity,
  computeIdentityHash,
  computeCapabilityManifestHash,
} from '@nobulex/identity';
import type { AgentIdentity, CreateIdentityOptions } from '@nobulex/identity';

// ---------------------------------------------------------------------------
// Random generators
// ---------------------------------------------------------------------------

const ITERATIONS = 20;

function randomBytes(n: number): Uint8Array {
  const buf = new Uint8Array(n);
  crypto.getRandomValues(buf);
  return buf;
}

function randomString(n: number): string {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  const bytes = randomBytes(n);
  let result = '';
  for (let i = 0; i < n; i++) {
    result += chars[bytes[i]! % chars.length];
  }
  return result;
}

function randomInt(min: number, max: number): number {
  const range = max - min + 1;
  const bytes = randomBytes(4);
  const value = (bytes[0]! | (bytes[1]! << 8) | (bytes[2]! << 16) | ((bytes[3]! & 0x7f) << 24));
  return min + (value % range);
}

function randomHex(byteLength: number): string {
  return toHex(randomBytes(byteLength));
}

/** Generate a random CCL action (dot-separated identifier) */
function randomAction(): string {
  const segments = randomInt(1, 3);
  const parts: string[] = [];
  for (let i = 0; i < segments; i++) {
    parts.push(randomString(randomInt(3, 8)));
  }
  return parts.join('.');
}

/** Generate a random CCL resource (slash-separated path) */
function randomResource(): string {
  const segments = randomInt(1, 4);
  const parts: string[] = [];
  for (let i = 0; i < segments; i++) {
    parts.push(randomString(randomInt(3, 8)));
  }
  return '/' + parts.join('/');
}

// ---------------------------------------------------------------------------
// Helpers for building covenants
// ---------------------------------------------------------------------------

async function makeCovenantOptions(kp: KeyPair): Promise<CovenantBuilderOptions> {
  const beneficiaryKp = await generateKeyPair();
  return {
    issuer: {
      id: 'issuer-' + randomString(6),
      publicKey: kp.publicKeyHex,
      role: 'issuer' as const,
    },
    beneficiary: {
      id: 'beneficiary-' + randomString(6),
      publicKey: beneficiaryKp.publicKeyHex,
      role: 'beneficiary' as const,
    },
    constraints: `permit read on '/data'`,
    privateKey: kp.privateKey,
  };
}

async function makeIdentityOptions(kp: KeyPair): Promise<CreateIdentityOptions> {
  return {
    operatorKeyPair: kp,
    model: {
      provider: 'test-provider',
      modelId: 'test-model',
      modelVersion: '1.0',
    },
    capabilities: ['read', 'write', 'execute'],
    deployment: {
      runtime: 'process' as const,
      region: 'us-east-1',
    },
  };
}

// ============================================================================
// CRYPTO PROPERTIES
// ============================================================================

describe('Crypto Properties', () => {
  // ---- Sign / Verify ----

  describe('Sign/Verify roundtrip', () => {
    it('for any message and key pair, sign then verify always returns true', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const message = randomBytes(randomInt(1, 256));
        const signature = await sign(message, kp.privateKey);
        const valid = await verify(message, signature, kp.publicKey);
        expect(valid).toBe(true);
      }
    });

    it('signString then verify works for arbitrary UTF-8 strings', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const msg = randomString(randomInt(1, 200));
        const sig = await signString(msg, kp.privateKey);
        const msgBytes = new TextEncoder().encode(msg);
        const valid = await verify(msgBytes, sig, kp.publicKey);
        expect(valid).toBe(true);
      }
    });

    it('empty message sign/verify roundtrips', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const message = new Uint8Array(0);
        const sig = await sign(message, kp.privateKey);
        const valid = await verify(message, sig, kp.publicKey);
        expect(valid).toBe(true);
      }
    });
  });

  describe('Different messages produce different signatures', () => {
    it('distinct messages yield distinct signatures with overwhelming probability', async () => {
      const kp = await generateKeyPair();
      const sigs = new Set<string>();
      for (let i = 0; i < ITERATIONS; i++) {
        const msg = randomBytes(32);
        const sig = await sign(msg, kp.privateKey);
        sigs.add(toHex(sig));
      }
      // With 20 random 32-byte messages, all signatures should be unique
      expect(sigs.size).toBe(ITERATIONS);
    });
  });

  describe('Wrong key always fails verification', () => {
    it('signature from key A does not verify with key B', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kpA = await generateKeyPair();
        const kpB = await generateKeyPair();
        const message = randomBytes(64);
        const sig = await sign(message, kpA.privateKey);
        const valid = await verify(message, sig, kpB.publicKey);
        expect(valid).toBe(false);
      }
    });

    it('verification with corrupted public key fails', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const message = randomBytes(32);
        const sig = await sign(message, kp.privateKey);
        // Corrupt the public key
        const badKey = new Uint8Array(kp.publicKey);
        badKey[0] = badKey[0]! ^ 0xff;
        const valid = await verify(message, sig, badKey);
        expect(valid).toBe(false);
      }
    });

    it('verification with corrupted signature fails', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const message = randomBytes(32);
        const sig = await sign(message, kp.privateKey);
        // Corrupt the signature
        const badSig = new Uint8Array(sig);
        badSig[randomInt(0, badSig.length - 1)] ^= 0xff;
        const valid = await verify(message, badSig, kp.publicKey);
        expect(valid).toBe(false);
      }
    });

    it('verification with corrupted message fails', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const message = randomBytes(32);
        const sig = await sign(message, kp.privateKey);
        // Corrupt the message
        const badMsg = new Uint8Array(message);
        badMsg[randomInt(0, badMsg.length - 1)] ^= 0xff;
        const valid = await verify(badMsg, sig, kp.publicKey);
        expect(valid).toBe(false);
      }
    });
  });

  // ---- SHA-256 ----

  describe('SHA-256 determinism', () => {
    it('same input always produces the same hash', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const data = randomBytes(randomInt(0, 512));
        const hash1 = sha256(data);
        const hash2 = sha256(data);
        expect(hash1).toBe(hash2);
      }
    });

    it('sha256String is deterministic for the same string', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const str = randomString(randomInt(0, 200));
        expect(sha256String(str)).toBe(sha256String(str));
      }
    });

    it('sha256Object is deterministic for the same object', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const obj = { a: randomString(5), b: randomInt(0, 1000), c: [1, 2, 3] };
        expect(sha256Object(obj)).toBe(sha256Object(obj));
      }
    });
  });

  describe('SHA-256 collision resistance', () => {
    it('different inputs produce different hashes', () => {
      const hashes = new Set<string>();
      for (let i = 0; i < ITERATIONS; i++) {
        const data = randomBytes(32);
        hashes.add(sha256(data));
      }
      expect(hashes.size).toBe(ITERATIONS);
    });

    it('appending a single byte changes the hash', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const data = randomBytes(randomInt(1, 64));
        const extended = new Uint8Array(data.length + 1);
        extended.set(data);
        extended[data.length] = randomInt(0, 255);
        expect(sha256(data)).not.toBe(sha256(extended));
      }
    });

    it('sha256 output is always 64 hex characters (32 bytes)', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const data = randomBytes(randomInt(0, 1000));
        const hash = sha256(data);
        expect(hash).toMatch(/^[0-9a-f]{64}$/);
      }
    });
  });

  // ---- toHex / fromHex ----

  describe('toHex/fromHex roundtrip', () => {
    it('fromHex(toHex(bytes)) equals original bytes', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const original = randomBytes(randomInt(0, 128));
        const hex = toHex(original);
        const decoded = fromHex(hex);
        expect(decoded).toEqual(original);
      }
    });

    it('toHex produces lowercase hex of correct length', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const len = randomInt(0, 100);
        const data = randomBytes(len);
        const hex = toHex(data);
        expect(hex.length).toBe(len * 2);
        expect(hex).toMatch(/^[0-9a-f]*$/);
      }
    });

    it('fromHex throws for odd-length strings', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const oddHex = randomHex(randomInt(1, 50)) + 'a'; // ensure odd by appending single char
        // oddHex may actually be even length after append; ensure odd
        const testHex = oddHex.length % 2 === 1 ? oddHex : oddHex + 'b';
        if (testHex.length % 2 === 1) {
          expect(() => fromHex(testHex)).toThrow('odd length');
        }
      }
    });

    it('empty bytes roundtrip to empty hex', () => {
      const empty = new Uint8Array(0);
      expect(toHex(empty)).toBe('');
      expect(fromHex('')).toEqual(empty);
    });
  });

  // ---- base64url ----

  describe('base64url encode/decode roundtrip', () => {
    it('decode(encode(bytes)) equals original bytes', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const original = randomBytes(randomInt(0, 256));
        const encoded = base64urlEncode(original);
        const decoded = base64urlDecode(encoded);
        expect(decoded).toEqual(original);
      }
    });

    it('base64url output contains no +, /, or = characters', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const data = randomBytes(randomInt(1, 100));
        const encoded = base64urlEncode(data);
        expect(encoded).not.toContain('+');
        expect(encoded).not.toContain('/');
        expect(encoded).not.toContain('=');
      }
    });

    it('empty input roundtrips correctly', () => {
      const empty = new Uint8Array(0);
      const encoded = base64urlEncode(empty);
      const decoded = base64urlDecode(encoded);
      expect(decoded).toEqual(empty);
    });

    it('single byte roundtrips correctly for all values 0-255', () => {
      // Fuzz a subset of byte values
      for (let i = 0; i < ITERATIONS; i++) {
        const byte = randomInt(0, 255);
        const data = new Uint8Array([byte]);
        const decoded = base64urlDecode(base64urlEncode(data));
        expect(decoded).toEqual(data);
      }
    });
  });

  // ---- canonicalizeJson ----

  describe('canonicalizeJson determinism', () => {
    it('same object always produces the same canonical form', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const obj = {
          z: randomString(5),
          a: randomInt(0, 100),
          m: [randomString(3), randomString(3)],
        };
        expect(canonicalizeJson(obj)).toBe(canonicalizeJson(obj));
      }
    });

    it('key order does not matter: {a,b} and {b,a} produce the same output', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const valA = randomString(5);
        const valB = randomInt(0, 1000);
        const obj1 = { a: valA, b: valB };
        const obj2 = { b: valB, a: valA };
        expect(canonicalizeJson(obj1)).toBe(canonicalizeJson(obj2));
      }
    });

    it('nested key order is also normalized', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const v1 = randomString(4);
        const v2 = randomString(4);
        const obj1 = { outer: { z: v1, a: v2 } };
        const obj2 = { outer: { a: v2, z: v1 } };
        expect(canonicalizeJson(obj1)).toBe(canonicalizeJson(obj2));
      }
    });

    it('output is valid JSON', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const obj = {
          key: randomString(10),
          num: randomInt(0, 9999),
          arr: [1, 2, 3],
          nested: { inner: randomString(5) },
        };
        const canonical = canonicalizeJson(obj);
        expect(() => JSON.parse(canonical)).not.toThrow();
      }
    });

    it('null and primitive values are handled correctly', () => {
      expect(canonicalizeJson(null)).toBe('null');
      expect(canonicalizeJson(42)).toBe('42');
      expect(canonicalizeJson(true)).toBe('true');
      expect(canonicalizeJson('hello')).toBe('"hello"');
    });
  });

  // ---- generateNonce ----

  describe('generateNonce uniqueness', () => {
    it('produces unique values across multiple calls', () => {
      const nonces = new Set<string>();
      for (let i = 0; i < ITERATIONS; i++) {
        nonces.add(toHex(generateNonce()));
      }
      expect(nonces.size).toBe(ITERATIONS);
    });

    it('nonce is always 32 bytes (64 hex chars)', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const nonce = generateNonce();
        expect(nonce.length).toBe(32);
        expect(toHex(nonce)).toMatch(/^[0-9a-f]{64}$/);
      }
    });
  });

  describe('generateId uniqueness', () => {
    it('produces unique IDs', () => {
      const ids = new Set<string>();
      for (let i = 0; i < ITERATIONS; i++) {
        ids.add(generateId());
      }
      expect(ids.size).toBe(ITERATIONS);
    });

    it('default ID is 32 hex chars (16 bytes)', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const id = generateId();
        expect(id).toMatch(/^[0-9a-f]{32}$/);
      }
    });
  });

  // ---- constantTimeEqual ----

  describe('constantTimeEqual', () => {
    it('equal buffers return true', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const len = randomInt(1, 64);
        const buf = randomBytes(len);
        const copy = new Uint8Array(buf);
        expect(constantTimeEqual(buf, copy)).toBe(true);
      }
    });

    it('different buffers of same length return false', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const len = randomInt(1, 64);
        const a = randomBytes(len);
        const b = new Uint8Array(a);
        b[randomInt(0, len - 1)] ^= 0xff; // flip one byte
        expect(constantTimeEqual(a, b)).toBe(false);
      }
    });

    it('different length buffers return false', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const a = randomBytes(randomInt(1, 64));
        const b = randomBytes(a.length + randomInt(1, 10));
        expect(constantTimeEqual(a, b)).toBe(false);
      }
    });

    it('empty buffers are equal', () => {
      expect(constantTimeEqual(new Uint8Array(0), new Uint8Array(0))).toBe(true);
    });

    it('self-comparison is always true', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const buf = randomBytes(randomInt(0, 128));
        expect(constantTimeEqual(buf, buf)).toBe(true);
      }
    });
  });
});

// ============================================================================
// CCL PROPERTIES
// ============================================================================

describe('CCL Properties', () => {
  // ---- Parse / Serialize roundtrip ----

  describe('Parse/Serialize roundtrip', () => {
    it('parse then serialize produces functionally equivalent CCL for permit rules', () => {
      const actions = ['read', 'write', 'delete', 'execute', 'list'];
      const resources = ['/data', '/files', '/api', '/secrets', '/logs'];

      for (let i = 0; i < ITERATIONS; i++) {
        const action = actions[randomInt(0, actions.length - 1)]!;
        const resource = resources[randomInt(0, resources.length - 1)]!;
        const source = `permit ${action} on '${resource}'`;
        const parsed = parse(source);
        const serialized = serialize(parsed);
        const reparsed = parse(serialized);

        // Both should have the same number of statements
        expect(reparsed.statements.length).toBe(parsed.statements.length);
        // And both should evaluate the same way
        const result1 = evaluate(parsed, action, resource);
        const result2 = evaluate(reparsed, action, resource);
        expect(result1.permitted).toBe(result2.permitted);
      }
    });

    it('parse then serialize roundtrips deny rules', () => {
      const cclSources = [
        "deny write on '/secrets'",
        "deny delete on '/production'",
        "deny execute on '/admin'",
      ];

      for (const source of cclSources) {
        const parsed = parse(source);
        const serialized = serialize(parsed);
        const reparsed = parse(serialized);
        expect(reparsed.denies.length).toBe(parsed.denies.length);
        expect(reparsed.denies[0]!.action).toBe(parsed.denies[0]!.action);
        expect(reparsed.denies[0]!.resource).toBe(parsed.denies[0]!.resource);
      }
    });

    it('parse then serialize roundtrips limit rules', () => {
      const limits = [
        'limit read 100 per 1 hours',
        'limit write 50 per 1 minutes',
        'limit delete 10 per 1 days',
      ];

      for (const source of limits) {
        const parsed = parse(source);
        const serialized = serialize(parsed);
        const reparsed = parse(serialized);
        expect(reparsed.limits.length).toBe(parsed.limits.length);
        expect(reparsed.limits[0]!.count).toBe(parsed.limits[0]!.count);
        expect(reparsed.limits[0]!.periodSeconds).toBe(parsed.limits[0]!.periodSeconds);
      }
    });
  });

  // ---- Deny wins ----

  describe('Deny wins over permit', () => {
    it('if a deny and permit match the same action/resource, deny wins', () => {
      const actions = ['read', 'write', 'delete', 'execute'];
      const resources = ['/data', '/files', '/api', '/logs'];

      for (let i = 0; i < ITERATIONS; i++) {
        const action = actions[randomInt(0, actions.length - 1)]!;
        const resource = resources[randomInt(0, resources.length - 1)]!;
        const source = `permit ${action} on '${resource}'\ndeny ${action} on '${resource}'`;
        const doc = parse(source);
        const result = evaluate(doc, action, resource);
        expect(result.permitted).toBe(false);
      }
    });

    it('deny on same resource overrides permit at equal specificity', () => {
      const actions = ['read', 'write', 'delete', 'execute', 'list', 'update', 'create', 'patch'];
      const resources = ['/data', '/files', '/api', '/logs', '/config', '/users'];

      for (let i = 0; i < ITERATIONS; i++) {
        const action = actions[randomInt(0, actions.length - 1)]!;
        const resource = resources[randomInt(0, resources.length - 1)]!;
        const source = `permit ${action} on '${resource}'\ndeny ${action} on '${resource}'`;
        const doc = parse(source);
        const result = evaluate(doc, action, resource);
        // At equal specificity, deny wins
        expect(result.permitted).toBe(false);
      }
    });
  });

  // ---- Merge commutativity for denies ----

  describe('Merge commutativity for denies', () => {
    it('merge(a,b) and merge(b,a) agree on deny results', () => {
      const cclPairs = [
        ["deny read on '/secrets'", "permit read on '/data'"],
        ["deny write on '/prod'", "deny delete on '/prod'"],
        ["permit read on '/logs'", "deny read on '/logs'"],
      ];

      for (const [srcA, srcB] of cclPairs) {
        const a = parse(srcA!);
        const b = parse(srcB!);
        const mergeAB = merge(a, b);
        const mergeBA = merge(b, a);

        // Both should have all denies from both documents
        expect(mergeAB.denies.length).toBe(mergeBA.denies.length);

        // Evaluation on denied resources should agree
        for (const deny of a.denies) {
          const resultAB = evaluate(mergeAB, deny.action, deny.resource);
          const resultBA = evaluate(mergeBA, deny.action, deny.resource);
          expect(resultAB.permitted).toBe(resultBA.permitted);
        }
        for (const deny of b.denies) {
          const resultAB = evaluate(mergeAB, deny.action, deny.resource);
          const resultBA = evaluate(mergeBA, deny.action, deny.resource);
          expect(resultAB.permitted).toBe(resultBA.permitted);
        }
      }
    });
  });

  // ---- Narrowing ----

  describe('Narrowing: child that only denies is a valid narrowing of parent', () => {
    it('a child with only deny rules narrows any parent', () => {
      const parents = [
        "permit read on '/data'",
        "permit read on '/data'\npermit write on '/data'",
        "permit read on '**'",
      ];
      const childDenies = [
        "deny write on '/data'",
        "deny delete on '/data'",
        "deny execute on '/admin'",
      ];

      for (const parentSrc of parents) {
        for (const childSrc of childDenies) {
          const parent = parse(parentSrc);
          const child = parse(childSrc);
          const result = validateNarrowing(parent, child);
          expect(result.valid).toBe(true);
        }
      }
    });

    it('a child that permits what parent denies is NOT a valid narrowing', () => {
      const parentSrc = "deny write on '/secrets'";
      const childSrc = "permit write on '/secrets'";

      const parent = parse(parentSrc);
      const child = parse(childSrc);
      const result = validateNarrowing(parent, child);
      expect(result.valid).toBe(false);
      expect(result.violations.length).toBeGreaterThan(0);
    });
  });

  // ---- Empty context ----

  describe('Empty context matches unconditional rules', () => {
    it('rules without conditions are matched with empty context', () => {
      const actions = ['read', 'write', 'delete', 'execute'];
      const resources = ['/data', '/files', '/api', '/logs'];

      for (let i = 0; i < ITERATIONS; i++) {
        const action = actions[randomInt(0, actions.length - 1)]!;
        const resource = resources[randomInt(0, resources.length - 1)]!;
        const source = `permit ${action} on '${resource}'`;
        const doc = parse(source);
        const result = evaluate(doc, action, resource, {});
        expect(result.permitted).toBe(true);
      }
    });

    it('rules with conditions fail when context is empty', () => {
      const source = "permit read on '/data' when user.role = 'admin'";
      const doc = parse(source);
      const result = evaluate(doc, 'read', '/data', {});
      expect(result.permitted).toBe(false);
    });
  });

  // ---- Wildcard ** matches any resource ----

  describe("Wildcard '**' matches any resource", () => {
    it('** resource pattern matches arbitrary paths', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const resource = randomResource();
        const source = "permit read on '**'";
        const doc = parse(source);
        const result = evaluate(doc, 'read', resource);
        expect(result.permitted).toBe(true);
      }
    });

    it('** action pattern matches arbitrary actions', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const action = randomAction();
        const source = "permit ** on '/data'";
        const doc = parse(source);
        const result = evaluate(doc, action, '/data');
        expect(result.permitted).toBe(true);
      }
    });

    it('matchResource with ** matches any path', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const resource = '/' + randomString(5) + '/' + randomString(5) + '/' + randomString(5);
        expect(matchResource('**', resource)).toBe(true);
      }
    });
  });

  // ---- Specificity ordering is total ----

  describe('Specificity ordering is total (no cycles)', () => {
    it('specificity is a non-negative integer', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const action = randomAction();
        const resource = randomResource();
        const spec = specificity(action, resource);
        expect(spec).toBeGreaterThanOrEqual(0);
        expect(Number.isInteger(spec)).toBe(true);
      }
    });

    it('more specific patterns have higher or equal specificity scores', () => {
      // Literal segments are more specific than wildcards
      expect(specificity('read', '/data/file')).toBeGreaterThan(specificity('**', '**'));
      expect(specificity('read', '/data')).toBeGreaterThan(specificity('*', '**'));
      expect(specificity('read.sub', '/a/b')).toBeGreaterThan(specificity('read', '/a'));
    });

    it('specificity is transitive: if a >= b and b >= c then a >= c', () => {
      const patterns: [string, string][] = [];
      for (let i = 0; i < ITERATIONS; i++) {
        patterns.push([randomAction(), randomResource()]);
      }

      // Sort by specificity
      patterns.sort((a, b) => specificity(a[0], a[1]) - specificity(b[0], b[1]));

      // Verify sorted order is consistent
      for (let i = 1; i < patterns.length; i++) {
        expect(specificity(patterns[i]![0], patterns[i]![1])).toBeGreaterThanOrEqual(
          specificity(patterns[i - 1]![0], patterns[i - 1]![1])
        );
      }
    });

    it('specificity is reflexive: spec(a, r) === spec(a, r)', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const action = randomAction();
        const resource = randomResource();
        expect(specificity(action, resource)).toBe(specificity(action, resource));
      }
    });
  });

  // ---- matchAction ----

  describe('matchAction properties', () => {
    it('exact action always matches itself', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const action = randomAction();
        expect(matchAction(action, action)).toBe(true);
      }
    });

    it('** matches any action', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const action = randomAction();
        expect(matchAction('**', action)).toBe(true);
      }
    });
  });

  // ---- matchResource ----

  describe('matchResource properties', () => {
    it('exact resource always matches itself', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const resource = randomResource();
        expect(matchResource(resource, resource)).toBe(true);
      }
    });

    it('** matches any resource path', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const resource = randomResource();
        expect(matchResource('**', resource)).toBe(true);
      }
    });
  });

  // ---- Default deny ----

  describe('Default deny when no rules match', () => {
    it('evaluating an action with no matching rules returns denied', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const source = "permit read on '/data'";
        const doc = parse(source);
        const unmatchedAction = 'unmatched_' + randomString(5);
        const result = evaluate(doc, unmatchedAction, '/data');
        expect(result.permitted).toBe(false);
      }
    });

    it('evaluating a resource with no matching rules returns denied', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const source = "permit read on '/data'";
        const doc = parse(source);
        const unmatchedResource = '/unmatched/' + randomString(5);
        const result = evaluate(doc, 'read', unmatchedResource);
        expect(result.permitted).toBe(false);
      }
    });
  });

  // ---- evaluateCondition ----

  describe('evaluateCondition properties', () => {
    it('equality condition passes when context has matching value', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const val = randomString(8);
        const condition = { field: 'user.role', operator: '=' as const, value: val };
        const ctx: EvaluationContext = { user: { role: val } };
        expect(evaluateCondition(condition, ctx)).toBe(true);
      }
    });

    it('inequality condition passes for different values', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const val1 = 'val_a_' + randomString(5);
        const val2 = 'val_b_' + randomString(5);
        const condition = { field: 'user.role', operator: '!=' as const, value: val1 };
        const ctx: EvaluationContext = { user: { role: val2 } };
        expect(evaluateCondition(condition, ctx)).toBe(true);
      }
    });

    it('missing context field evaluates to false (safe default)', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const condition = {
          field: 'missing.' + randomString(5),
          operator: '=' as const,
          value: 'anything',
        };
        expect(evaluateCondition(condition, {})).toBe(false);
      }
    });
  });
});

// ============================================================================
// CORE PROPERTIES
// ============================================================================

describe('Core Properties', () => {
  // ---- Build / Verify roundtrip ----

  describe('Build/Verify roundtrip', () => {
    it('any covenant built with buildCovenant passes verifyCovenant', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const result = await verifyCovenant(doc);
        expect(result.valid).toBe(true);
      }
    });

    it('built covenants have all 11 checks passing', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const result = await verifyCovenant(doc);
        expect(result.checks.length).toBe(11);
        for (const check of result.checks) {
          expect(check.passed).toBe(true);
        }
      }
    });
  });

  // ---- Tampered documents fail verification ----

  describe('Tampered documents fail verification', () => {
    it('modifying constraints after building fails verification', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const tampered = { ...doc, constraints: "deny read on '/everything'" };
        const result = await verifyCovenant(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying the issuer id after building fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const tampered = { ...doc, issuer: { ...doc.issuer, id: 'tampered-' + randomString(5) } };
        const result = await verifyCovenant(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying the nonce after building fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const tampered = { ...doc, nonce: toHex(randomBytes(32)) };
        const result = await verifyCovenant(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying the signature after building fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const tampered = { ...doc, signature: randomHex(64) };
        const result = await verifyCovenant(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying the beneficiary after building fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const tampered = {
          ...doc,
          beneficiary: { ...doc.beneficiary, id: 'tampered-' + randomString(5) },
        };
        const result = await verifyCovenant(tampered);
        expect(result.valid).toBe(false);
      }
    });
  });

  // ---- canonicalForm determinism ----

  describe('canonicalForm is deterministic', () => {
    it('same document always produces the same canonical form', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const c1 = canonicalForm(doc);
        const c2 = canonicalForm(doc);
        expect(c1).toBe(c2);
      }
    });

    it('canonical form excludes id, signature, and countersignatures', async () => {
      const kp = await generateKeyPair();
      const options = await makeCovenantOptions(kp);
      const doc = await buildCovenant(options);
      const canonical = canonicalForm(doc);
      const parsed = JSON.parse(canonical);
      expect(parsed).not.toHaveProperty('id');
      expect(parsed).not.toHaveProperty('signature');
      expect(parsed).not.toHaveProperty('countersignatures');
    });
  });

  // ---- computeId determinism ----

  describe('computeId is deterministic', () => {
    it('same document always produces the same ID', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const id1 = computeId(doc);
        const id2 = computeId(doc);
        expect(id1).toBe(id2);
      }
    });

    it('computeId matches the document id field', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        expect(computeId(doc)).toBe(doc.id);
      }
    });
  });

  // ---- Countersigned documents still pass verification ----

  describe('Countersigned documents pass verification', () => {
    it('adding a countersignature does not break verification', async () => {
      for (let i = 0; i < 5; i++) {
        const issuerKp = await generateKeyPair();
        const options = await makeCovenantOptions(issuerKp);
        const doc = await buildCovenant(options);
        const auditorKp = await generateKeyPair();
        const countersigned = await countersignCovenant(doc, auditorKp, 'auditor');
        const result = await verifyCovenant(countersigned);
        expect(result.valid).toBe(true);
      }
    });

    it('multiple countersignatures all pass verification', async () => {
      const issuerKp = await generateKeyPair();
      const options = await makeCovenantOptions(issuerKp);
      let doc = await buildCovenant(options);

      for (let i = 0; i < 5; i++) {
        const signerKp = await generateKeyPair();
        doc = await countersignCovenant(doc, signerKp, 'auditor');
      }

      const result = await verifyCovenant(doc);
      expect(result.valid).toBe(true);
      expect(doc.countersignatures!.length).toBe(5);
    });
  });

  // ---- resignCovenant produces valid documents ----

  describe('resignCovenant produces valid documents', () => {
    it('re-signed documents pass verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const resigned = await resignCovenant(doc, kp.privateKey);
        const result = await verifyCovenant(resigned);
        expect(result.valid).toBe(true);
      }
    });

    it('re-signed documents have a different nonce and id', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const resigned = await resignCovenant(doc, kp.privateKey);
        expect(resigned.nonce).not.toBe(doc.nonce);
        expect(resigned.id).not.toBe(doc.id);
      }
    });

    it('re-signing strips countersignatures', async () => {
      const kp = await generateKeyPair();
      const options = await makeCovenantOptions(kp);
      const doc = await buildCovenant(options);
      const auditorKp = await generateKeyPair();
      const countersigned = await countersignCovenant(doc, auditorKp, 'auditor');
      expect(countersigned.countersignatures!.length).toBe(1);
      const resigned = await resignCovenant(countersigned, kp.privateKey);
      expect(resigned.countersignatures).toBeUndefined();
    });
  });

  // ---- Chain depth validation ----

  describe('Chain depth validation', () => {
    it('chain depth within limit passes verification', async () => {
      const kp = await generateKeyPair();
      const parentOptions = await makeCovenantOptions(kp);
      const parent = await buildCovenant(parentOptions);

      const childKp = await generateKeyPair();
      const childOptions = await makeCovenantOptions(childKp);
      childOptions.chain = {
        parentId: parent.id,
        relation: 'delegates',
        depth: 1,
      };
      const child = await buildCovenant(childOptions);
      const result = await verifyCovenant(child);
      expect(result.valid).toBe(true);
    });

    it('chain depth exceeding MAX_CHAIN_DEPTH fails at build time', async () => {
      const kp = await generateKeyPair();
      const options = await makeCovenantOptions(kp);
      options.chain = {
        parentId: sha256String('parent') as any,
        relation: 'delegates',
        depth: MAX_CHAIN_DEPTH + 1,
      };
      await expect(buildCovenant(options)).rejects.toThrow('chain.depth');
    });
  });

  // ---- Serialize / Deserialize roundtrip ----

  describe('deserializeCovenant(serializeCovenant(doc)) roundtrips', () => {
    it('roundtrip preserves all fields', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const serialized = serializeCovenant(doc);
        const deserialized = deserializeCovenant(serialized);

        expect(deserialized.id).toBe(doc.id);
        expect(deserialized.version).toBe(doc.version);
        expect(deserialized.issuer.id).toBe(doc.issuer.id);
        expect(deserialized.issuer.publicKey).toBe(doc.issuer.publicKey);
        expect(deserialized.beneficiary.id).toBe(doc.beneficiary.id);
        expect(deserialized.constraints).toBe(doc.constraints);
        expect(deserialized.nonce).toBe(doc.nonce);
        expect(deserialized.signature).toBe(doc.signature);
      }
    });

    it('deserialized documents still pass verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeCovenantOptions(kp);
        const doc = await buildCovenant(options);
        const deserialized = deserializeCovenant(serializeCovenant(doc));
        const result = await verifyCovenant(deserialized);
        expect(result.valid).toBe(true);
      }
    });

    it('malformed JSON throws on deserialization', () => {
      const badInputs = [
        'not json',
        '[]',
        'null',
        '42',
        '"string"',
        '{"id": 42}',
      ];
      for (const input of badInputs) {
        expect(() => deserializeCovenant(input)).toThrow();
      }
    });
  });

  // ---- Cross-key isolation ----

  describe('Cross-key isolation', () => {
    it('document signed by key A cannot be verified if issuer publicKey is changed to key B', async () => {
      for (let i = 0; i < 5; i++) {
        const kpA = await generateKeyPair();
        const kpB = await generateKeyPair();
        const options = await makeCovenantOptions(kpA);
        const doc = await buildCovenant(options);

        // Change the issuer's public key to kpB
        const tampered = {
          ...doc,
          issuer: { ...doc.issuer, publicKey: kpB.publicKeyHex },
        };
        const result = await verifyCovenant(tampered);
        expect(result.valid).toBe(false);
      }
    });
  });
});

// ============================================================================
// IDENTITY PROPERTIES
// ============================================================================

describe('Identity Properties', () => {
  // ---- Create / Verify roundtrip ----

  describe('Create/Verify roundtrip', () => {
    it('any identity created with createIdentity passes verifyIdentity', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const result = await verifyIdentity(identity);
        expect(result.valid).toBe(true);
      }
    });

    it('all verification checks pass for freshly created identities', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const result = await verifyIdentity(identity);
        for (const check of result.checks) {
          expect(check.passed).toBe(true);
        }
      }
    });

    it('created identity has version 1 and one lineage entry', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        expect(identity.version).toBe(1);
        expect(identity.lineage.length).toBe(1);
        expect(identity.lineage[0]!.changeType).toBe('created');
        expect(identity.lineage[0]!.parentHash).toBeNull();
      }
    });
  });

  // ---- Evolved identities maintain lineage ----

  describe('Evolved identities maintain lineage', () => {
    it('evolved identity has incremented version and extended lineage', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);

        const evolved = await evolveIdentity(identity, {
          operatorKeyPair: kp,
          changeType: 'capability_change',
          description: 'Added new capability',
          updates: {
            capabilities: ['read', 'write', 'execute', 'admin'],
          },
        });

        expect(evolved.version).toBe(identity.version + 1);
        expect(evolved.lineage.length).toBe(identity.lineage.length + 1);

        // Last lineage entry should reference previous hash
        const lastEntry = evolved.lineage[evolved.lineage.length - 1]!;
        const prevEntry = identity.lineage[identity.lineage.length - 1]!;
        expect(lastEntry.parentHash).toBe(prevEntry.identityHash);
      }
    });

    it('evolved identity still passes verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);

        const evolved = await evolveIdentity(identity, {
          operatorKeyPair: kp,
          changeType: 'model_update',
          description: 'Updated model version',
          updates: {
            model: {
              provider: 'test-provider',
              modelId: 'test-model',
              modelVersion: '2.0',
            },
          },
        });

        const result = await verifyIdentity(evolved);
        expect(result.valid).toBe(true);
      }
    });

    it('multi-step evolution preserves lineage chain integrity', async () => {
      const kp = await generateKeyPair();
      const options = await makeIdentityOptions(kp);
      let identity = await createIdentity(options);

      for (let step = 0; step < 5; step++) {
        identity = await evolveIdentity(identity, {
          operatorKeyPair: kp,
          changeType: 'capability_change',
          description: `Step ${step + 1}`,
          updates: {
            capabilities: ['read', 'write', `cap_${step}`],
          },
        });

        const result = await verifyIdentity(identity);
        expect(result.valid).toBe(true);
        expect(identity.version).toBe(step + 2);
        expect(identity.lineage.length).toBe(step + 2);
      }
    });
  });

  // ---- Different operators produce different identity hashes ----

  describe('Different operators produce different identity hashes', () => {
    it('identities from different key pairs have different ids', async () => {
      const ids = new Set<string>();
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        ids.add(identity.id);
      }
      expect(ids.size).toBe(ITERATIONS);
    });

    it('identities with different capabilities have different manifest hashes', async () => {
      const hashes = new Set<string>();
      for (let i = 0; i < ITERATIONS; i++) {
        const caps = [randomString(8), randomString(8), randomString(8)];
        const hash = computeCapabilityManifestHash(caps);
        hashes.add(hash);
      }
      expect(hashes.size).toBe(ITERATIONS);
    });

    it('computeCapabilityManifestHash is order-independent', () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const a = randomString(5);
        const b = randomString(5);
        const c = randomString(5);
        const hash1 = computeCapabilityManifestHash([a, b, c]);
        const hash2 = computeCapabilityManifestHash([c, a, b]);
        const hash3 = computeCapabilityManifestHash([b, c, a]);
        expect(hash1).toBe(hash2);
        expect(hash2).toBe(hash3);
      }
    });
  });

  // ---- Tampered identities fail verification ----

  describe('Tampered identities fail verification', () => {
    it('modifying the operator public key after creation fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const kp2 = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const tampered = { ...identity, operatorPublicKey: kp2.publicKeyHex };
        const result = await verifyIdentity(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying the signature fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const tampered = { ...identity, signature: randomHex(64) };
        const result = await verifyIdentity(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying the id fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const tampered = { ...identity, id: sha256String('fake') };
        const result = await verifyIdentity(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying capabilities after creation fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const tampered = { ...identity, capabilities: ['tampered'] };
        const result = await verifyIdentity(tampered);
        expect(result.valid).toBe(false);
      }
    });

    it('modifying the version number fails verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const tampered = { ...identity, version: 99 };
        const result = await verifyIdentity(tampered);
        expect(result.valid).toBe(false);
      }
    });
  });

  // ---- Identity hash determinism ----

  describe('computeIdentityHash is deterministic', () => {
    it('same identity input produces same hash', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const options = await makeIdentityOptions(kp);
        const identity = await createIdentity(options);
        const { id: _id, signature: _sig, ...rest } = identity;
        const hash1 = computeIdentityHash(rest as any);
        const hash2 = computeIdentityHash(rest as any);
        expect(hash1).toBe(hash2);
      }
    });
  });

  // ---- Operator transfer changes identity ----

  describe('Operator transfer changes identity', () => {
    it('transferring operator produces a new identity that passes verification', async () => {
      for (let i = 0; i < 5; i++) {
        const kp1 = await generateKeyPair();
        const kp2 = await generateKeyPair();
        const options = await makeIdentityOptions(kp1);
        const identity = await createIdentity(options);

        const transferred = await evolveIdentity(identity, {
          operatorKeyPair: kp1,
          changeType: 'operator_transfer',
          description: 'Transfer to new operator',
          updates: {
            operatorPublicKey: kp2.publicKeyHex,
          },
        });

        // The transferred identity is signed by the OLD key (kp1),
        // but records the new operator public key.
        // verifyIdentity checks signature against operatorPublicKey,
        // which is now kp2 -- so it may fail signature check.
        // This is expected behavior: the identity records the transfer
        // but the next evolution by kp2 would produce a valid identity.
        expect(transferred.lineage.length).toBe(2);
        expect(transferred.lineage[1]!.changeType).toBe('operator_transfer');
      }
    });
  });

  // ---- Capabilities sorting ----

  describe('Capabilities are always sorted', () => {
    it('created identity has lexicographically sorted capabilities', async () => {
      for (let i = 0; i < ITERATIONS; i++) {
        const kp = await generateKeyPair();
        const caps = [randomString(5), randomString(5), randomString(5)];
        const options = await makeIdentityOptions(kp);
        options.capabilities = caps;
        const identity = await createIdentity(options);
        const sorted = [...caps].sort();
        expect(identity.capabilities).toEqual(sorted);
      }
    });
  });
});
