import { describe, expect, test } from 'bun:test';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import {
  KTLogManager,
  MemoryKTLogStore,
  computeBundleHash,
  ktProofFromWire,
  ktProofToWire,
  verifyBundleAbsence,
  verifyBundleInclusion,
  verifyBundleTombstone,
} from '../src/index.js';
import { KTSplitViewError, KTVerificationError } from '../src/errors.js';

const crypto = new SubtleCryptoProvider();

async function makeManager() {
  const kp = await crypto.generateEd25519KeyPair();
  const store = new MemoryKTLogStore();
  const mgr = await KTLogManager.create({
    crypto,
    store,
    signingPrivateKey: kp.privateKey,
    signingPublicKey: kp.publicKey,
  });
  return { mgr, kp, store };
}

function fakeBundle(seed: number) {
  return {
    identitySigningKey: new Uint8Array(32).fill(seed),
    identityDHKey: new Uint8Array(32).fill(seed + 1),
    signedPreKey: {
      keyId: 1,
      publicKey: new Uint8Array(32).fill(seed + 2),
      signature: new Uint8Array(64).fill(seed + 3),
    },
  };
}

describe('KTLogManager — happy paths', () => {
  test('register + buildBundleInclusionProof + verify', async () => {
    const { mgr, kp } = await makeManager();
    const bundle = fakeBundle(0x10);
    const bundleHash = computeBundleHash(bundle);
    await mgr.recordRegister('alice', bundleHash);
    const sth = await mgr.publishSTH();

    const proof = await mgr.buildBundleInclusionProof('alice', sth);
    expect(proof).not.toBeNull();
    expect(proof!.body.kind).toBe('inclusion');

    const verified = await verifyBundleInclusion(
      { crypto, logPublicKey: kp.publicKey },
      'alice',
      bundle,
      proof!,
    );
    expect(verified.treeSize).toBe(1);
  });

  test('absence proof for unknown address verifies', async () => {
    const { mgr, kp } = await makeManager();
    await mgr.recordRegister('alice', computeBundleHash(fakeBundle(0x10)));
    const sth = await mgr.publishSTH();
    const proof = mgr.buildBundleAbsenceProof('zeta', sth);
    expect(proof).not.toBeNull();
    expect(proof!.body.kind).toBe('absence');
    await verifyBundleAbsence({ crypto, logPublicKey: kp.publicKey }, 'zeta', proof!);
  });

  test('tombstone proof verifies after delete', async () => {
    const { mgr, kp } = await makeManager();
    await mgr.recordRegister('alice', computeBundleHash(fakeBundle(0x10)));
    await mgr.recordDelete('alice');
    const sth = await mgr.publishSTH();
    const proof = await mgr.buildBundleInclusionProof('alice', sth);
    expect(proof!.body.kind).toBe('tombstone');
    const verified = await verifyBundleTombstone(
      { crypto, logPublicKey: kp.publicKey },
      'alice',
      proof!,
    );
    expect(verified.treeSize).toBe(2);
  });

  test('multiple addresses + STH at increasing tree sizes', async () => {
    const { mgr, kp } = await makeManager();
    const aliceBundle = fakeBundle(0x10);
    const bobBundle = fakeBundle(0x20);
    await mgr.recordRegister('alice', computeBundleHash(aliceBundle));
    await mgr.recordRegister('bob', computeBundleHash(bobBundle));
    const sth = await mgr.publishSTH();
    expect(sth.treeSize).toBe(2);

    const proofAlice = await mgr.buildBundleInclusionProof('alice', sth);
    const proofBob = await mgr.buildBundleInclusionProof('bob', sth);
    await verifyBundleInclusion(
      { crypto, logPublicKey: kp.publicKey },
      'alice',
      aliceBundle,
      proofAlice!,
    );
    await verifyBundleInclusion(
      { crypto, logPublicKey: kp.publicKey },
      'bob',
      bobBundle,
      proofBob!,
    );
  });

  test('rotation: new register replaces old', async () => {
    const { mgr, kp } = await makeManager();
    const v1 = fakeBundle(0x10);
    const v2 = fakeBundle(0x55);
    await mgr.recordRegister('alice', computeBundleHash(v1));
    await mgr.recordRegister('alice', computeBundleHash(v2));
    const sth = await mgr.publishSTH();
    const proof = await mgr.buildBundleInclusionProof('alice', sth);
    // Latest is v2; verifying with v1's bundle should fail.
    await expect(
      verifyBundleInclusion({ crypto, logPublicKey: kp.publicKey }, 'alice', v1, proof!),
    ).rejects.toBeInstanceOf(KTVerificationError);
    await verifyBundleInclusion(
      { crypto, logPublicKey: kp.publicKey },
      'alice',
      v2,
      proof!,
    );
  });
});

describe('KTLogManager — wire encoding roundtrip', () => {
  test('inclusion proof survives wire roundtrip', async () => {
    const { mgr, kp } = await makeManager();
    const bundle = fakeBundle(0x42);
    await mgr.recordRegister('alice', computeBundleHash(bundle));
    const sth = await mgr.publishSTH();
    const proof = await mgr.buildBundleInclusionProof('alice', sth);

    const wire = ktProofToWire(proof!);
    const json = JSON.stringify(wire);
    const back = ktProofFromWire(JSON.parse(json));
    await verifyBundleInclusion(
      { crypto, logPublicKey: kp.publicKey },
      'alice',
      bundle,
      back,
    );
  });
});

describe('Tampering detection', () => {
  test('forged bundle (different signing key) is rejected', async () => {
    const { mgr, kp } = await makeManager();
    const real = fakeBundle(0x10);
    await mgr.recordRegister('alice', computeBundleHash(real));
    const sth = await mgr.publishSTH();
    const proof = await mgr.buildBundleInclusionProof('alice', sth);

    const forged = { ...real, identitySigningKey: new Uint8Array(32).fill(0xff) };
    await expect(
      verifyBundleInclusion({ crypto, logPublicKey: kp.publicKey }, 'alice', forged, proof!),
    ).rejects.toBeInstanceOf(KTVerificationError);
  });

  test('proof for alice cannot be re-used for bob (address mismatch)', async () => {
    const { mgr, kp } = await makeManager();
    const aliceBundle = fakeBundle(0x10);
    await mgr.recordRegister('alice', computeBundleHash(aliceBundle));
    const sth = await mgr.publishSTH();
    const proof = await mgr.buildBundleInclusionProof('alice', sth);
    await expect(
      verifyBundleInclusion(
        { crypto, logPublicKey: kp.publicKey },
        'bob',
        aliceBundle,
        proof!,
      ),
    ).rejects.toBeInstanceOf(KTVerificationError);
  });

  test('split-view: same tree_size with different roots is detected by witness', async () => {
    const { mgr, kp } = await makeManager();
    const bundle = fakeBundle(0x10);
    await mgr.recordRegister('alice', computeBundleHash(bundle));
    const sth = await mgr.publishSTH();
    // Forge a *different* STH at the same tree_size — pretend the server
    // signed two divergent versions.
    const sth2 = await mgr.publishSTH();
    expect(sth2.treeSize).toBe(sth.treeSize);

    // To simulate a conflicting STH, sign one with a tampered root_hash.
    const tampered = { ...sth, rootHash: new Uint8Array(sth.rootHash) };
    tampered.rootHash[0] ^= 0xff;
    // Re-sign with the same key so it would individually verify…
    const forged = await (await import('../src/sth.js')).signSth(crypto, kp.privateKey, {
      treeSize: tampered.treeSize,
      timestampMs: tampered.timestampMs,
      rootHash: tampered.rootHash,
      indexRoot: tampered.indexRoot,
      logId: tampered.logId,
    });

    const { LightWitness } = await import('../src/witness.js');
    const witness = new LightWitness({
      crypto,
      logPublicKey: kp.publicKey,
      fetcher: {
        async fetchLatestSTH() {
          return (await import('../src/sth.js')).sthToWire(sth, (b) =>
            Buffer.from(b).toString('base64'),
          );
        },
        async fetchConsistencyProof() {
          return { proof: [] };
        },
      },
    });
    await witness.observe(sth);
    await expect(witness.observe(forged)).rejects.toBeInstanceOf(KTSplitViewError);
  });
});
