/**
 * Unit tests for the payer-attribution persistence layer.
 *
 * Goals:
 *   1. priceToMicro parses common price formats correctly (no silent zeros).
 *   2. recordPaidCall is a strict no-op when KV creds are absent (no fetch fired).
 *   3. recordPaidCall fires the correct pipeline command when KV creds are present.
 *   4. 100 sequential recordPaidCall invocations all reach Upstash (defense
 *      against silent fire-and-forget failures dropping calls on the floor).
 *   5. Read APIs (getRecentPaidCalls, getPathCounts, getLifetimeTotals,
 *      readHeartbeat) are no-ops when creds are absent.
 *
 * No real network calls. We mock global.fetch and inspect the requests.
 */

describe('payer-log persistence', () => {
  const ORIG_ENV = { ...process.env };
  let fetchMock: jest.Mock;

  beforeEach(() => {
    jest.resetModules();
    process.env = { ...ORIG_ENV };
    delete process.env.KV_REST_API_URL;
    delete process.env.KV_REST_API_TOKEN;
    delete process.env.UPSTASH_REDIS_REST_URL;
    delete process.env.UPSTASH_REDIS_REST_TOKEN;

    fetchMock = jest.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ result: '0' }),
    });
    // Replace global fetch (jest-fetch-mock is wired but we want a custom mock here).
    (global as unknown as { fetch: jest.Mock }).fetch = fetchMock;
  });

  afterEach(() => {
    process.env = { ...ORIG_ENV };
  });

  test('priceToMicro parses common formats', () => {
    const { priceToMicro } = require('../app/api/_lib/payer-log');
    expect(priceToMicro('$0.01')).toBe(10000);
    expect(priceToMicro('$0.05')).toBe(50000);
    expect(priceToMicro('$1.00')).toBe(1_000_000);
    expect(priceToMicro('0.50 USDC')).toBe(500000);
    expect(priceToMicro('1.5')).toBe(1_500_000);
    expect(priceToMicro('')).toBe(0);
    expect(priceToMicro('not a price')).toBe(0);
    expect(priceToMicro('-1')).toBe(0);
  });

  test('recordPaidCall is a no-op when KV creds are absent', async () => {
    const { recordPaidCall } = require('../app/api/_lib/payer-log');
    recordPaidCall({
      ts: Date.now(),
      path: '/x402/test',
      payer: '0xabc',
      price: '$0.01',
      priceMicro: 10000,
      chain: 'base',
      status: 200,
      ms: 12,
      source: 'action',
    });
    // Give the fire-and-forget promise a chance to fire (it shouldn't).
    await new Promise((r) => setTimeout(r, 50));
    expect(fetchMock).not.toHaveBeenCalled();
  });

  test('isPersistenceConfigured reflects env state', () => {
    const mod1 = require('../app/api/_lib/payer-log');
    expect(mod1.isPersistenceConfigured()).toBe(false);

    jest.resetModules();
    process.env.KV_REST_API_URL = 'https://example.upstash.io';
    process.env.KV_REST_API_TOKEN = 'token-abc';
    const mod2 = require('../app/api/_lib/payer-log');
    expect(mod2.isPersistenceConfigured()).toBe(true);
  });

  test('recordPaidCall fires correct pipeline when KV configured', async () => {
    process.env.KV_REST_API_URL = 'https://example.upstash.io';
    process.env.KV_REST_API_TOKEN = 'token-abc';
    const { recordPaidCall } = require('../app/api/_lib/payer-log');

    recordPaidCall({
      ts: 1700000000000,
      path: '/x402/oracle/price-feed',
      payer: '0x7e571E959cC7C75Ccdd2eAC24f8775ea2eAa2F09',
      price: '$0.01',
      priceMicro: 10000,
      chain: 'base',
      status: 200,
      ms: 42,
      source: 'multinet',
    });

    await new Promise((r) => setTimeout(r, 50));

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const [url, init] = fetchMock.mock.calls[0];
    expect(url).toBe('https://example.upstash.io/pipeline');
    expect(init.method).toBe('POST');
    expect(init.headers.Authorization).toBe('Bearer token-abc');
    const body = JSON.parse(init.body);
    expect(Array.isArray(body)).toBe(true);
    // Expect: LPUSH, LTRIM, HSET, HINCRBY, INCR, INCRBY, SET (self-heartbeat)
    expect(body.length).toBe(7);
    expect(body[0][0]).toBe('LPUSH');
    expect(body[0][1]).toBe('vaultfire:payers:recent');
    expect(body[1][0]).toBe('LTRIM');
    expect(body[2][0]).toBe('HSET');
    expect(body[3][0]).toBe('HINCRBY');
    expect(body[4][0]).toBe('INCR');
    expect(body[5][0]).toBe('INCRBY');
    expect(body[5][2]).toBe('10000');
    // Self-heartbeat: every paid call also bumps the heartbeat key.
    expect(body[6][0]).toBe('SET');
    expect(body[6][1]).toBe('vaultfire:health:heartbeat');
  });

  test('100 sequential recordPaidCall calls all reach Upstash', async () => {
    process.env.KV_REST_API_URL = 'https://example.upstash.io';
    process.env.KV_REST_API_TOKEN = 'token-abc';
    const { recordPaidCall } = require('../app/api/_lib/payer-log');

    for (let i = 0; i < 100; i++) {
      recordPaidCall({
        ts: 1700000000000 + i,
        path: `/x402/test/${i}`,
        payer: `0x${i.toString(16).padStart(40, '0')}`,
        price: '$0.01',
        priceMicro: 10000,
        chain: 'base',
        status: 200,
        ms: i,
        source: 'action',
      });
    }

    // Wait for all fire-and-forget promises to settle.
    await new Promise((r) => setTimeout(r, 200));

    expect(fetchMock).toHaveBeenCalledTimes(100);
    // Spot-check: each call had a unique path in the LPUSH payload.
    const paths = new Set<string>();
    for (const call of fetchMock.mock.calls) {
      const body = JSON.parse(call[1].body);
      const lpush = body[0];
      const entry = JSON.parse(lpush[2]);
      paths.add(entry.path);
    }
    expect(paths.size).toBe(100);
  });

  test('recordPaidCall does NOT throw when fetch rejects', async () => {
    process.env.KV_REST_API_URL = 'https://example.upstash.io';
    process.env.KV_REST_API_TOKEN = 'token-abc';
    fetchMock.mockRejectedValueOnce(new Error('network down'));
    const { recordPaidCall } = require('../app/api/_lib/payer-log');

    expect(() =>
      recordPaidCall({
        ts: Date.now(),
        path: '/x402/test',
        payer: '0xabc',
        price: '$0.01',
        priceMicro: 10000,
        chain: 'base',
        status: 200,
        ms: 1,
        source: 'action',
      }),
    ).not.toThrow();

    // Allow the fire-and-forget to fail in the background.
    await new Promise((r) => setTimeout(r, 50));
    // Test passes if we didn't crash.
  });

  test('read APIs are graceful no-ops when creds absent', async () => {
    const { getRecentPaidCalls, getPathCounts, getLifetimeTotals, readHeartbeat } =
      require('../app/api/_lib/payer-log');
    expect(await getRecentPaidCalls(10)).toEqual([]);
    expect(await getPathCounts()).toEqual({});
    expect(await getLifetimeTotals()).toEqual({ calls: 0, usdcMicro: 0 });
    expect(await readHeartbeat()).toBeNull();
    expect(fetchMock).not.toHaveBeenCalled();
  });
});
