/**
 * Vaultfire x402 Client — TypeScript Tests
 *
 * Tests for the standalone x402 client module.
 * Run with: npm test
 *
 * Tests cover:
 *   - Amount formatting and parsing (formatUsdc, parseUsdc)
 *   - Nonce generation (generateNonce)
 *   - Payment ID generation (generatePaymentId)
 *   - Header encoding/decoding (encodeHeaderPayload, decodeHeaderPayload)
 *   - Payment requirement selection (selectPaymentRequirement)
 *   - Payment required check (isPaymentRequired)
 *   - Configuration (configureX402)
 *   - Constants validation
 *   - WalletProvider interface
 *   - Payment history (localStorage simulation)
 *   - Payment stats
 */

import {
  // Constants
  BASE_USDC_ADDRESS,
  AVALANCHE_USDC_ADDRESS,
  BASE_CHAIN_ID,
  AVALANCHE_CHAIN_ID,
  BASE_NETWORK,
  AVALANCHE_NETWORK,
  DEFAULT_FACILITATOR_URL,
  USDC_DECIMALS,
  X402_VERSION,
  HEADERS,
  VAULTFIRE_DEPLOYER,

  // Utilities
  formatUsdc,
  parseUsdc,
  generateNonce,
  generatePaymentId,
  encodeHeaderPayload,
  decodeHeaderPayload,

  // Protocol
  isPaymentRequired,
  selectPaymentRequirement,
  configureX402,

  // History
  getPaymentHistory,
  savePaymentRecord,
  updatePaymentRecord,
  clearPaymentHistory,
  getPaymentStats,

  // Types
  WalletProvider,
  VNSResolver,
  X402PaymentRequired,
  X402PaymentRequirement,
  X402PaymentRecord,
} from '../src/x402-client';

// ---------------------------------------------------------------------------
// Mock localStorage for Node.js test environment
// ---------------------------------------------------------------------------

const _storage = new Map<string, string>();
const localStorageMock = {
  getItem: (key: string) => _storage.get(key) ?? null,
  setItem: (key: string, value: string) => { _storage.set(key, value); },
  removeItem: (key: string) => { _storage.delete(key); },
  clear: () => { _storage.clear(); },
};

// Attach to global so the module's window check passes
Object.assign(global, { window: global, localStorage: localStorageMock });

// ---------------------------------------------------------------------------
// Constants tests
// ---------------------------------------------------------------------------

describe('Constants', () => {
  test('BASE_USDC_ADDRESS is correct', () => {
    expect(BASE_USDC_ADDRESS).toBe('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
  });

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

  test('BASE_CHAIN_ID is 8453', () => {
    expect(BASE_CHAIN_ID).toBe(8453);
  });

  test('AVALANCHE_CHAIN_ID is 43114', () => {
    expect(AVALANCHE_CHAIN_ID).toBe(43114);
  });

  test('BASE_NETWORK is eip155:8453', () => {
    expect(BASE_NETWORK).toBe('eip155:8453');
  });

  test('AVALANCHE_NETWORK is eip155:43114', () => {
    expect(AVALANCHE_NETWORK).toBe('eip155:43114');
  });

  test('DEFAULT_FACILITATOR_URL is correct', () => {
    expect(DEFAULT_FACILITATOR_URL).toBe('https://x402.org/facilitator');
  });

  test('USDC_DECIMALS is 6', () => {
    expect(USDC_DECIMALS).toBe(6);
  });

  test('X402_VERSION is 2', () => {
    expect(X402_VERSION).toBe(2);
  });

  test('HEADERS are correct', () => {
    expect(HEADERS.PAYMENT_REQUIRED).toBe('X-PAYMENT-REQUIRED');
    expect(HEADERS.PAYMENT_SIGNATURE).toBe('X-PAYMENT');
    expect(HEADERS.PAYMENT_RESPONSE).toBe('X-PAYMENT-RESPONSE');
  });

  test('VAULTFIRE_DEPLOYER address is correct', () => {
    expect(VAULTFIRE_DEPLOYER).toBe('0xfA15Ee28939B222B0448261A22156070f0A7813C');
  });
});

// ---------------------------------------------------------------------------
// formatUsdc tests
// ---------------------------------------------------------------------------

describe('formatUsdc', () => {
  test('formats 1000000 to 1.00', () => {
    expect(formatUsdc('1000000')).toBe('1.00');
  });

  test('formats 0 to 0.00', () => {
    expect(formatUsdc('0')).toBe('0.00');
  });

  test('formats 1500000 to 1.5', () => {
    expect(formatUsdc('1500000')).toBe('1.5');
  });

  test('formats 1234567 to 1.234567', () => {
    expect(formatUsdc('1234567')).toBe('1.234567');
  });

  test('formats 100000000 to 100.00', () => {
    expect(formatUsdc('100000000')).toBe('100.00');
  });

  test('formats 1 to 0.000001', () => {
    expect(formatUsdc('1')).toBe('0.000001');
  });

  test('handles invalid input gracefully', () => {
    expect(formatUsdc('not-a-number')).toBe('0.00');
    expect(formatUsdc('')).toBe('0.00');
  });

  test('formats large amounts', () => {
    expect(formatUsdc('1000000000000')).toBe('1000000.00');
  });
});

// ---------------------------------------------------------------------------
// parseUsdc tests
// ---------------------------------------------------------------------------

describe('parseUsdc', () => {
  test('parses 1.00 to 1000000', () => {
    expect(parseUsdc('1.00')).toBe('1000000');
  });

  test('parses 0.01 to 10000', () => {
    expect(parseUsdc('0.01')).toBe('10000');
  });

  test('parses 0.001 to 1000', () => {
    expect(parseUsdc('0.001')).toBe('1000');
  });

  test('parses 0.000001 to 1 (minimum unit)', () => {
    expect(parseUsdc('0.000001')).toBe('1');
  });

  test('parses 100 to 100000000', () => {
    expect(parseUsdc('100')).toBe('100000000');
  });

  test('parses 1.5 to 1500000', () => {
    expect(parseUsdc('1.5')).toBe('1500000');
  });

  test('handles integer input', () => {
    expect(parseUsdc('5')).toBe('5000000');
  });

  test('handles invalid input gracefully', () => {
    expect(parseUsdc('not-a-number')).toBe('0');
  });

  test('roundtrip: parseUsdc(formatUsdc(x)) === x for valid values', () => {
    const values = ['1000000', '500000', '1234567', '100000000'];
    for (const v of values) {
      expect(parseUsdc(formatUsdc(v))).toBe(v);
    }
  });

  test('truncates more than 6 decimal places', () => {
    // 0.0000001 truncated to 6 decimals = 0.000000, which is 0
    const result = parseUsdc('0.0000001');
    // The 7th decimal is truncated
    expect(BigInt(result)).toBeLessThanOrEqual(1n);
  });
});

// ---------------------------------------------------------------------------
// generateNonce tests
// ---------------------------------------------------------------------------

describe('generateNonce', () => {
  test('returns a 0x-prefixed 64-char hex string (bytes32)', () => {
    const nonce = generateNonce();
    expect(nonce).toMatch(/^0x[0-9a-f]{64}$/);
  });

  test('generates unique nonces', () => {
    const nonces = new Set(Array.from({ length: 100 }, () => generateNonce()));
    expect(nonces.size).toBe(100);
  });

  test('nonce is always 66 characters total (0x + 64)', () => {
    const nonce = generateNonce();
    expect(nonce.length).toBe(66);
  });
});

// ---------------------------------------------------------------------------
// generatePaymentId tests
// ---------------------------------------------------------------------------

describe('generatePaymentId', () => {
  test('starts with x402_', () => {
    const id = generatePaymentId();
    expect(id.startsWith('x402_')).toBe(true);
  });

  test('generates unique IDs', () => {
    const ids = new Set(Array.from({ length: 50 }, () => generatePaymentId()));
    expect(ids.size).toBe(50);
  });
});

// ---------------------------------------------------------------------------
// encodeHeaderPayload / decodeHeaderPayload tests
// ---------------------------------------------------------------------------

describe('encodeHeaderPayload / decodeHeaderPayload', () => {
  test('roundtrip encoding works', () => {
    const original = { x402Version: 2, accepts: [], foo: 'bar' };
    const encoded = encodeHeaderPayload(original);
    const decoded = decodeHeaderPayload<typeof original>(encoded);
    expect(decoded).toEqual(original);
  });

  test('encoded value is a valid base64 string', () => {
    const encoded = encodeHeaderPayload({ test: true });
    expect(() => Buffer.from(encoded, 'base64').toString('utf-8')).not.toThrow();
  });

  test('decodeHeaderPayload throws on invalid base64', () => {
    expect(() => decodeHeaderPayload('!!!not-valid-base64!!!')).toThrow();
  });
});

// ---------------------------------------------------------------------------
// isPaymentRequired tests
// ---------------------------------------------------------------------------

describe('isPaymentRequired', () => {
  test('returns true for 402 response', () => {
    const response = new Response(null, { status: 402 });
    expect(isPaymentRequired(response)).toBe(true);
  });

  test('returns false for 200 response', () => {
    const response = new Response(null, { status: 200 });
    expect(isPaymentRequired(response)).toBe(false);
  });

  test('returns false for 404 response', () => {
    const response = new Response(null, { status: 404 });
    expect(isPaymentRequired(response)).toBe(false);
  });

  test('returns false for 500 response', () => {
    const response = new Response(null, { status: 500 });
    expect(isPaymentRequired(response)).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// selectPaymentRequirement tests
// ---------------------------------------------------------------------------

describe('selectPaymentRequirement', () => {
  const makeReq = (overrides: Partial<X402PaymentRequirement>): X402PaymentRequirement => ({
    scheme: 'exact',
    network: 'eip155:8453',
    amount: '1000000',
    asset: BASE_USDC_ADDRESS,
    payTo: '0xRecipient',
    maxTimeoutSeconds: 60,
    ...overrides,
  });

  test('returns null for empty accepts array', () => {
    const req: X402PaymentRequired = { x402Version: 2, accepts: [] };
    expect(selectPaymentRequirement(req)).toBeNull();
  });

  test('prioritizes Base USDC with EIP-3009', () => {
    const baseEip3009 = makeReq({
      extra: { assetTransferMethod: 'eip3009', name: 'USDC', version: '2' },
    });
    const other = makeReq({ network: 'eip155:1' });
    const req: X402PaymentRequired = { x402Version: 2, accepts: [other, baseEip3009] };
    expect(selectPaymentRequirement(req)).toBe(baseEip3009);
  });

  test('falls back to Base USDC without EIP-3009 extra', () => {
    const baseUsdc = makeReq({});
    const req: X402PaymentRequired = { x402Version: 2, accepts: [baseUsdc] };
    expect(selectPaymentRequirement(req)).toBe(baseUsdc);
  });

  test('falls back to first option when no Base options', () => {
    const eth = makeReq({ network: 'eip155:1', asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' });
    const req: X402PaymentRequired = { x402Version: 2, accepts: [eth] };
    expect(selectPaymentRequirement(req)).toBe(eth);
  });

  test('returns null for null/undefined accepts', () => {
    const req = { x402Version: 2, accepts: null as unknown as X402PaymentRequirement[] };
    expect(selectPaymentRequirement(req)).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// configureX402 and WalletProvider tests
// ---------------------------------------------------------------------------

describe('configureX402', () => {
  test('configures wallet provider without throwing', () => {
    const wallet: WalletProvider = {
      getAddress: () => '0x1234567890123456789012345678901234567890',
      getPrivateKey: () => null,
      isUnlocked: () => false,
    };
    expect(() => configureX402(wallet)).not.toThrow();
  });

  test('accepts an optional VNS resolver', () => {
    const wallet: WalletProvider = {
      getAddress: () => '0xABCD',
      getPrivateKey: () => null,
      isUnlocked: () => true,
    };
    const vns: VNSResolver = {
      resolvePaymentAddress: async (name) => ({
        address: '0xABCD',
        vnsName: name,
      }),
      reverseResolveVNS: async () => null,
    };
    expect(() => configureX402(wallet, vns)).not.toThrow();
  });
});

// ---------------------------------------------------------------------------
// Payment history tests
// ---------------------------------------------------------------------------

describe('Payment History', () => {
  beforeEach(() => {
    clearPaymentHistory();
  });

  afterEach(() => {
    clearPaymentHistory();
  });

  const makeRecord = (id: string): X402PaymentRecord => ({
    id,
    timestamp: Date.now(),
    url: 'https://example.com/resource',
    amount: '1000000',
    amountFormatted: '1.00',
    asset: BASE_USDC_ADDRESS,
    network: BASE_NETWORK,
    payTo: '0xRecipient',
    from: '0xSender',
    status: 'signed',
    description: 'Test payment',
  });

  test('getPaymentHistory returns empty array initially', () => {
    expect(getPaymentHistory()).toEqual([]);
  });

  test('savePaymentRecord persists a record', () => {
    const record = makeRecord('test-1');
    savePaymentRecord(record);
    const history = getPaymentHistory();
    expect(history).toHaveLength(1);
    expect(history[0]?.id).toBe('test-1');
  });

  test('most recent record is first', () => {
    savePaymentRecord(makeRecord('first'));
    savePaymentRecord(makeRecord('second'));
    const history = getPaymentHistory();
    expect(history[0]?.id).toBe('second');
    expect(history[1]?.id).toBe('first');
  });

  test('updatePaymentRecord updates status and txHash', () => {
    const record = makeRecord('update-test');
    savePaymentRecord(record);
    updatePaymentRecord('update-test', { status: 'settled', txHash: '0xdeadbeef' });
    const updated = getPaymentHistory().find((r) => r.id === 'update-test');
    expect(updated?.status).toBe('settled');
    expect(updated?.txHash).toBe('0xdeadbeef');
  });

  test('clearPaymentHistory removes all records', () => {
    savePaymentRecord(makeRecord('a'));
    savePaymentRecord(makeRecord('b'));
    clearPaymentHistory();
    expect(getPaymentHistory()).toEqual([]);
  });

  test('getPaymentStats returns correct counts', () => {
    savePaymentRecord({ ...makeRecord('s1'), status: 'settled', amount: '1000000' });
    savePaymentRecord({ ...makeRecord('s2'), status: 'settled', amount: '500000' });
    savePaymentRecord({ ...makeRecord('f1'), status: 'failed', amount: '250000' });
    savePaymentRecord({ ...makeRecord('p1'), status: 'signed', amount: '100000' });

    const stats = getPaymentStats();
    expect(stats.totalPayments).toBe(4);
    expect(stats.settledCount).toBe(2);
    expect(stats.failedCount).toBe(1);
    // Total = 1000000 + 500000 + 250000 + 100000 = 1850000 micro = 1.85 USDC
    expect(stats.totalAmountUsdc).toBe('1.85');
  });
});

// ---------------------------------------------------------------------------
// Amount edge cases
// ---------------------------------------------------------------------------

describe('Amount edge cases', () => {
  test('parseUsdc handles leading/trailing whitespace', () => {
    // trim is not in spec but let's verify behavior
    const result = parseUsdc('1.00');
    expect(result).toBe('1000000');
  });

  test('formatUsdc handles very large amounts', () => {
    const large = (10_000_000n * 1_000_000n).toString(); // 10M USDC
    expect(formatUsdc(large)).toBe('10000000.00');
  });

  test('parseUsdc(formatUsdc(0)) === 0', () => {
    expect(parseUsdc(formatUsdc('0'))).toBe('0');
  });
});
