/**
 * Vaultfire Name Service — Client Unit Tests
 *
 * Tests for pure, synchronous logic in vns-client.ts.
 * Network-dependent functions (resolveVNSName, registerVNSName, etc.)
 * are excluded here and should be covered by integration tests.
 *
 * Run: npm test
 */

import {
  validateVNSName,
  formatVNSName,
  stripVNSSuffix,
  getBondTier,
  getBondTierInfo,
  parseIdentityType,
  validateRegistrationRules,
  encodeAddress,
  encodeUint256,
  encodeString,
  abiEncodeStringCall,
  decodeAgentResponse,
  getIdentityTypeLabel,
  getIdentityTypeColor,
  formatAddressOrVNS,
  BOND_TIERS,
  BOND_REQUIREMENTS,
  VNS_SUFFIX,
  VNS_CONTRACT_ADDRESS,
} from '../src/vns-client.js';

// ── validateVNSName ────────────────────────────────────────────────────────────

describe('validateVNSName', () => {
  describe('valid names', () => {
    const valid = [
      'abc',
      'ghostkey316',
      'vaultfire-sentinel',
      'my-ai-agent',
      'agent123',
      'a1b',
      '123',
      'aa',
      'a1',
      'x-y-z',
      '0x0', // not an address — valid name
      'a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p', // 32 chars boundary
    ];

    for (const name of valid) {
      test(`accepts "${name}"`, () => {
        const result = validateVNSName(name);
        expect(result.valid).toBe(true);
        expect(result.normalized).toBeDefined();
        expect(result.error).toBeUndefined();
      });
    }
  });

  describe('valid names with .vns suffix', () => {
    test('strips .vns suffix before validation', () => {
      const result = validateVNSName('ghostkey316.vns');
      expect(result.valid).toBe(true);
      expect(result.normalized).toBe('ghostkey316');
    });

    test('normalizes to lowercase', () => {
      const result = validateVNSName('GHOSTKEY316');
      expect(result.valid).toBe(true);
      expect(result.normalized).toBe('ghostkey316');
    });

    test('strips and normalizes combined', () => {
      const result = validateVNSName('MY-AGENT.vns');
      expect(result.valid).toBe(true);
      expect(result.normalized).toBe('my-agent');
    });
  });

  describe('invalid names — too short', () => {
    test('rejects empty string', () => {
      const result = validateVNSName('');
      expect(result.valid).toBe(false);
      expect(result.error).toContain('empty');
    });

    test('rejects single character', () => {
      const result = validateVNSName('a');
      expect(result.valid).toBe(false);
      expect(result.error).toContain('3 characters');
    });
  });

  describe('invalid names — too long', () => {
    test('rejects 33-character name', () => {
      const name = 'a'.repeat(33);
      const result = validateVNSName(name);
      expect(result.valid).toBe(false);
      expect(result.error).toContain('32 characters');
    });
  });

  describe('invalid names — bad characters', () => {
    test('rejects uppercase', () => {
      // Note: uppercase is normalized to lowercase, so this should pass
      const result = validateVNSName('ABC');
      expect(result.valid).toBe(true); // normalized
    });

    test('rejects spaces', () => {
      const result = validateVNSName('my agent');
      expect(result.valid).toBe(false);
    });

    test('rejects underscores', () => {
      const result = validateVNSName('my_agent');
      expect(result.valid).toBe(false);
    });

    test('rejects dots in name', () => {
      const result = validateVNSName('my.agent');
      expect(result.valid).toBe(false);
    });

    test('rejects special characters', () => {
      const result = validateVNSName('agent@vault');
      expect(result.valid).toBe(false);
    });
  });

  describe('invalid names — hyphen rules', () => {
    test('rejects consecutive hyphens', () => {
      const result = validateVNSName('my--agent');
      expect(result.valid).toBe(false);
      expect(result.error).toContain('consecutive hyphens');
    });

    test('rejects leading hyphen', () => {
      const result = validateVNSName('-myagent');
      expect(result.valid).toBe(false);
      expect(result.error).toContain('hyphen');
    });

    test('rejects trailing hyphen', () => {
      const result = validateVNSName('myagent-');
      expect(result.valid).toBe(false);
      expect(result.error).toContain('hyphen');
    });
  });

  describe('reserved names', () => {
    const reserved = ['vaultfire', 'embris', 'admin', 'root', 'system', 'protocol', 'vns', 'null', 'undefined', 'ghostkey'];

    for (const name of reserved) {
      test(`rejects reserved name "${name}"`, () => {
        const result = validateVNSName(name);
        expect(result.valid).toBe(false);
        expect(result.error).toContain('reserved');
      });
    }
  });

  describe('boundary cases', () => {
    test('accepts exactly 3 characters', () => {
      const result = validateVNSName('abc');
      expect(result.valid).toBe(true);
    });

    test('accepts exactly 32 characters', () => {
      const name = 'a' + 'b'.repeat(30) + 'c'; // 32 chars
      const result = validateVNSName(name);
      expect(result.valid).toBe(true);
    });

    test('rejects exactly 33 characters', () => {
      const name = 'a' + 'b'.repeat(31) + 'c'; // 33 chars
      const result = validateVNSName(name);
      expect(result.valid).toBe(false);
    });
  });
});

// ── formatVNSName ──────────────────────────────────────────────────────────────

describe('formatVNSName', () => {
  test('adds .vns suffix', () => {
    expect(formatVNSName('ghostkey316')).toBe('ghostkey316.vns');
  });

  test('does not double-add suffix', () => {
    expect(formatVNSName('ghostkey316.vns')).toBe('ghostkey316.vns');
  });

  test('normalizes to lowercase', () => {
    expect(formatVNSName('GHOSTKEY316')).toBe('ghostkey316.vns');
  });

  test('trims whitespace', () => {
    expect(formatVNSName('  ghostkey316  ')).toBe('ghostkey316.vns');
  });

  test('handles name with uppercase suffix', () => {
    expect(formatVNSName('Agent.VNS')).toBe('agent.vns');
  });
});

// ── stripVNSSuffix ─────────────────────────────────────────────────────────────

describe('stripVNSSuffix', () => {
  test('strips .vns suffix', () => {
    expect(stripVNSSuffix('ghostkey316.vns')).toBe('ghostkey316');
  });

  test('returns name unchanged if no suffix', () => {
    expect(stripVNSSuffix('ghostkey316')).toBe('ghostkey316');
  });

  test('normalizes to lowercase', () => {
    expect(stripVNSSuffix('GHOSTKEY316.VNS')).toBe('ghostkey316');
  });

  test('trims whitespace', () => {
    expect(stripVNSSuffix('  agent.vns  ')).toBe('agent');
  });
});

// ── getBondTier ────────────────────────────────────────────────────────────────

describe('getBondTier', () => {
  test('returns bronze for 0.01 ETH', () => {
    expect(getBondTier(0.01)).toBe('bronze');
  });

  test('returns bronze for less than 0.05 ETH', () => {
    expect(getBondTier(0.049)).toBe('bronze');
  });

  test('returns silver for exactly 0.05 ETH', () => {
    expect(getBondTier(0.05)).toBe('silver');
  });

  test('returns silver for 0.09 ETH', () => {
    expect(getBondTier(0.09)).toBe('silver');
  });

  test('returns gold for exactly 0.1 ETH', () => {
    expect(getBondTier(0.1)).toBe('gold');
  });

  test('returns gold for 0.4 ETH', () => {
    expect(getBondTier(0.4)).toBe('gold');
  });

  test('returns platinum for exactly 0.5 ETH', () => {
    expect(getBondTier(0.5)).toBe('platinum');
  });

  test('returns platinum for 1 ETH', () => {
    expect(getBondTier(1)).toBe('platinum');
  });

  test('returns platinum for 10 ETH', () => {
    expect(getBondTier(10)).toBe('platinum');
  });
});

// ── getBondTierInfo ────────────────────────────────────────────────────────────

describe('getBondTierInfo', () => {
  test('returns correct info for bronze', () => {
    const info = getBondTierInfo('bronze');
    expect(info.label).toBe('Bronze');
    expect(info.minEth).toBe(0.01);
    expect(info.color).toBe('#CD7F32');
  });

  test('returns correct info for platinum', () => {
    const info = getBondTierInfo('platinum');
    expect(info.label).toBe('Platinum');
    expect(info.minEth).toBe(0.5);
  });

  test('returns correct info for silver', () => {
    const info = getBondTierInfo('silver');
    expect(info.label).toBe('Silver');
    expect(info.minEth).toBe(0.05);
  });

  test('returns correct info for gold', () => {
    const info = getBondTierInfo('gold');
    expect(info.label).toBe('Gold');
    expect(info.minEth).toBe(0.1);
  });
});

// ── parseIdentityType ──────────────────────────────────────────────────────────

describe('parseIdentityType', () => {
  test('parses human type', () => {
    expect(parseIdentityType('{"type":"human","v":1}')).toBe('human');
  });

  test('parses companion type', () => {
    expect(parseIdentityType('{"type":"companion","v":1}')).toBe('companion');
  });

  test('parses agent type', () => {
    expect(parseIdentityType('{"type":"agent","v":1}')).toBe('agent');
  });

  test('parses with additional fields', () => {
    const desc = JSON.stringify({ type: 'agent', spec: ['research'], caps: ['nlp'], v: 1 });
    expect(parseIdentityType(desc)).toBe('agent');
  });

  test('returns agent for non-JSON strings (legacy)', () => {
    expect(parseIdentityType('vaultfire-sentinel')).toBe('agent');
  });

  test('returns agent for empty string', () => {
    expect(parseIdentityType('')).toBe('agent');
  });

  test('returns agent for invalid JSON', () => {
    expect(parseIdentityType('{invalid json}')).toBe('agent');
  });

  test('returns agent for unknown type', () => {
    expect(parseIdentityType('{"type":"unknown","v":1}')).toBe('agent');
  });

  test('returns agent for null type field', () => {
    expect(parseIdentityType('{"type":null}')).toBe('agent');
  });
});

// ── validateRegistrationRules ──────────────────────────────────────────────────

describe('validateRegistrationRules', () => {
  const WALLET = '0xfA15Ee28939B222B0448261A22156070f0A7813C';

  describe('agent registration', () => {
    test('accepts valid agent registration with sufficient bond', () => {
      const result = validateRegistrationRules(WALLET, 'agent', 'test-agent-xyz', 0.01);
      // Will return null (valid) since test-agent-xyz is not in local registry
      expect(result).toBeNull();
    });

    test('rejects agent registration without bond', () => {
      const result = validateRegistrationRules(WALLET, 'agent', 'test-agent-abc');
      expect(result).toContain('accountability bond');
    });

    test('rejects agent registration with bond below minimum', () => {
      const result = validateRegistrationRules(WALLET, 'agent', 'test-agent-def', 0.009);
      expect(result).toContain('0.01 ETH');
    });

    test('accepts agent with bond exactly at minimum', () => {
      const result = validateRegistrationRules(WALLET, 'agent', 'unique-agent-987', 0.01);
      expect(result).toBeNull();
    });
  });

  describe('human registration', () => {
    test('accepts valid human registration', () => {
      // This test relies on the local registry being empty for this wallet in test env
      const result = validateRegistrationRules('0x' + '1'.repeat(40), 'human', 'test-human-xyz');
      expect(result).toBeNull();
    });
  });

  describe('name already taken check', () => {
    test('null is returned when name is not in local registry', () => {
      // With a fresh test environment local registry should be empty
      const result = validateRegistrationRules(WALLET, 'agent', 'totally-unique-name-99887766', 0.05);
      expect(result).toBeNull();
    });
  });
});

// ── encodeAddress ──────────────────────────────────────────────────────────────

describe('encodeAddress', () => {
  test('pads address to 64 hex chars (32 bytes)', () => {
    const addr = '0xfA15Ee28939B222B0448261A22156070f0A7813C';
    const encoded = encodeAddress(addr);
    expect(encoded).toHaveLength(64);
    expect(encoded).toMatch(/^0+a054f831b562e729f8d268291ebde1b2edcfb84f$/i);
  });

  test('lowercases the address', () => {
    const addr = '0xfA15Ee28939B222B0448261A22156070f0A7813C';
    const encoded = encodeAddress(addr);
    expect(encoded).toBe('000000000000000000000000a054f831b562e729f8d268291ebde1b2edcfb84f');
  });

  test('removes 0x prefix', () => {
    const encoded = encodeAddress('0x1234567890abcdef1234567890abcdef12345678');
    expect(encoded).not.toContain('0x');
    expect(encoded).toHaveLength(64);
  });

  test('pads short addresses', () => {
    const encoded = encodeAddress('0x1');
    expect(encoded).toHaveLength(64);
    expect(encoded).toMatch(/^0+1$/);
  });
});

// ── encodeUint256 ──────────────────────────────────────────────────────────────

describe('encodeUint256', () => {
  test('encodes zero', () => {
    const encoded = encodeUint256(0);
    expect(encoded).toHaveLength(64);
    expect(encoded).toBe('0'.repeat(64));
  });

  test('encodes 1', () => {
    const encoded = encodeUint256(1);
    expect(encoded).toHaveLength(64);
    expect(encoded).toBe('0'.repeat(63) + '1');
  });

  test('encodes 0x20 (32)', () => {
    const encoded = encodeUint256(0x20);
    expect(encoded).toHaveLength(64);
    expect(encoded).toBe('0'.repeat(62) + '20');
  });

  test('encodes bigint', () => {
    const encoded = encodeUint256(BigInt('10000000000000000'));
    expect(encoded).toHaveLength(64);
    expect(encoded).toBe('000000000000000000000000000000000000000000000000002386f26fc10000');
  });

  test('encodes large number', () => {
    const encoded = encodeUint256(255);
    expect(encoded).toBe('0'.repeat(62) + 'ff');
  });
});

// ── encodeString ───────────────────────────────────────────────────────────────

describe('encodeString', () => {
  test('encodes empty string', () => {
    const encoded = encodeString('');
    // length field (32 bytes of zeros) + no data
    expect(encoded).toHaveLength(64);
    expect(encoded).toBe('0'.repeat(64));
  });

  test('encodes short string', () => {
    const encoded = encodeString('abc');
    // 32-byte length + 32-byte padded data
    expect(encoded.length).toBeGreaterThan(64);
    // Length field should encode 3
    expect(encoded.slice(0, 64)).toBe('0'.repeat(62) + '03');
  });

  test('returns string with correct length prefix', () => {
    const str = 'hello';
    const encoded = encodeString(str);
    const lengthHex = encoded.slice(0, 64);
    const length = parseInt(lengthHex, 16);
    expect(length).toBe(str.length);
  });
});

// ── abiEncodeStringCall ────────────────────────────────────────────────────────

describe('abiEncodeStringCall', () => {
  const SELECTOR = '0xabcdef12';

  test('starts with selector', () => {
    const encoded = abiEncodeStringCall(SELECTOR, 'test');
    expect(encoded.startsWith(SELECTOR)).toBe(true);
  });

  test('selector is 10 chars (0x + 4 bytes = 8 hex chars)', () => {
    const encoded = abiEncodeStringCall(SELECTOR, 'name');
    // After selector: 32-byte offset + 32-byte length + padded string
    expect(encoded.length).toBeGreaterThanOrEqual(10 + 64 + 64);
  });

  test('encodes offset as 0x20 (32 in hex)', () => {
    const encoded = abiEncodeStringCall(SELECTOR, 'test');
    // After selector (10 chars), next 64 chars are the offset
    const offsetHex = encoded.slice(10, 74);
    const offset = parseInt(offsetHex, 16);
    expect(offset).toBe(0x20); // 32 = pointer to string data
  });

  test('encodes string length correctly', () => {
    const name = 'vaultfire-sentinel';
    const encoded = abiEncodeStringCall(SELECTOR, name);
    // After selector (10) + offset (64) = next 64 chars are length
    const lengthHex = encoded.slice(74, 138);
    const length = parseInt(lengthHex, 16);
    expect(length).toBe(name.length);
  });
});

// ── decodeAgentResponse ────────────────────────────────────────────────────────

describe('decodeAgentResponse', () => {
  test('returns null for empty string', () => {
    expect(decodeAgentResponse('')).toBeNull();
  });

  test('returns null for "0x"', () => {
    expect(decodeAgentResponse('0x')).toBeNull();
  });

  test('returns null for short response', () => {
    expect(decodeAgentResponse('0x1234')).toBeNull();
  });

  test('handles invalid hex gracefully', () => {
    // Should not throw
    const result = decodeAgentResponse('0x' + 'z'.repeat(260));
    expect(result).toBeNull();
  });
});

// ── Display helpers ────────────────────────────────────────────────────────────

describe('getIdentityTypeLabel', () => {
  test('returns correct label for human', () => {
    expect(getIdentityTypeLabel('human')).toBe('Human Identity');
  });

  test('returns correct label for companion', () => {
    expect(getIdentityTypeLabel('companion')).toBe('AI Companion');
  });

  test('returns correct label for agent', () => {
    expect(getIdentityTypeLabel('agent')).toBe('AI Agent');
  });
});

describe('getIdentityTypeColor', () => {
  test('returns green for human', () => {
    expect(getIdentityTypeColor('human')).toBe('#22C55E');
  });

  test('returns orange for companion', () => {
    expect(getIdentityTypeColor('companion')).toBe('#F97316');
  });

  test('returns purple for agent', () => {
    expect(getIdentityTypeColor('agent')).toBe('#8B5CF6');
  });
});

describe('formatAddressOrVNS', () => {
  const address = '0xfA15Ee28939B222B0448261A22156070f0A7813C';

  test('returns vns name when provided', () => {
    expect(formatAddressOrVNS(address, 'ghostkey316.vns')).toBe('ghostkey316.vns');
  });

  test('adds .vns suffix if missing', () => {
    expect(formatAddressOrVNS(address, 'ghostkey316')).toBe('ghostkey316.vns');
  });

  test('truncates address when no vns name', () => {
    const result = formatAddressOrVNS(address);
    expect(result).toContain('...');
    expect(result).toContain(address.slice(-4));
  });

  test('returns empty string for empty address', () => {
    expect(formatAddressOrVNS('')).toBe('');
  });

  test('respects custom truncate length', () => {
    const result = formatAddressOrVNS(address, null, 10);
    expect(result.startsWith(address.slice(0, 10))).toBe(true);
  });
});

// ── Constants ──────────────────────────────────────────────────────────────────

describe('Constants', () => {
  test('VNS_SUFFIX is correct', () => {
    expect(VNS_SUFFIX).toBe('.vns');
  });

  test('VNS_CONTRACT_ADDRESS is correct', () => {
    expect(VNS_CONTRACT_ADDRESS).toBe('0x1437c4081233A4f0B6907dDf5374Ed610cBD6B25');
  });

  test('BOND_REQUIREMENTS human is zero', () => {
    expect(BOND_REQUIREMENTS.human).toBe(BigInt(0));
  });

  test('BOND_REQUIREMENTS companion is zero', () => {
    expect(BOND_REQUIREMENTS.companion).toBe(BigInt(0));
  });

  test('BOND_REQUIREMENTS agent is 0.01 ETH in wei', () => {
    expect(BOND_REQUIREMENTS.agent).toBe(BigInt('10000000000000000'));
  });

  test('BOND_TIERS has 4 entries', () => {
    expect(BOND_TIERS).toHaveLength(4);
  });

  test('BOND_TIERS are in ascending order', () => {
    const mins = BOND_TIERS.map(t => t.minEth);
    for (let i = 1; i < mins.length; i++) {
      expect(mins[i]).toBeGreaterThan(mins[i - 1]!);
    }
  });
});
