import { describe, test, expect } from 'bun:test';
import { IdempotencyCache, IdempotencyConflictError } from '../../src/index.js';

describe('IdempotencyCache.begin', () => {
  test('first call returns fresh', () => {
    const cache = new IdempotencyCache();
    const result = cache.begin('alice', 'key1', { path: '/foo' });
    expect(result.status).toBe('fresh');
  });

  test('replay returns cached response', () => {
    const cache = new IdempotencyCache();
    const a = cache.begin('alice', 'key1', { path: '/foo' });
    if (a.status !== 'fresh') throw new Error('expected fresh');
    a.commit({ ok: true });

    const b = cache.begin('alice', 'key1', { path: '/foo' });
    expect(b.status).toBe('replay');
    if (b.status === 'replay') {
      expect(b.response).toEqual({ ok: true });
    }
  });

  test('argsHash mismatch throws IdempotencyConflictError', () => {
    const cache = new IdempotencyCache();
    const a = cache.begin('alice', 'key1', { path: '/foo' });
    if (a.status !== 'fresh') throw new Error('expected fresh');
    a.commit({ ok: true });

    expect(() =>
      cache.begin('alice', 'key1', { path: '/different' }),
    ).toThrow(IdempotencyConflictError);
  });

  test('inflight retry returns wait-promise', async () => {
    const cache = new IdempotencyCache();
    const a = cache.begin('alice', 'key1', { x: 1 });
    if (a.status !== 'fresh') throw new Error('expected fresh');

    const b = cache.begin('alice', 'key1', { x: 1 });
    expect(b.status).toBe('wait');

    if (b.status === 'wait') {
      a.commit({ result: 42 });
      const v = await b.promise;
      expect(v).toEqual({ result: 42 });
    }
  });

  test('different senders are isolated', () => {
    const cache = new IdempotencyCache();
    const a = cache.begin('alice', 'k', { x: 1 });
    if (a.status !== 'fresh') throw new Error('expected fresh');
    a.commit({ side: 'alice' });

    const b = cache.begin('bob', 'k', { x: 1 });
    expect(b.status).toBe('fresh');
  });

  test('abandon removes the entry so retries proceed fresh', () => {
    const cache = new IdempotencyCache();
    const a = cache.begin('alice', 'key', { p: 1 });
    if (a.status !== 'fresh') throw new Error('expected fresh');
    a.abandon();

    const b = cache.begin('alice', 'key', { p: 1 });
    expect(b.status).toBe('fresh');
  });
});

describe('IdempotencyCache TTL + LRU', () => {
  test('expired entries are evicted on next access', async () => {
    const cache = new IdempotencyCache({ ttlMs: 5 });
    const a = cache.begin('alice', 'k', { x: 1 });
    if (a.status !== 'fresh') throw new Error('expected fresh');
    a.commit('done');

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

    const b = cache.begin('alice', 'k', { x: 1 });
    expect(b.status).toBe('fresh'); // expired → fresh again
  });

  test('LRU caps per-sender entries', () => {
    const cache = new IdempotencyCache({ maxEntriesPerSender: 3 });
    for (let i = 0; i < 5; i++) {
      const r = cache.begin('alice', `key${i}`, { i });
      if (r.status === 'fresh') r.commit(i);
    }
    expect(cache.size()).toBe(3); // first two evicted
  });
});

describe('IdempotencyCache.prune', () => {
  test('removes only TTL-expired entries', async () => {
    // Two senders so begin() on one doesn't auto-evict the other's expired entry.
    const cache = new IdempotencyCache({ ttlMs: 5 });
    const a = cache.begin('alice', 'old', { x: 1 });
    if (a.status === 'fresh') a.commit(1);
    await new Promise((r) => setTimeout(r, 15));
    const b = cache.begin('bob', 'new', { x: 2 });
    if (b.status === 'fresh') b.commit(2);

    const removed = cache.prune();
    expect(removed).toBe(1); // alice/old expired; bob/new fresh
    expect(cache.size()).toBe(1);
  });
});
