/**
 * Known-answer tests for drivuno-crypto.
 *
 * Every vector below was produced by the code in this package from the fixed
 * inputs shown. If your checkout reproduces them, you are running the same
 * cryptography DRIVUNO ships. Randomised operations (fresh nonces, ephemeral
 * keys) are covered by round-trip and interoperability tests instead.
 */
import { describe, it, expect } from "vitest";
import {
  deriveMasterKey,
  symEncryptWithNonce,
  symDecrypt,
  recoveryKeyFromDisplay,
  sharePasswordProof,
  recoveryCodeProof,
  newKeyPair,
  sealForRecipient,
  openSealed,
  streamEncryptBuffer,
  streamDecryptInit,
  STREAM_CIPHER_CHUNK,
  utf8,
  fromUtf8,
} from "./crypto";
import {
  canonicalJson,
  signManifest,
  verifyManifest,
  constantTimeEqual,
  wipeBytes,
  ciphertextHash,
} from "./crypto-signatures";
import { padmeSize, padPlaintext, unpadPlaintext, isPadded } from "./metadata-privacy";
import { computeSha256Hex } from "./integrity";
import { sealForRecipientServer } from "./mail-seal-server";
import _sodium from "libsodium-wrappers-sumo";

const hex = (b: Uint8Array) =>
  Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
const fromHex = (h: string) => {
  const out = new Uint8Array(h.length / 2);
  for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
  return out;
};
const range = (n: number, off = 0) => new Uint8Array(n).map((_, i) => (i + off) & 0xff);

describe("Argon2id master-key derivation", () => {
  it("matches the published known-answer vector", async () => {
    const mk = await deriveMasterKey("correct horse battery staple", {
      salt: range(16),
      opsLimit: 2,
      memLimit: 67_108_864,
    });
    expect(hex(mk)).toBe(
      "c05ce4c4dd7e0e45ee6011cc59d068ade47df1b01fc0cf9cd4678bdf68a5b7b0",
    );
  });
});

describe("XChaCha20-Poly1305 content encryption", () => {
  const key = range(32);
  const nonce = range(24, 0x20);
  const plain = utf8("Hello, zero-knowledge world!");

  it("matches the published known-answer vector", async () => {
    const ct = await symEncryptWithNonce(plain, key, nonce);
    expect(hex(ct)).toBe(
      "553c21a7141b34f15dcc3f9023296ea65130b55bbab371da930d380824f3eb38b1a90181e7a8c3985f47e3fc",
    );
  });

  it("decrypts its own output and rejects a flipped bit", async () => {
    const ct = await symEncryptWithNonce(plain, key, nonce);
    expect(fromUtf8(await symDecrypt(ct, nonce, key))).toBe("Hello, zero-knowledge world!");
    const bad = ct.slice();
    bad[0] ^= 1;
    await expect(symDecrypt(bad, nonce, key)).rejects.toThrow();
  });
});

describe("Streaming AEAD (large files)", () => {
  it("round-trips a multi-chunk buffer with a final tag", async () => {
    const key = range(32, 0x80);
    const plain = new Uint8Array(STREAM_CIPHER_CHUNK * 2 + 123).map((_, i) => i & 0xff);
    const { header, ciphertext } = await streamEncryptBuffer(plain, key);
    const state = await streamDecryptInit(header, key);
    const out = new Uint8Array(plain.length);
    let off = 0;
    let sawFinal = false;
    for (let p = 0; p < ciphertext.length; p += STREAM_CIPHER_CHUNK) {
      const { message, final } = state.pull(ciphertext.subarray(p, p + STREAM_CIPHER_CHUNK));
      out.set(message, off);
      off += message.length;
      sawFinal = final;
    }
    expect(sawFinal).toBe(true);
    expect(hex(out)).toBe(hex(plain));
  });
});

describe("Padmé length padding", () => {
  it("matches the published bucket table", () => {
    const table: Array<[number, number]> = [
      [0, 64], [1, 64], [63, 64], [64, 64], [65, 72], [100, 104],
      [1000, 1024], [65536, 65536], [1_000_000, 1_015_808], [4_888_888, 4_980_736],
    ];
    for (const [n, expected] of table) expect(padmeSize(n)).toBe(expected);
  });

  it("padPlaintext output matches the published vector and unpads exactly", () => {
    const padded = padPlaintext(utf8("abc"));
    expect(padded.length).toBe(64);
    expect(hex(padded)).toBe(
      "44500000000361626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    );
    expect(isPadded(padded)).toBe(true);
    expect(fromUtf8(unpadPlaintext(padded))).toBe("abc");
    // Legacy, unpadded content must pass through untouched.
    const legacy = utf8('{"plain":true}');
    expect(fromUtf8(unpadPlaintext(legacy))).toBe('{"plain":true}');
  });
});

describe("Ed25519 manifest signatures", () => {
  it("canonicalises JSON deterministically", () => {
    expect(canonicalJson({ b: 1, a: { d: [2, 3], c: "x" } })).toBe('{"a":{"c":"x","d":[2,3]},"b":1}');
  });

  it("produces and verifies the published signature vector", async () => {
    const s = await _sodium.ready.then(() => _sodium);
    const kp = s.crypto_sign_seed_keypair(range(32, 0x40));
    expect(hex(kp.publicKey)).toBe(
      "2543b92ff1095511476adc8369db6ddc933665a11978dda1404ee1066ca9559d",
    );
    const manifest = { b: 1, a: { d: [2, 3], c: "x" } };
    const sig = await signManifest(manifest, kp.privateKey);
    expect(hex(sig)).toBe(
      "eb9f9cdeab13210573daf6eec0e05512c22b1166d9d9e7c4e2fb0752691d36fa93058ee413395298349a88a16ae125a8f4c7d6296dd617e0bcd3a56ea9e77d0a",
    );
    expect(await verifyManifest(manifest, sig, kp.publicKey)).toBe(true);
    expect(await verifyManifest({ ...manifest, b: 2 }, sig, kp.publicKey)).toBe(false);
  });
});

describe("Recovery key encoding", () => {
  it("matches the published display vector and round-trips", () => {
    const display = "AAASEA-2EAWDA-QCAKBJ-FS2DJQ-B6JBCE-SVCSLT-NF22DE-PBYHA7-D2RS";
    const raw = recoveryKeyFromDisplay(display);
    expect(hex(raw)).toBe(
      "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
    );
  });

  it("accepts ambiguous characters typed by humans (1/I → L)", () => {
    const raw = recoveryKeyFromDisplay(
      "AAASEA-2EAWDA-QCAKBJ-FS2DJQ-B6JBCE-SVCSLT-NF22DE-PBYHA7-D2RS".toLowerCase(),
    );
    expect(hex(raw)).toBe(
      "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
    );
  });
});

describe("Online proof-of-knowledge schemes", () => {
  it("share-password proof matches the published vector", async () => {
    expect(await sharePasswordProof(range(32))).toBe(
      "b01776a6202dbcd042d8290ffd2757ba2f1ecd5cecff5d5338db0dd0b90b8a61",
    );
  });

  it("recovery-code proof matches the published vector", async () => {
    expect(await recoveryCodeProof(range(32))).toBe(
      "fc2d2b80e7f895355671151cbed8e9a0969363b74b9d4d493e8ca8ea9a9b6e90",
    );
  });

  it("the two proofs are domain-separated", async () => {
    expect(await sharePasswordProof(range(32))).not.toBe(await recoveryCodeProof(range(32)));
  });
});

describe("Ciphertext integrity", () => {
  it("SHA-256 of the literal 'drivuno' matches the published vector", async () => {
    expect(await computeSha256Hex(utf8("drivuno"))).toBe(
      "01b8a20e4311c5b8f82699d718536beee4c85b49c7b5cfa8277416d190df4952",
    );
  });
});

describe("X25519 sealed boxes", () => {
  it("round-trips for the recipient and no one else", async () => {
    const alice = await newKeyPair();
    const eve = await newKeyPair();
    const sealed = await sealForRecipient(utf8("folder key material"), alice.publicKey);
    expect(fromUtf8(await openSealed(sealed, alice.publicKey, alice.privateKey))).toBe(
      "folder key material",
    );
    await expect(openSealed(sealed, eve.publicKey, eve.privateKey)).rejects.toThrow();
  });
});

describe("Edge reference implementation (pure JS)", () => {
  it("seals inbound mail in a format libsodium opens byte-for-byte", async () => {
    const s = await _sodium.ready.then(() => _sodium);
    const kp = s.crypto_box_seed_keypair(range(32, 0x60));
    const sealed = sealForRecipientServer(utf8("interop check"), kp.publicKey);
    const opened = s.crypto_box_seal_open(sealed, kp.publicKey, kp.privateKey);
    expect(new TextDecoder().decode(opened)).toBe("interop check");
  });
});

describe("Memory hygiene & comparison helpers", () => {
  it("constantTimeEqual compares content, not identity", () => {
    expect(constantTimeEqual(range(4), range(4))).toBe(true);
    expect(constantTimeEqual(range(4), range(4, 1))).toBe(false);
    expect(constantTimeEqual(range(4), range(5))).toBe(false);
  });

  it("wipeBytes zero-fills sensitive buffers", () => {
    const buf = range(16, 0xaa);
    wipeBytes(buf);
    expect(hex(buf)).toBe("00".repeat(16));
  });
});

describe("SHA-256 implementations agree", () => {
  // integrity.ts deliberately uses Web Crypto (native, zero-dependency) while
  // crypto-signatures.ts uses libsodium's crypto_hash_sha256. Both are FIPS
  // 180-4 SHA-256; this test makes the agreement an enforced invariant.
  it("Web Crypto and libsodium produce identical digests", async () => {
    const vectors = [
      new Uint8Array(0),
      utf8("drivuno"),
      range(256),
      range(4096, 0x11),
    ];
    for (const v of vectors) {
      const webCryptoHex = await computeSha256Hex(v);
      const libsodiumHex = hex(await ciphertextHash(v));
      expect(webCryptoHex).toBe(libsodiumHex);
    }
    // Known-answer: SHA-256("") per FIPS 180-4.
    expect(await computeSha256Hex(new Uint8Array(0))).toBe(
      "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    );
  });
});
