import { describe, test, expect, beforeEach } from 'bun:test';
import { Hono } from 'hono';
import {
  createInboxServer,
  MemoryInboxStore,
  computeMsgId,
  type InboxStore,
} from '../src/index.js';
import { signPayload } from '@shade/server';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { generateIdentityKeyPair, toBase64 } from '@shade/core';

const crypto = new SubtleCryptoProvider();

async function makeIdentity() {
  return generateIdentityKeyPair(crypto);
}

function randBytes(n: number): Uint8Array {
  const buf = new Uint8Array(n);
  globalThis.crypto.getRandomValues(buf);
  return buf;
}

describe('Shade Inbox Server', () => {
  let store: InboxStore;
  let app: Hono;

  beforeEach(() => {
    store = new MemoryInboxStore();
    app = createInboxServer({ crypto, store, disableRateLimit: true });
  });

  function req(method: string, path: string, body?: any) {
    const init: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
    if (body !== undefined) init.body = JSON.stringify(body);
    return app.request(path, init);
  }

  async function registerBob(address = 'bob') {
    const bob = await makeIdentity();
    const body = await signPayload(crypto, bob.signingPrivateKey, {
      address,
      signingKey: toBase64(bob.signingPublicKey),
    });
    const res = await req('POST', '/v1/inbox/register', body);
    expect(res.status).toBe(200);
    return bob;
  }

  async function putMsg(args: {
    sender: Awaited<ReturnType<typeof makeIdentity>>;
    recipient: string;
    ciphertext: Uint8Array;
    ttlSeconds?: number;
  }) {
    const msgId = await computeMsgId(args.ciphertext);
    const body: Record<string, unknown> = {
      senderSigningKey: toBase64(args.sender.signingPublicKey),
      msgId,
      ciphertext: toBase64(args.ciphertext),
    };
    if (args.ttlSeconds !== undefined) body.ttlSeconds = args.ttlSeconds;
    const signed = await signPayload(crypto, args.sender.signingPrivateKey, body);
    const res = await req('POST', `/v1/inbox/${args.recipient}`, signed);
    return { res, msgId };
  }

  // ─── Health ─────────────────────────────────────────────────

  test('health endpoint responds', async () => {
    const res = await req('GET', '/health');
    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json.service).toBe('shade-inbox-server');
  });

  // ─── Registration (TOFU) ────────────────────────────────────

  describe('POST /v1/inbox/register', () => {
    test('accepts valid registration', async () => {
      await registerBob();
    });

    test('idempotent re-register with same key', async () => {
      const bob = await registerBob('bob');
      const body = await signPayload(crypto, bob.signingPrivateKey, {
        address: 'bob',
        signingKey: toBase64(bob.signingPublicKey),
      });
      const res = await req('POST', '/v1/inbox/register', body);
      expect(res.status).toBe(200);
    });

    test('rejects different key claiming same address', async () => {
      await registerBob('bob');
      const eve = await makeIdentity();
      const body = await signPayload(crypto, eve.signingPrivateKey, {
        address: 'bob',
        signingKey: toBase64(eve.signingPublicKey),
      });
      const res = await req('POST', '/v1/inbox/register', body);
      expect(res.status).toBe(401);
    });

    test('rejects unsigned body', async () => {
      const bob = await makeIdentity();
      const res = await req('POST', '/v1/inbox/register', {
        address: 'bob',
        signingKey: toBase64(bob.signingPublicKey),
      });
      expect(res.status).toBe(400);
    });

    test('rejects bad signature', async () => {
      const bob = await makeIdentity();
      const res = await req('POST', '/v1/inbox/register', {
        address: 'bob',
        signingKey: toBase64(bob.signingPublicKey),
        signedAt: Date.now(),
        signature: toBase64(randBytes(64)),
      });
      expect(res.status).toBe(401);
    });
  });

  // ─── PUT blob ───────────────────────────────────────────────

  describe('POST /v1/inbox/:address (PUT blob)', () => {
    test('stores a signed blob from sender', async () => {
      await registerBob();
      const alice = await makeIdentity();
      const ct = randBytes(128);
      const { res, msgId } = await putMsg({ sender: alice, recipient: 'bob', ciphertext: ct });
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.msgId).toBe(msgId);
      expect(json.idempotent).toBe(false);
    });

    test('idempotent on duplicate ciphertext', async () => {
      await registerBob();
      const alice = await makeIdentity();
      const ct = randBytes(64);
      const first = await putMsg({ sender: alice, recipient: 'bob', ciphertext: ct });
      expect(first.res.status).toBe(200);
      const second = await putMsg({ sender: alice, recipient: 'bob', ciphertext: ct });
      expect(second.res.status).toBe(200);
      const j2 = await second.res.json();
      expect(j2.idempotent).toBe(true);
      expect(j2.msgId).toBe(first.msgId);
    });

    test('rejects mismatched msgId', async () => {
      await registerBob();
      const alice = await makeIdentity();
      const ct = randBytes(64);
      const wrongId = '0'.repeat(64);
      const body = await signPayload(crypto, alice.signingPrivateKey, {
        senderSigningKey: toBase64(alice.signingPublicKey),
        msgId: wrongId,
        ciphertext: toBase64(ct),
      });
      const res = await req('POST', '/v1/inbox/bob', body);
      expect(res.status).toBe(400);
    });

    test('rejects PUT to unregistered address', async () => {
      const alice = await makeIdentity();
      const ct = randBytes(64);
      const { res } = await putMsg({ sender: alice, recipient: 'nobody', ciphertext: ct });
      expect(res.status).toBe(404);
    });

    test('rejects bad sender signature', async () => {
      await registerBob();
      const alice = await makeIdentity();
      const eve = await makeIdentity();
      const ct = randBytes(64);
      const msgId = await computeMsgId(ct);
      // Sign with Eve, claim Alice's key.
      const body = await signPayload(crypto, eve.signingPrivateKey, {
        senderSigningKey: toBase64(alice.signingPublicKey),
        msgId,
        ciphertext: toBase64(ct),
      });
      const res = await req('POST', '/v1/inbox/bob', body);
      expect(res.status).toBe(401);
    });

    test('rejects ciphertext > maxBlobBytes', async () => {
      const small = createInboxServer({
        crypto,
        store: new MemoryInboxStore(),
        disableRateLimit: true,
        quota: { maxBlobBytes: 256 },
      });
      // Register bob in this fresh app.
      const bob = await makeIdentity();
      const reg = await signPayload(crypto, bob.signingPrivateKey, {
        address: 'bob',
        signingKey: toBase64(bob.signingPublicKey),
      });
      await small.request('/v1/inbox/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(reg),
      });
      const alice = await makeIdentity();
      const ct = randBytes(257);
      const msgId = await computeMsgId(ct);
      const body = await signPayload(crypto, alice.signingPrivateKey, {
        senderSigningKey: toBase64(alice.signingPublicKey),
        msgId,
        ciphertext: toBase64(ct),
      });
      const res = await small.request('/v1/inbox/bob', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      expect(res.status).toBe(400);
    });

    test('rejects stale signature (replay window)', async () => {
      await registerBob();
      const alice = await makeIdentity();
      const ct = randBytes(64);
      const msgId = await computeMsgId(ct);
      // Hand-craft: sign normally, then mutate signedAt to 10 minutes ago.
      const signed = await signPayload(crypto, alice.signingPrivateKey, {
        senderSigningKey: toBase64(alice.signingPublicKey),
        msgId,
        ciphertext: toBase64(ct),
      });
      (signed as any).signedAt = Date.now() - 10 * 60 * 1000;
      const res = await req('POST', '/v1/inbox/bob', signed);
      // signedAt mutated → signature invalid → 401, OR replay → 409.
      expect([401, 409]).toContain(res.status);
    });

    test('enforces per-address quota', async () => {
      const small = createInboxServer({
        crypto,
        store: new MemoryInboxStore(),
        disableRateLimit: true,
        quota: { maxBlobsPerAddress: 2 },
      });
      const bob = await makeIdentity();
      const reg = await signPayload(crypto, bob.signingPrivateKey, {
        address: 'bob',
        signingKey: toBase64(bob.signingPublicKey),
      });
      await small.request('/v1/inbox/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(reg),
      });

      const alice = await makeIdentity();
      for (let i = 0; i < 2; i++) {
        const ct = randBytes(32 + i);
        const msgId = await computeMsgId(ct);
        const body = await signPayload(crypto, alice.signingPrivateKey, {
          senderSigningKey: toBase64(alice.signingPublicKey),
          msgId,
          ciphertext: toBase64(ct),
        });
        const r = await small.request('/v1/inbox/bob', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        expect(r.status).toBe(200);
      }
      // Third should be quota-rejected.
      const ct = randBytes(99);
      const msgId = await computeMsgId(ct);
      const body = await signPayload(crypto, alice.signingPrivateKey, {
        senderSigningKey: toBase64(alice.signingPublicKey),
        msgId,
        ciphertext: toBase64(ct),
      });
      const r = await small.request('/v1/inbox/bob', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      expect(r.status).toBe(400);
    });
  });

  // ─── FETCH ──────────────────────────────────────────────────

  describe('POST /v1/inbox/:address/fetch', () => {
    test('returns blobs after registration', async () => {
      const bob = await registerBob();
      const alice = await makeIdentity();
      const ct1 = randBytes(64);
      const ct2 = randBytes(80);
      await putMsg({ sender: alice, recipient: 'bob', ciphertext: ct1 });
      await putMsg({ sender: alice, recipient: 'bob', ciphertext: ct2 });

      const fetchBody = await signPayload(crypto, bob.signingPrivateKey, { address: 'bob', sinceCursor: 0 });
      const res = await req('POST', '/v1/inbox/bob/fetch', fetchBody);
      expect(res.status).toBe(200);
      const json = await res.json();
      expect(json.blobs.length).toBe(2);
      expect(typeof json.cursor).toBe('number');
    });

    test('cursor pagination skips already-seen blobs', async () => {
      const bob = await registerBob();
      const alice = await makeIdentity();
      await putMsg({ sender: alice, recipient: 'bob', ciphertext: randBytes(20) });
      const firstFetch = await signPayload(crypto, bob.signingPrivateKey, { address: 'bob', sinceCursor: 0 });
      const r1 = await req('POST', '/v1/inbox/bob/fetch', firstFetch);
      const j1 = await r1.json();
      const cursor = j1.cursor;
      expect(j1.blobs.length).toBe(1);
      // Add a second blob.
      await putMsg({ sender: alice, recipient: 'bob', ciphertext: randBytes(30) });
      const secondFetch = await signPayload(crypto, bob.signingPrivateKey, { address: 'bob', sinceCursor: cursor });
      const r2 = await req('POST', '/v1/inbox/bob/fetch', secondFetch);
      const j2 = await r2.json();
      expect(j2.blobs.length).toBe(1);
    });

    test('rejects fetch from a different signing key', async () => {
      await registerBob();
      const eve = await makeIdentity();
      const fetchBody = await signPayload(crypto, eve.signingPrivateKey, { address: 'bob', sinceCursor: 0 });
      const res = await req('POST', '/v1/inbox/bob/fetch', fetchBody);
      expect(res.status).toBe(401);
    });

    test('rejects fetch on unregistered address', async () => {
      const eve = await makeIdentity();
      const fetchBody = await signPayload(crypto, eve.signingPrivateKey, { address: 'nobody', sinceCursor: 0 });
      const res = await req('POST', '/v1/inbox/nobody/fetch', fetchBody);
      expect(res.status).toBe(404);
    });
  });

  // ─── DELETE / ack ───────────────────────────────────────────

  describe('DELETE /v1/inbox/:address/:msgId', () => {
    test('removes a blob after ack', async () => {
      const bob = await registerBob();
      const alice = await makeIdentity();
      const ct = randBytes(64);
      const { msgId } = await putMsg({ sender: alice, recipient: 'bob', ciphertext: ct });

      const ackBody = await signPayload(crypto, bob.signingPrivateKey, { address: 'bob', msgId });
      const res = await req('DELETE', `/v1/inbox/bob/${msgId}`, ackBody);
      expect(res.status).toBe(200);
      const j = await res.json();
      expect(j.ok).toBe(true);

      // Subsequent fetch should return zero.
      const fetchBody = await signPayload(crypto, bob.signingPrivateKey, { address: 'bob', sinceCursor: 0 });
      const r2 = await req('POST', '/v1/inbox/bob/fetch', fetchBody);
      const j2 = await r2.json();
      expect(j2.blobs.length).toBe(0);
    });

    test('rejects ack from a different signing key', async () => {
      const bob = await registerBob();
      const alice = await makeIdentity();
      const eve = await makeIdentity();
      const ct = randBytes(64);
      const { msgId } = await putMsg({ sender: alice, recipient: 'bob', ciphertext: ct });
      const ackBody = await signPayload(crypto, eve.signingPrivateKey, { address: 'bob', msgId });
      const res = await req('DELETE', `/v1/inbox/bob/${msgId}`, ackBody);
      expect(res.status).toBe(401);
      // and the blob must still be there
      const fetchBody = await signPayload(crypto, bob.signingPrivateKey, { address: 'bob', sinceCursor: 0 });
      const r2 = await req('POST', '/v1/inbox/bob/fetch', fetchBody);
      const j2 = await r2.json();
      expect(j2.blobs.length).toBe(1);
    });
  });
});
