import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  makeEmptyRingBuffer,
  parseRingBuffer,
  serializeRingBuffer,
  checkBlockHistorySufficient,
  updateHourlyData,
  ensureStatsExists,
  recordEthReserveSnapshot,
  updateEthReserve,
  markPositionsUpdated,
  refreshOutstandingStake,
  refreshMinStake,
  RING_BUFFER_SEGMENTS,
  MINIMUM_BLOCKS_FOR_RINGBUFFER,
  type StatsContext,
} from '../src/helpers/stats.js';

// Constants duplicated from the mock so tests don't re-import ponder:schema
const HOURS = 168;
const SECS_PER_HOUR = 3600;

// ---------------------------------------------------------------------------
// Helper factories
// ---------------------------------------------------------------------------

interface MockStatsRow {
  ringBuffer: string[];
  ringBufferPointer: number;
  lastHourlyUpdateTimestamp: bigint;
  holderCount: number;
  minStake?: bigint;
  stakeTotalSupply?: bigint;
  outstandingStake?: bigint;
  kraikenTotalSupply?: bigint;
}

function emptyStatsRow(overrides: Partial<MockStatsRow> = {}): MockStatsRow {
  return {
    ringBuffer: makeEmptyRingBuffer().map(String),
    ringBufferPointer: 0,
    lastHourlyUpdateTimestamp: 0n,
    holderCount: 0,
    minStake: 0n,
    stakeTotalSupply: 0n,
    outstandingStake: 0n,
    kraikenTotalSupply: 0n,
    ...overrides,
  };
}

/** Build a ring buffer string[] with a known value at a specific slot. */
function ringBufferWith(
  pointer: number,
  slotOffset: number,
  value: bigint,
  hoursBack = 0,
): string[] {
  const buf = makeEmptyRingBuffer();
  const idx = ((pointer - hoursBack + HOURS) % HOURS) * RING_BUFFER_SEGMENTS + slotOffset;
  buf[idx] = value;
  return buf.map(String);
}

function createSetMock() {
  return vi.fn().mockResolvedValue(undefined);
}

function createMockContext(statsRow: MockStatsRow | null = null): StatsContext {
  const setFn = createSetMock();
  const ctx = {
    db: {
      find: vi.fn().mockResolvedValue(statsRow),
      update: vi.fn().mockReturnValue({ set: setFn }),
      insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
    },
    client: {
      readContract: vi.fn().mockResolvedValue(0n),
    },
    contracts: {
      Kraiken: { abi: [], address: '0x0000000000000000000000000000000000000001' as `0x${string}` },
      Stake: { abi: [], address: '0x0000000000000000000000000000000000000002' as `0x${string}` },
    },
    network: {
      contracts: {
        Kraiken: { abi: [], address: '0x0000000000000000000000000000000000000001' },
      },
    },
    logger: {
      warn: vi.fn(),
      info: vi.fn(),
      error: vi.fn(),
    },
  };
  // Cast: vitest uses esbuild (no type-check), so the slim mock satisfies the runtime contract.
  return ctx as unknown as StatsContext;
}

// ---------------------------------------------------------------------------
// makeEmptyRingBuffer
// ---------------------------------------------------------------------------

describe('makeEmptyRingBuffer', () => {
  it('returns an array of the correct length', () => {
    const buf = makeEmptyRingBuffer();
    expect(buf).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
  });

  it('fills every element with 0n', () => {
    const buf = makeEmptyRingBuffer();
    expect(buf.every(v => v === 0n)).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// parseRingBuffer
// ---------------------------------------------------------------------------

describe('parseRingBuffer', () => {
  it('converts a string array to bigint array', () => {
    const raw = ['1', '2', '3'];
    expect(parseRingBuffer(raw)).toEqual([1n, 2n, 3n]);
  });

  it('returns empty ring buffer for null input', () => {
    expect(parseRingBuffer(null)).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
    expect(parseRingBuffer(null).every(v => v === 0n)).toBe(true);
  });

  it('returns empty ring buffer for undefined input', () => {
    expect(parseRingBuffer(undefined)).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
  });

  it('returns empty ring buffer for empty array input', () => {
    expect(parseRingBuffer([])).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
  });
});

// ---------------------------------------------------------------------------
// serializeRingBuffer
// ---------------------------------------------------------------------------

describe('serializeRingBuffer', () => {
  it('converts bigint array to string array', () => {
    expect(serializeRingBuffer([0n, 1n, 1000n])).toEqual(['0', '1', '1000']);
  });

  it('round-trips through parseRingBuffer', () => {
    const original = makeEmptyRingBuffer();
    original[0] = 42n;
    original[RING_BUFFER_SEGMENTS] = 99n;
    const serialized = serializeRingBuffer(original);
    expect(parseRingBuffer(serialized)).toEqual(original);
  });
});

// ---------------------------------------------------------------------------
// checkBlockHistorySufficient
// ---------------------------------------------------------------------------

describe('checkBlockHistorySufficient', () => {
  it('returns false when context has no network contracts', () => {
    const ctx = { network: {} } as unknown as StatsContext;
    const event = { block: { number: 200n } } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
    expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
  });

  it('returns false when context is missing network entirely', () => {
    const ctx = {} as unknown as StatsContext;
    const event = { block: { number: 200n } } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
    expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
  });

  it('returns false when not enough blocks have passed', () => {
    const ctx = createMockContext();
    const event = {
      block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER - 1) },
    } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
    expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
  });

  it('uses console fallback when context has no logger', () => {
    // Context with network.contracts.Kraiken but no logger — exercises logger.ts fallback branch
    const ctx = {
      network: { contracts: { Kraiken: {} } },
    } as unknown as StatsContext;
    const event = {
      block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER - 1) },
    } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
    // Should not throw; console.warn is used as fallback
    expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
  });

  it('returns true when sufficient blocks have passed', () => {
    const ctx = createMockContext();
    const event = {
      block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER + 1) },
    } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
    expect(checkBlockHistorySufficient(ctx, event)).toBe(true);
  });

  it('returns true at exact threshold', () => {
    const ctx = createMockContext();
    const event = {
      block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER) },
    } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
    expect(checkBlockHistorySufficient(ctx, event)).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// updateHourlyData
// ---------------------------------------------------------------------------

describe('updateHourlyData', () => {
  it('returns early when no stats row exists', async () => {
    const ctx = createMockContext(null);
    await updateHourlyData(ctx, BigInt(SECS_PER_HOUR * 10));
    expect((ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } }).db.update).not.toHaveBeenCalled();
  });

  it('initialises lastHourlyUpdateTimestamp when it is 0n', async () => {
    const row = emptyStatsRow({ lastHourlyUpdateTimestamp: 0n });
    const ctx = createMockContext(row);
    const timestamp = BigInt(SECS_PER_HOUR * 5);
    await updateHourlyData(ctx, timestamp);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    // Should store the current hour
    expect(setArg.lastHourlyUpdateTimestamp).toBe((timestamp / BigInt(SECS_PER_HOUR)) * BigInt(SECS_PER_HOUR));
  });

  it('advances the pointer when a new hour has started', async () => {
    const baseTimestamp = BigInt(SECS_PER_HOUR * 100);
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: 0,
    });
    const ctx = createMockContext(row);
    // Move 3 hours forward
    const newTimestamp = baseTimestamp + BigInt(SECS_PER_HOUR * 3 + 60);
    await updateHourlyData(ctx, newTimestamp);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.ringBufferPointer).toBe(3);
  });

  it('clamps hours elapsed to HOURS_IN_RING_BUFFER to prevent full-buffer clear loops', async () => {
    const baseTimestamp = BigInt(SECS_PER_HOUR * 100);
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: 0,
    });
    const ctx = createMockContext(row);
    // Jump 500 hours — exceeds ring buffer
    const newTimestamp = baseTimestamp + BigInt(SECS_PER_HOUR * 500);
    await updateHourlyData(ctx, newTimestamp);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    // pointer should wrap around within HOURS range
    expect(setArg.ringBufferPointer).toBeGreaterThanOrEqual(0);
    expect(setArg.ringBufferPointer).toBeLessThan(HOURS);
  });

  it('computes metrics when hour has advanced (mintedDay, burnedDay, etc.)', async () => {
    const pointer = 5;
    // Slot 1 (minted) at index i=0 (current hour) through i=23 — 10n in each of last 48 hours
    const buf = makeEmptyRingBuffer();
    for (let h = 0; h < 48; h++) {
      const idx = ((pointer - h + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
      buf[idx + 1] = 10n; // minted
      buf[idx + 2] = 5n; // burned
    }
    const baseTimestamp = BigInt(SECS_PER_HOUR * 200);
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: pointer,
      ringBuffer: buf.map(String),
    });
    const ctx = createMockContext(row);

    await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    // After advancing 2 hours (new pointer=7), slots 6+7 are cleared (were already 0).
    // Filled slots: i=2..49 from pointer=7 (48 slots × 10n minted / 5n burned).
    // mintedLastDay: i<24 → i=2..23 = 22 slots × 10n = 220n
    // mintedLastWeek: 48 × 10n = 480n
    // mintNextHourProjected = mintedWeek / 168n = 480n / 168n = 2n (BigInt truncation)
    // burnNextHourProjected = burnedWeek / 168n = 240n / 168n = 1n
    expect(setArg.mintedLastWeek).toBe(480n);
    expect(setArg.mintedLastDay).toBe(220n);
    expect(setArg.burnedLastWeek).toBe(240n);
    expect(setArg.burnedLastDay).toBe(110n);
    expect(setArg.mintNextHourProjected).toBe(2n);
    expect(setArg.burnNextHourProjected).toBe(1n);
    expect(setArg.netSupplyChangeDay).toBe(110n);
    expect(setArg.netSupplyChangeWeek).toBe(240n);
  });

  it('carries forward holderCount into new ring buffer slots', async () => {
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: BigInt(SECS_PER_HOUR * 10),
      ringBufferPointer: 0,
      holderCount: 42,
    });
    const ctx = createMockContext(row);
    await updateHourlyData(ctx, BigInt(SECS_PER_HOUR * 12));

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArgs = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    const updatedBuf = parseRingBuffer(setArgs.ringBuffer as string[]);
    // pointer advanced to 2; slot 3 of new pointer should be 42n
    const newPointer = 2;
    expect(updatedBuf[newPointer * RING_BUFFER_SEGMENTS + 3]).toBe(42n);
  });

  it('computes projections when same hour (no advancement)', async () => {
    const pointer = 3;
    const baseTimestamp = BigInt(SECS_PER_HOUR * 100);
    // Add some minted data in the current and previous slot
    const buf = makeEmptyRingBuffer();
    buf[pointer * RING_BUFFER_SEGMENTS + 1] = 100n; // current hour minted
    const prevPointer = ((pointer - 1 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
    buf[prevPointer + 1] = 80n; // previous hour minted
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: pointer,
      ringBuffer: buf.map(String),
    });
    const ctx = createMockContext(row);

    // Same hour but 30 minutes in
    const sameHourTimestamp = baseTimestamp + BigInt(SECS_PER_HOUR / 2);
    await updateHourlyData(ctx, sameHourTimestamp);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    // elapsed = 1800s (30 min); current minted = 100n, prev minted = 80n
    // projectedTotal = (100n * 3600n) / 1800n = 200n
    // medium = (80n + 200n) / 2n = 140n → mintProjection = 140n
    // burn: current=0n, prev=0n → medium=0n → fallback: mintedWeek/7 = 0n
    expect(setArg.mintNextHourProjected).toBe(140n);
    expect(setArg.burnNextHourProjected).toBe(0n);
  });

  it('handles zero elapsedSeconds in projection (exact hour boundary)', async () => {
    const pointer = 0;
    const exactHour = BigInt(SECS_PER_HOUR * 50);
    const buf = makeEmptyRingBuffer();
    buf[pointer * RING_BUFFER_SEGMENTS + 1] = 50n; // some minted
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: exactHour,
      ringBufferPointer: pointer,
      ringBuffer: buf.map(String),
    });
    const ctx = createMockContext(row);

    // Pass exactly the same timestamp (elapsedSeconds = 0)
    await updateHourlyData(ctx, exactHour);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
  });

  it('computes ETH reserve change metrics when reserves are populated', async () => {
    const pointer = 25;
    const buf = makeEmptyRingBuffer();
    // Set current hour ETH reserve (i=0)
    buf[pointer * RING_BUFFER_SEGMENTS + 0] = 2000n;
    // Set 24h ago ETH reserve (i=23)
    const idx24h = ((pointer - 23 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
    buf[idx24h + 0] = 1000n;
    const baseTimestamp = BigInt(SECS_PER_HOUR * 200);
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: pointer,
      ringBuffer: buf.map(String),
    });
    const ctx = createMockContext(row);
    await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.ethReserveLastDay).toBeGreaterThanOrEqual(0n);
  });
});

// ---------------------------------------------------------------------------
// recordEthReserveSnapshot
// ---------------------------------------------------------------------------

describe('recordEthReserveSnapshot', () => {
  it('returns early when no stats row exists', async () => {
    const ctx = createMockContext(null);
    await recordEthReserveSnapshot(ctx, 500n);
    expect((ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } }).db.update).not.toHaveBeenCalled();
  });

  it('writes the ETH balance into slot 0 of the current pointer', async () => {
    const pointer = 2;
    const row = emptyStatsRow({ ringBufferPointer: pointer });
    const ctx = createMockContext(row);
    const ethBalance = 12345678n;

    await recordEthReserveSnapshot(ctx, ethBalance);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    const updatedBuf = parseRingBuffer(setArg.ringBuffer as string[]);
    expect(updatedBuf[pointer * RING_BUFFER_SEGMENTS + 0]).toBe(ethBalance);
  });
});

// ---------------------------------------------------------------------------
// updateEthReserve
// ---------------------------------------------------------------------------

describe('updateEthReserve', () => {
  it('skips update when weth balance is 0', async () => {
    const ctx = createMockContext(emptyStatsRow());
    // readContract returns 0n by default
    await updateEthReserve(ctx, '0xpool0000000000000000000000000000000000001' as `0x${string}`);
    expect((ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } }).db.update).not.toHaveBeenCalled();
  });

  it('updates lastEthReserve when weth balance is non-zero', async () => {
    const ctx = createMockContext(emptyStatsRow());
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockResolvedValue(9999n);

    await updateEthReserve(ctx, '0xpool0000000000000000000000000000000000001' as `0x${string}`);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.lastEthReserve).toBe(9999n);
  });

  it('logs warning and returns when readContract throws', async () => {
    const ctx = createMockContext(emptyStatsRow());
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockRejectedValue(new Error('rpc error'));

    await updateEthReserve(ctx, '0xpool0000000000000000000000000000000000001' as `0x${string}`);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// ensureStatsExists
// ---------------------------------------------------------------------------

describe('ensureStatsExists', () => {
  it('returns existing stats without re-creating when row exists', async () => {
    const row = emptyStatsRow();
    const ctx = createMockContext(row);

    const result = await ensureStatsExists(ctx, 1000n);

    const dbMock = ctx as unknown as { db: { insert: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.insert).not.toHaveBeenCalled();
    expect(result).toBe(row);
  });

  it('creates a new stats row when none exists', async () => {
    // First call returns null, second call (post-insert) returns the row
    const row = emptyStatsRow();
    const dbFindMock = vi.fn()
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce(row);
    const ctx = createMockContext(null);
    (ctx as unknown as { db: { find: ReturnType<typeof vi.fn> } }).db.find = dbFindMock;

    const result = await ensureStatsExists(ctx, 1000n);

    const dbMock = ctx as unknown as { db: { insert: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.insert).toHaveBeenCalled();
    expect(result).toBe(row);
  });

  it('uses fallback values when readContract throws during creation', async () => {
    const row = emptyStatsRow();
    const dbFindMock = vi.fn()
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce(row);
    const ctx = createMockContext(null);
    (ctx as unknown as { db: { find: ReturnType<typeof vi.fn> } }).db.find = dbFindMock;
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockRejectedValue(new Error('rpc error'));

    const result = await ensureStatsExists(ctx, 1000n);

    expect(result).toBe(row);
    // insert should still have been called with fallback 0n values
    const dbMock = ctx as unknown as { db: { insert: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.insert).toHaveBeenCalled();
  });

  it('works without timestamp argument', async () => {
    const row = emptyStatsRow();
    const ctx = createMockContext(row);
    const result = await ensureStatsExists(ctx);
    expect(result).toBe(row);
  });
});

// ---------------------------------------------------------------------------
// markPositionsUpdated
// ---------------------------------------------------------------------------

describe('markPositionsUpdated', () => {
  it('updates positionsUpdatedAt in the stats row', async () => {
    const row = emptyStatsRow();
    const ctx = createMockContext(row);
    const ts = 7777n;

    await markPositionsUpdated(ctx, ts);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.positionsUpdatedAt).toBe(ts);
  });
});

// ---------------------------------------------------------------------------
// getStakeTotalSupply
// ---------------------------------------------------------------------------

describe('getStakeTotalSupply', () => {
  beforeEach(() => {
    vi.resetModules();
  });

  it('reads totalSupply from contract when cache is cold', async () => {
    // Fresh import to reset module-level cache
    const { getStakeTotalSupply: freshGet } = await import('../src/helpers/stats.js');
    const row = emptyStatsRow({ stakeTotalSupply: 0n });
    const ctx = createMockContext(row);
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockResolvedValue(555n);

    const result = await freshGet(ctx);

    expect(result).toBe(555n);
  });

  it('returns cached value without calling readContract on second call', async () => {
    // Fresh import to get a clean module with null cache
    const { getStakeTotalSupply: freshGet } = await import('../src/helpers/stats.js');
    const row = emptyStatsRow({ stakeTotalSupply: 0n });
    const readContractMock = vi.fn().mockResolvedValue(777n);
    const ctx = createMockContext(row);
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = readContractMock;

    // First call — populates cache via readContract
    const first = await freshGet(ctx);
    expect(first).toBe(777n);

    // Second call — should return cached value, readContract not called again
    const second = await freshGet(ctx);
    expect(second).toBe(777n);
    expect(readContractMock).toHaveBeenCalledTimes(1);
  });
});

// ---------------------------------------------------------------------------
// refreshOutstandingStake
// ---------------------------------------------------------------------------

describe('refreshOutstandingStake', () => {
  it('reads outstandingStake from contract and persists it', async () => {
    const row = emptyStatsRow();
    const ctx = createMockContext(row);
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockResolvedValue(1234n);

    await refreshOutstandingStake(ctx);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.outstandingStake).toBe(1234n);
  });
});

// ---------------------------------------------------------------------------
// refreshMinStake
// ---------------------------------------------------------------------------

describe('refreshMinStake', () => {
  it('skips update when minStake is unchanged', async () => {
    const row = emptyStatsRow({ minStake: 500n });
    const ctx = createMockContext(row);
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockResolvedValue(500n);

    await refreshMinStake(ctx, row as unknown as Awaited<ReturnType<typeof ensureStatsExists>>);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).not.toHaveBeenCalled();
  });

  it('updates minStake when the on-chain value has changed', async () => {
    const row = emptyStatsRow({ minStake: 100n });
    const ctx = createMockContext(row);
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockResolvedValue(200n);

    await refreshMinStake(ctx, row as unknown as Awaited<ReturnType<typeof ensureStatsExists>>);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.minStake).toBe(200n);
  });

  it('fetches stats when no statsData arg is provided and row exists', async () => {
    const row = emptyStatsRow({ minStake: 0n });
    const ctx = createMockContext(row);
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockResolvedValue(300n);

    await refreshMinStake(ctx);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
  });

  it('creates stats row when find returns null', async () => {
    const row = emptyStatsRow({ minStake: 0n });
    const dbFindMock = vi.fn()
      .mockResolvedValueOnce(null)  // first find (refresh check)
      .mockResolvedValueOnce(null)  // ensureStatsExists find
      .mockResolvedValueOnce(row);  // ensureStatsExists post-insert
    const ctx = createMockContext(null);
    (ctx as unknown as { db: { find: ReturnType<typeof vi.fn> } }).db.find = dbFindMock;
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockResolvedValue(300n);

    await refreshMinStake(ctx);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
  });

  it('logs warning and returns when readContract throws', async () => {
    const row = emptyStatsRow({ minStake: 100n });
    const ctx = createMockContext(row);
    (ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
      = vi.fn().mockRejectedValue(new Error('rpc error'));

    await refreshMinStake(ctx, row as unknown as Awaited<ReturnType<typeof ensureStatsExists>>);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).not.toHaveBeenCalled();
  });

  it('uses ringBufferWith helper to build non-trivial buffers', () => {
    const pointer = 10;
    const buf = parseRingBuffer(ringBufferWith(pointer, 1, 999n));
    expect(buf[pointer * RING_BUFFER_SEGMENTS + 1]).toBe(999n);
  });
});

// ---------------------------------------------------------------------------
// RING_BUFFER_SEGMENTS and MINIMUM_BLOCKS_FOR_RINGBUFFER exports
// ---------------------------------------------------------------------------

describe('constants', () => {
  it('RING_BUFFER_SEGMENTS is 4', () => {
    expect(RING_BUFFER_SEGMENTS).toBe(4);
  });

  it('MINIMUM_BLOCKS_FOR_RINGBUFFER is 100', () => {
    expect(MINIMUM_BLOCKS_FOR_RINGBUFFER).toBe(100);
  });
});

// ---------------------------------------------------------------------------
// computeMetrics coverage: holderCount and ethReserve at 24h/7d boundaries
// ---------------------------------------------------------------------------

describe('computeMetrics boundary coverage (via updateHourlyData)', () => {
  it('captures ethReserve7dAgo from the oldest non-zero slot', async () => {
    const pointer = 2;
    const buf = makeEmptyRingBuffer();
    // Fill a slot > 24 hours back with a non-zero ETH reserve
    const oldSlot = ((pointer - 50 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
    buf[oldSlot + 0] = 500n;
    // Current hour also has a value
    buf[pointer * RING_BUFFER_SEGMENTS + 0] = 1000n;

    const baseTimestamp = BigInt(SECS_PER_HOUR * 200);
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: pointer,
      ringBuffer: buf.map(String),
    });
    const ctx = createMockContext(row);
    await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.ethReserveLastWeek).toBeGreaterThanOrEqual(0n);
  });

  it('records holderCount at 24h and 7d ago markers', async () => {
    const pointer = 30;
    const buf = makeEmptyRingBuffer();
    // Set holderCount at current pointer (i=0)
    buf[pointer * RING_BUFFER_SEGMENTS + 3] = 100n;
    // Set holderCount at 24h ago (i=23)
    const idx24h = ((pointer - 23 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
    buf[idx24h + 3] = 80n;
    // Set holderCount far back (i=150, older than 7d)
    const idxOld = ((pointer - 150 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
    buf[idxOld + 3] = 50n;

    const baseTimestamp = BigInt(SECS_PER_HOUR * 500);
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: pointer,
      ringBuffer: buf.map(String),
    });
    const ctx = createMockContext(row);
    await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    expect(dbMock.db.update).toHaveBeenCalled();
  });

  it('handles projection fallback when weekly totals are also zero', async () => {
    // All zeros in ring buffer — medium will be 0, fallback is weekly/7
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: BigInt(SECS_PER_HOUR * 100),
      ringBufferPointer: 0,
    });
    const ctx = createMockContext(row);
    const sameHourTimestamp = BigInt(SECS_PER_HOUR * 100) + BigInt(SECS_PER_HOUR / 4);
    await updateHourlyData(ctx, sameHourTimestamp);

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    // Projection should be 0 (weekly/7 = 0/7 = 0)
    expect(setArg.mintNextHourProjected).toBe(0n);
    expect(setArg.burnNextHourProjected).toBe(0n);
  });

  it('computes netSupplyChange from minted minus burned', async () => {
    const pointer = 1;
    const buf = makeEmptyRingBuffer();
    for (let h = 0; h < 24; h++) {
      const idx = ((pointer - h + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
      buf[idx + 1] = 20n; // minted
      buf[idx + 2] = 8n; // burned
    }
    const baseTimestamp = BigInt(SECS_PER_HOUR * 300);
    const row = emptyStatsRow({
      lastHourlyUpdateTimestamp: baseTimestamp,
      ringBufferPointer: pointer,
      ringBuffer: buf.map(String),
    });
    const ctx = createMockContext(row);
    await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR));

    const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
    const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
    expect(setArg.netSupplyChangeDay).toBeGreaterThan(0n);
    expect(setArg.netSupplyChangeWeek).toBeGreaterThan(0n);
  });
});

