/**
 * Acceptance test for V3.12 §"End-to-end test: split-view detection".
 *
 * Scenario:
 *   - One legitimate STH-signing key (the operator's pinned key).
 *   - Two divergent log views A and B, both signed by the same key
 *     (simulating a malicious server that hands different responses to
 *     different clients).
 *   - Two clients (Bob, Charlie) each fetch alice's bundle, but each is
 *     served from a different view.
 *   - A `LightWitness` cross-pollinates the two clients' STHs.
 *   - The witness must reject the second STH at the same tree_size with
 *     a `KTSplitViewError`.
 *
 * Also asserts the *positive* path: when both clients see the same view,
 * no error is raised.
 */

import { describe, expect, test } from 'bun:test';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import {
  KTLogManager,
  KTSplitViewError,
  LightWitness,
  MemoryKTLogStore,
  computeBundleHash,
  computeLogId,
} from '@shade/key-transparency';

const crypto = new SubtleCryptoProvider();

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('Split-view E2E', () => {
  test('two divergent views at the same tree_size are caught by witness', async () => {
    const operator = await crypto.generateEd25519KeyPair();

    // View A — alice has the *real* identity (seed 0x10)
    const viewA = await KTLogManager.create({
      crypto,
      store: new MemoryKTLogStore(),
      signingPrivateKey: operator.privateKey,
      signingPublicKey: operator.publicKey,
    });
    await viewA.recordRegister('alice', computeBundleHash(fakeBundle(0x10)));
    const sthA = await viewA.publishSTH();

    // View B — alice has a *malicious* identity (seed 0xff)
    const viewB = await KTLogManager.create({
      crypto,
      store: new MemoryKTLogStore(),
      signingPrivateKey: operator.privateKey,
      signingPublicKey: operator.publicKey,
    });
    await viewB.recordRegister('alice', computeBundleHash(fakeBundle(0xff)));
    const sthB = await viewB.publishSTH();

    // Both STHs claim tree_size = 1 with the same logId, but with
    // different rootHash + indexRoot. This is what a split-view attack
    // looks like on the wire.
    expect(sthA.treeSize).toBe(sthB.treeSize);
    expect(Buffer.from(sthA.logId).toString('hex')).toBe(
      Buffer.from(sthB.logId).toString('hex'),
    );
    expect(Buffer.from(sthA.rootHash).toString('hex')).not.toBe(
      Buffer.from(sthB.rootHash).toString('hex'),
    );

    // Bob has been served STH A; Charlie has been served STH B.
    // They share a witness (gossip-style):
    const witness = new LightWitness({
      crypto,
      logPublicKey: operator.publicKey,
      fetcher: {
        async fetchLatestSTH() {
          throw new Error('not used in this test');
        },
        async fetchConsistencyProof() {
          return { proof: [] };
        },
      },
    });

    await witness.observe(sthA);
    expect(witness.compare(sthA)).toBe('agree');
    expect(witness.compare(sthB)).toBe('split-view');
    await expect(witness.observe(sthB)).rejects.toBeInstanceOf(KTSplitViewError);
  });

  test('positive path: same view → no false alarm', async () => {
    const operator = await crypto.generateEd25519KeyPair();
    const view = await KTLogManager.create({
      crypto,
      store: new MemoryKTLogStore(),
      signingPrivateKey: operator.privateKey,
      signingPublicKey: operator.publicKey,
    });
    await view.recordRegister('alice', computeBundleHash(fakeBundle(0x10)));
    const sth1 = await view.publishSTH();
    await view.recordRegister('bob', computeBundleHash(fakeBundle(0x20)));
    const sth2 = await view.publishSTH();

    const witness = new LightWitness({
      crypto,
      logPublicKey: operator.publicKey,
      fetcher: {
        async fetchLatestSTH() {
          throw new Error('not used');
        },
        async fetchConsistencyProof(from, to) {
          const result = await view.buildHistoricalConsistencyProof(from, to);
          return { proof: result.map((b) => Buffer.from(b).toString('base64')) };
        },
      },
    });
    await witness.observe(sth1);
    await witness.observe(sth2);
    expect(witness.compare(sth2)).toBe('agree');
  });

  test('rewriting history (forked log at tree_size 1) fails consistency from sth1 → sth2', async () => {
    const operator = await crypto.generateEd25519KeyPair();
    const real = await KTLogManager.create({
      crypto,
      store: new MemoryKTLogStore(),
      signingPrivateKey: operator.privateKey,
      signingPublicKey: operator.publicKey,
    });
    await real.recordRegister('alice', computeBundleHash(fakeBundle(0x10)));
    const sth1 = await real.publishSTH();

    // Build a divergent log that pretends 'mallory' was the first leaf,
    // not 'alice'. The forked tree's STH at size 2 must NOT pass a
    // consistency proof against sth1.
    const fork = await KTLogManager.create({
      crypto,
      store: new MemoryKTLogStore(),
      signingPrivateKey: operator.privateKey,
      signingPublicKey: operator.publicKey,
    });
    await fork.recordRegister('mallory', computeBundleHash(fakeBundle(0xee)));
    await fork.recordRegister('bob', computeBundleHash(fakeBundle(0x20)));
    const forkedSth2 = await fork.publishSTH();
    const forkedConsistency = await fork.buildConsistencyProof(sth1.treeSize);

    const witness = new LightWitness({
      crypto,
      logPublicKey: operator.publicKey,
      fetcher: {
        async fetchLatestSTH() {
          throw new Error('not used');
        },
        async fetchConsistencyProof() {
          return { proof: forkedConsistency.proof.map((b) => Buffer.from(b).toString('base64')) };
        },
      },
    });
    await witness.observe(sth1);
    await expect(witness.observe(forkedSth2)).rejects.toThrow();

    // sanity: logId pinning still valid
    expect(Buffer.from(forkedSth2.logId).toString('hex')).toBe(
      Buffer.from(computeLogId(operator.publicKey)).toString('hex'),
    );
  });
});
