import { describe, expect, test } from 'bun:test';
import fc from 'fast-check';
import {
  MerkleLog,
  auditPath,
  consistencyProof,
  emptyRootHash,
  leafHash,
  nodeHash,
  recomputeRootFromAuditPath,
  verifyConsistencyProof,
} from '../src/index.js';
import { mth } from '../src/log.js';

function buildLog(n: number): MerkleLog {
  const log = new MerkleLog();
  for (let i = 0; i < n; i++) {
    log.appendData(new Uint8Array([i & 0xff, (i >> 8) & 0xff]));
  }
  return log;
}

describe('MerkleLog basics', () => {
  test('empty tree root = SHA-256(empty)', () => {
    const log = new MerkleLog();
    expect(log.rootHash()).toEqual(emptyRootHash());
  });

  test('single-leaf tree root = leaf_hash(d)', () => {
    const log = new MerkleLog();
    const d = new Uint8Array([0xab, 0xcd]);
    log.appendData(d);
    expect(log.rootHash()).toEqual(leafHash(d));
  });

  test('two-leaf tree root = node_hash(leaf0, leaf1)', () => {
    const log = new MerkleLog();
    const d0 = new Uint8Array([1]);
    const d1 = new Uint8Array([2]);
    log.appendData(d0);
    log.appendData(d1);
    expect(log.rootHash()).toEqual(nodeHash(leafHash(d0), leafHash(d1)));
  });

  test('append-only ordering preserved', () => {
    const log = new MerkleLog();
    log.appendData(new Uint8Array([1]));
    log.appendData(new Uint8Array([2]));
    expect(log.size).toBe(2);
    log.appendData(new Uint8Array([3]));
    expect(log.size).toBe(3);
  });
});

describe('Audit path verification', () => {
  test('valid audit path reconstructs root for every leaf, every size 1..32', () => {
    for (let n = 1; n <= 32; n++) {
      const log = buildLog(n);
      const root = log.rootHash();
      for (let m = 0; m < n; m++) {
        const path = log.auditPath(m);
        const lh = log.leafHashAt(m);
        const reconstructed = recomputeRootFromAuditPath(lh, m, n, path);
        expect(Buffer.from(reconstructed).toString('hex')).toBe(
          Buffer.from(root).toString('hex'),
        );
      }
    }
  });

  test('property: tampered leaf fails verification', () => {
    const log = buildLog(7);
    const root = log.rootHash();
    const path = log.auditPath(3);
    const tampered = new Uint8Array(log.leafHashAt(3));
    tampered[0] ^= 0xff;
    const reconstructed = recomputeRootFromAuditPath(tampered, 3, 7, path);
    expect(Buffer.from(reconstructed).toString('hex')).not.toBe(
      Buffer.from(root).toString('hex'),
    );
  });

  test('property: tampered audit path fails verification', () => {
    const log = buildLog(11);
    const root = log.rootHash();
    const path = log.auditPath(5);
    if (path.length === 0) return;
    const tampered = path.map((p, i) => {
      if (i === 0) {
        const x = new Uint8Array(p);
        x[0] ^= 0xff;
        return x;
      }
      return p;
    });
    const reconstructed = recomputeRootFromAuditPath(log.leafHashAt(5), 5, 11, tampered);
    expect(Buffer.from(reconstructed).toString('hex')).not.toBe(
      Buffer.from(root).toString('hex'),
    );
  });

  test('property-based: random N and m', () => {
    fc.assert(
      fc.property(fc.integer({ min: 1, max: 100 }), (n) => {
        const log = buildLog(n);
        const root = log.rootHash();
        const m = Math.min(n - 1, Math.floor(Math.random() * n));
        const path = log.auditPath(m);
        const reconstructed = recomputeRootFromAuditPath(log.leafHashAt(m), m, n, path);
        return Buffer.from(reconstructed).toString('hex') === Buffer.from(root).toString('hex');
      }),
      { numRuns: 50 },
    );
  });
});

describe('Consistency proofs', () => {
  test('m === 0 always consistent', () => {
    const log = buildLog(5);
    const newRoot = log.rootHash();
    expect(verifyConsistencyProof(0, 5, emptyRootHash(), newRoot, [])).toBe(true);
  });

  test('m === n consistent only when both roots match and proof empty', () => {
    const log = buildLog(5);
    const root = log.rootHash();
    expect(verifyConsistencyProof(5, 5, root, root, [])).toBe(true);
    const wrong = new Uint8Array(root);
    wrong[0] ^= 0xff;
    expect(verifyConsistencyProof(5, 5, wrong, root, [])).toBe(false);
  });

  test('valid proof verifies for every (m, n) up to 16', () => {
    for (let n = 1; n <= 16; n++) {
      const newLog = buildLog(n);
      const newRoot = newLog.rootHash();
      for (let m = 0; m <= n; m++) {
        const oldLog = buildLog(m);
        const oldRoot = oldLog.rootHash();
        const proof = newLog.consistencyProof(m);
        expect(verifyConsistencyProof(m, n, oldRoot, newRoot, proof)).toBe(true);
      }
    }
  });

  test('fork detection — re-write history fails consistency', () => {
    const original = buildLog(5);
    const oldRoot = original.rootHash();

    // Server "rewrites" leaf 2: build a new log where leaf 2 has different data.
    const tampered = new MerkleLog();
    for (let i = 0; i < 5; i++) {
      tampered.appendData(
        i === 2 ? new Uint8Array([0x99, 0x99]) : new Uint8Array([i & 0xff, (i >> 8) & 0xff]),
      );
    }
    tampered.appendData(new Uint8Array([0x77]));
    const tamperedRoot = tampered.rootHash();
    const proof = tampered.consistencyProof(5);

    expect(verifyConsistencyProof(5, 6, oldRoot, tamperedRoot, proof)).toBe(false);
  });
});

describe('mth helper', () => {
  test('mth over slice == sub-tree root', () => {
    const log = buildLog(4);
    const leaves = log.exportLeaves();
    const left = mth(leaves, 0, 2);
    const right = mth(leaves, 2, 4);
    const root = mth(leaves, 0, 4);
    expect(root).toEqual(nodeHash(left, right));
  });
});

describe('Direct auditPath helper (tree size 1)', () => {
  test('singleton tree audit path is empty', () => {
    const log = buildLog(1);
    const path = log.auditPath(0);
    expect(path.length).toBe(0);
    expect(consistencyProof([log.leafHashAt(0)], 1, 1)).toEqual([]);
  });

  test('auditPath out-of-range throws', () => {
    expect(() => auditPath([], 0, 0)).toThrow();
    const log = buildLog(3);
    expect(() => log.auditPath(3)).toThrow();
  });
});
