import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { ed25519 } from '@noble/curves/ed25519.js';
import { setupFileRig, type FileTestRig } from '../integration/helpers/rig.js';
import {
  bytesToBase64,
  canonicalRpcBytes,
  hashArgs,
  InvalidSignatureError,
  type FileEntry,
  NotFoundError,
} from '../../src/index.js';

/**
 * The dispatcher's `verifySender` callback gets the canonical bytes the
 * client claims they signed. By plugging a real Ed25519 verify in tests,
 * we can demonstrate that:
 *   - A valid sig over the canonical bytes is accepted.
 *   - Tampering ANY bound field (kind, args, signedAt, sender) breaks
 *     verification → InvalidSignatureError.
 */

describe('Tampered envelope — Ed25519 sig verification', () => {
  let rig: FileTestRig;
  // Generate a stable Ed25519 keypair for Alice. Bob will pin it.
  const alicePriv = ed25519.utils.randomSecretKey();
  const alicePub = ed25519.getPublicKey(alicePriv);

  beforeAll(async () => {
    rig = await setupFileRig({
      verifySender: (sender, canonical, sigBase64) => {
        // We only know Alice's key for this test. Bob's pub key would be
        // looked up similarly in a real app.
        if (sender !== 'alice') return false;
        // Decode base64 sig
        try {
          const bin = atob(sigBase64);
          const sigBytes = new Uint8Array(bin.length);
          for (let i = 0; i < bin.length; i++) sigBytes[i] = bin.charCodeAt(i);
          return ed25519.verify(sigBytes, canonical, alicePub);
        } catch {
          return false;
        }
      },
      stat: async (ctx) => {
        if (ctx.path !== '/exists.txt') throw new NotFoundError(ctx.path);
        const e: FileEntry = { name: 'exists.txt', kind: 'file', size: 1, mtime: 0, metadata: {} };
        return e;
      },
    });
    // Re-create the client with a real signRequest hook.
    // (Rig's default fs has signRequest=undefined; we replace it.)
    const { createFileClient, ShadeFileRpcChannel, PendingRpcRegistry, attachClientRouting } = await import('../../src/index.js');
    const aliceChannel = new ShadeFileRpcChannel(rig.alice);
    const alicePending = new PendingRpcRegistry();
    attachClientRouting(aliceChannel, alicePending);
    rig.fs = createFileClient(rig.alice, aliceChannel, alicePending, 'bob', {
      defaultTimeoutMs: 5000,
      signRequest: async (canonical) => {
        const sig = ed25519.sign(canonical, alicePriv);
        return bytesToBase64(sig);
      },
    });
  });

  afterAll(async () => {
    await rig.teardown();
  });

  test('valid signature → request succeeds', async () => {
    const result = await rig.fs.stat('/exists.txt');
    expect(result.name).toBe('exists.txt');
  });

  test('tampered args → InvalidSignatureError', async () => {
    // Craft a request manually: sign over '/a' but ship '/b'.
    const { createFileClient, ShadeFileRpcChannel, PendingRpcRegistry, attachClientRouting } = await import('../../src/index.js');
    const aliceChannel = new ShadeFileRpcChannel(rig.alice);
    const alicePending = new PendingRpcRegistry();
    attachClientRouting(aliceChannel, alicePending);
    const tamperedFs = createFileClient(rig.alice, aliceChannel, alicePending, 'bob', {
      defaultTimeoutMs: 3000,
      signRequest: async (_canonical) => {
        // Sign over a DIFFERENT canonical (different argsHash), so the
        // server's recomputation won't match.
        const fake = canonicalRpcBytes({
          address: 'alice',
          signedAt: 0,
          kind: 'shade.fs.list/v1',
          id: 'AAAAAAAAAAAAAAAAAAAAAA',
          argsHash: hashArgs({ tampered: true }),
        });
        const sig = ed25519.sign(fake, alicePriv);
        return bytesToBase64(sig);
      },
    });
    await expect(tamperedFs.stat('/exists.txt')).rejects.toBeInstanceOf(InvalidSignatureError);
  });

  test('valid signature from unknown signer → InvalidSignatureError', async () => {
    const { createFileClient, ShadeFileRpcChannel, PendingRpcRegistry, attachClientRouting } = await import('../../src/index.js');
    const aliceChannel = new ShadeFileRpcChannel(rig.alice);
    const alicePending = new PendingRpcRegistry();
    attachClientRouting(aliceChannel, alicePending);

    const otherPriv = ed25519.utils.randomSecretKey();
    const wrongFs = createFileClient(rig.alice, aliceChannel, alicePending, 'bob', {
      defaultTimeoutMs: 3000,
      signRequest: async (canonical) => {
        const sig = ed25519.sign(canonical, otherPriv);
        return bytesToBase64(sig);
      },
    });
    await expect(wrongFs.stat('/exists.txt')).rejects.toBeInstanceOf(InvalidSignatureError);
  });
});
