import { describe, test, expect } from 'bun:test';
import { RateLimiter, MemoryRateLimitStore, createPrekeyServer, MemoryPrekeyStore, signPayload } from '../src/index.js';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { generateIdentityKeyPair, RateLimitError } from '@shade/core';

const crypto = new SubtleCryptoProvider();

describe('RateLimiter', () => {
  test('allows requests up to capacity', async () => {
    const store = new MemoryRateLimitStore();
    const rl = new RateLimiter(store, { capacity: 5, refillPerSecond: 1 });

    for (let i = 0; i < 5; i++) {
      await rl.consume('user:1');
    }
  });

  test('throws RateLimitError when exhausted', async () => {
    const store = new MemoryRateLimitStore();
    const rl = new RateLimiter(store, { capacity: 3, refillPerSecond: 0.1 });

    for (let i = 0; i < 3; i++) await rl.consume('user:1');
    expect(rl.consume('user:1')).rejects.toThrow(RateLimitError);
  });

  test('different keys have independent limits', async () => {
    const store = new MemoryRateLimitStore();
    const rl = new RateLimiter(store, { capacity: 2, refillPerSecond: 0.1 });

    await rl.consume('user:1');
    await rl.consume('user:1');
    // user:1 exhausted
    expect(rl.consume('user:1')).rejects.toThrow(RateLimitError);

    // user:2 still has full capacity
    await rl.consume('user:2');
    await rl.consume('user:2');
  });

  test('refills over time', async () => {
    const store = new MemoryRateLimitStore();
    const rl = new RateLimiter(store, { capacity: 2, refillPerSecond: 100 }); // fast refill for test

    await rl.consume('user:1');
    await rl.consume('user:1');

    // Wait a bit for refill (100 tokens/sec → 2 tokens in 20ms)
    await new Promise((r) => setTimeout(r, 30));

    // Should be refilled
    await rl.consume('user:1');
  });

  test('RateLimitError has retryAfterSeconds', async () => {
    const store = new MemoryRateLimitStore();
    const rl = new RateLimiter(store, { capacity: 1, refillPerSecond: 0.5 });

    await rl.consume('user:1');
    try {
      await rl.consume('user:1');
      expect.unreachable();
    } catch (err) {
      expect(err).toBeInstanceOf(RateLimitError);
      expect((err as RateLimitError).retryAfterSeconds).toBeGreaterThan(0);
    }
  });
});

describe('Rate limiting integration with routes', () => {
  test('register endpoint rate-limits per IP', async () => {
    const app = createPrekeyServer({ crypto, store: new MemoryPrekeyStore() });

    async function doRegister(addressSuffix: number) {
      const identity = await generateIdentityKeyPair(crypto);
      const body: any = {
        address: `user${addressSuffix}`,
        identitySigningKey: Buffer.from(identity.signingPublicKey).toString('base64'),
        identityDHKey: Buffer.from(identity.dhPublicKey).toString('base64'),
        signedPreKey: {
          keyId: 1,
          publicKey: Buffer.from(crypto.randomBytes(32)).toString('base64'),
          signature: Buffer.from(crypto.randomBytes(64)).toString('base64'),
        },
      };
      const signed = await signPayload(crypto, identity.signingPrivateKey, body);
      return app.request('/v1/keys/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'x-forwarded-for': '203.0.113.1' },
        body: JSON.stringify(signed),
      });
    }

    // Register limit is 5/hour, so after 5 successful, the 6th should fail
    const results: number[] = [];
    for (let i = 0; i < 7; i++) {
      const res = await doRegister(i);
      results.push(res.status);
    }

    // First 5 should succeed (200), rest should be rate-limited (429)
    expect(results.filter((s) => s === 200).length).toBeGreaterThanOrEqual(5);
    expect(results.filter((s) => s === 429).length).toBeGreaterThanOrEqual(1);
  });

  // V4.8.1 — `SHADE_DISABLE_RATE_LIMIT=1` in standalone.ts is plumbed
  // through to `createPrekeyServer({ disableRateLimit })`. This test
  // covers the "what happens when the flag is true" path; the env-var
  // → option conversion in standalone.ts is a one-liner verified by
  // inspection.
  test('register endpoint allows >5/hour from a single IP when disableRateLimit is set', async () => {
    const app = createPrekeyServer({
      crypto,
      store: new MemoryPrekeyStore(),
      disableRateLimit: true,
    });

    async function doRegister(addressSuffix: number) {
      const identity = await generateIdentityKeyPair(crypto);
      const body: any = {
        address: `user${addressSuffix}`,
        identitySigningKey: Buffer.from(identity.signingPublicKey).toString('base64'),
        identityDHKey: Buffer.from(identity.dhPublicKey).toString('base64'),
        signedPreKey: {
          keyId: 1,
          publicKey: Buffer.from(crypto.randomBytes(32)).toString('base64'),
          signature: Buffer.from(crypto.randomBytes(64)).toString('base64'),
        },
      };
      const signed = await signPayload(crypto, identity.signingPrivateKey, body);
      return app.request('/v1/keys/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'x-forwarded-for': '203.0.113.1' },
        body: JSON.stringify(signed),
      });
    }

    const results: number[] = [];
    for (let i = 0; i < 12; i++) {
      const res = await doRegister(i);
      results.push(res.status);
    }
    // No 429 anywhere — the limit is OFF.
    expect(results.filter((s) => s === 429).length).toBe(0);
    expect(results.filter((s) => s === 200).length).toBe(12);
  });

  test('rate limit returns Retry-After header', async () => {
    const app = createPrekeyServer({ crypto, store: new MemoryPrekeyStore() });

    // Burn through the delete limit (5/hour)
    const identity = await generateIdentityKeyPair(crypto);

    // First register
    const regBody: any = {
      address: 'ratelimit',
      identitySigningKey: Buffer.from(identity.signingPublicKey).toString('base64'),
      identityDHKey: Buffer.from(identity.dhPublicKey).toString('base64'),
      signedPreKey: {
        keyId: 1,
        publicKey: Buffer.from(crypto.randomBytes(32)).toString('base64'),
        signature: Buffer.from(crypto.randomBytes(64)).toString('base64'),
      },
    };
    const signedReg = await signPayload(crypto, identity.signingPrivateKey, regBody);
    await app.request('/v1/keys/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'x-forwarded-for': '203.0.113.2' },
      body: JSON.stringify(signedReg),
    });

    // Burn through the delete limit
    for (let i = 0; i < 5; i++) {
      const delBody = await signPayload(crypto, identity.signingPrivateKey, { address: 'ratelimit' });
      await app.request('/v1/keys/ratelimit', {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(delBody),
      });
    }

    // 6th should be rate-limited
    const delBody = await signPayload(crypto, identity.signingPrivateKey, { address: 'ratelimit' });
    const res = await app.request('/v1/keys/ratelimit', {
      method: 'DELETE',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(delBody),
    });
    expect(res.status).toBe(429);
    expect(res.headers.get('Retry-After')).toBeTruthy();
  });
});
