import { describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
  EvidenceTrustSchema,
  signEvidenceForTest,
  verifyEvidenceEnvelope,
  type EvidencePayload,
  type EvidenceTrust,
} from "./evidence";
import { ControllerStore, DeliveryEvidenceAdmissionService } from "./store";

const now = Date.parse("2026-08-11T00:00:00.000Z");
const sha = "a".repeat(40);
const tree = "b".repeat(40);
const digest = "c".repeat(64);
const definitionDigest = "d".repeat(64);

async function fixture(): Promise<{
  pair: CryptoKeyPair;
  trust: EvidenceTrust;
  payload: EvidencePayload;
}> {
  const pair = await crypto.subtle.generateKey(
    "Ed25519",
    true,
    ["sign", "verify"],
  ) as CryptoKeyPair;
  const publicKey = Buffer.from(
    await crypto.subtle.exportKey("raw", pair.publicKey),
  ).toString("base64url");
  const trust = EvidenceTrustSchema.parse({
    authorityToken: "dedicated",
    keys: [{
      keyId: "key-1",
      publicKey,
      issuer: "issuer",
      audience: "controller",
      repository: "overdeck",
      workflow: "deploy",
    }],
  });
  const payload: EvidencePayload = {
    attestationId: "attest-1",
    keyId: "key-1",
    issuer: "issuer",
    audience: "controller",
    repository: "overdeck",
    workflow: "deploy",
    featureId: "dark-delivery",
    definitionDigest,
    targetId: "controller",
    candidateSha: sha,
    candidateTree: tree,
    deployedSha: sha,
    deployedTree: tree,
    artifactDigest: digest,
    result: "PASSED",
    issuedAt: "2026-08-10T23:59:00.000Z",
    expiresAt: "2026-08-11T00:04:00.000Z",
    deployment: {
      deploymentId: "deploy-1",
      status: "ACTIVE",
      observedAt: "2026-08-10T23:59:30.000Z",
      evidenceReference: "deploy",
    },
    installedProof: {
      proofId: "installed-proof",
      entrypoint: "overdeck-controller.service",
      result: "PASSED",
      observedAt: "2026-08-10T23:59:30.000Z",
      evidenceReference: "installed",
    },
    receipts: [
      {
        receiptId: "acceptance",
        kind: "ACCEPTANCE",
        checkId: "accept",
        result: "PASSED",
        observedAt: "2026-08-10T23:59:30.000Z",
        evidenceReference: "accept",
      },
      {
        receiptId: "smoke",
        kind: "SMOKE",
        checkId: "smoke",
        result: "PASSED",
        observedAt: "2026-08-10T23:59:30.000Z",
        evidenceReference: "smoke",
      },
    ],
  };
  return { pair, trust, payload };
}

async function envelope(
  payload: EvidencePayload,
  privateKey: CryptoKey,
): Promise<{ payload: EvidencePayload; signature: string }> {
  return {
    payload,
    signature: await signEvidenceForTest(payload, privateKey),
  };
}

describe("delivery evidence attestation", () => {
  test("admits an exact valid Ed25519 envelope", async () => {
    const { pair, trust, payload } = await fixture();
    await expect(verifyEvidenceEnvelope(
      await envelope(payload, pair.privateKey),
      trust,
      now,
      { deploymentId: "deploy-1", targetId: "controller", deployedSha: sha, deployedTree: tree, artifactDigest: digest },
    )).resolves.toMatchObject({
      attestationId: "attest-1",
      deployment: { deploymentId: "deploy-1" },
      installedProof: {
        deploymentId: "deploy-1",
        deployedTree: tree,
        entrypoint: "overdeck-controller.service",
      },
    });
  });

  test("rejects every signed-payload identity tamper", async () => {
    const { pair, trust, payload } = await fixture();
    const signed = await envelope(payload, pair.privateKey);
    const changes: Array<Partial<EvidencePayload>> = [
      { issuer: "bad" },
      { audience: "bad" },
      { repository: "bad" },
      { workflow: "bad" },
      { featureId: "other" },
      { targetId: "other" },
      { candidateSha: "e".repeat(40) },
      { candidateTree: "e".repeat(40) },
      { deployedSha: "e".repeat(40) },
      { deployedTree: "e".repeat(40) },
      { artifactDigest: "e".repeat(64) },
    ];
    for (const change of changes) {
      await expect(verifyEvidenceEnvelope(
        { ...signed, payload: { ...signed.payload, ...change } },
        trust,
        now,
      )).rejects.toThrow();
    }
    await expect(verifyEvidenceEnvelope({
      ...signed,
      payload: { ...signed.payload, result: "FAILED" },
    }, trust, now)).rejects.toThrow();
  });

  test("rejects a deployment that does not exactly match its candidate", async () => {
    const { pair, trust, payload } = await fixture();
    const mismatches: Array<Partial<EvidencePayload>> = [
      { deployedSha: "e".repeat(40) },
      { deployedTree: "e".repeat(40) },
    ];
    for (const mismatch of mismatches) {
      const changed = { ...payload, ...mismatch };
      await expect(verifyEvidenceEnvelope(
        await envelope(changed, pair.privateKey),
        trust,
        now,
        { deploymentId: "deploy-1", targetId: "controller", deployedSha: sha, deployedTree: tree, artifactDigest: digest },
      )).rejects.toThrow(/deployment identity does not match candidate/);
    }
  });

  test("rejects malformed signatures and public keys", async () => {
    const { pair, trust, payload } = await fixture();
    const signed = await envelope(payload, pair.privateKey);
    for (const signature of ["bad", `${signed.signature}=`, "_"]) {
      await expect(verifyEvidenceEnvelope(
        { ...signed, signature },
        trust,
        now,
      )).rejects.toThrow();
    }
    await expect(verifyEvidenceEnvelope(signed, {
      ...trust,
      keys: [{ ...trust.keys[0]!, publicKey: "bad" }],
    }, now)).rejects.toThrow();
  });

  test("rejects unknown keys and invalid attestation windows", async () => {
    const { pair, trust, payload } = await fixture();
    const changes: Array<Partial<EvidencePayload>> = [
      { keyId: "unknown" },
      { expiresAt: "2026-08-10T23:59:59.000Z" },
      { issuedAt: "2026-08-11T00:00:01.000Z" },
      { expiresAt: "2026-08-11T00:10:00.000Z" },
      { expiresAt: "2026-08-10T23:58:00.000Z" },
    ];
    for (const change of changes) {
      const changed = { ...payload, ...change };
      await expect(verifyEvidenceEnvelope(
        await envelope(changed, pair.privateKey),
        trust,
        now,
      )).rejects.toThrow();
    }
  });

  test("rejects observations outside the signed time window", async () => {
    const { pair, trust, payload } = await fixture();
    const invalidPayloads: EvidencePayload[] = [
      {
        ...payload,
        deployment: {
          ...payload.deployment,
          observedAt: "2026-08-10T23:58:59.000Z",
        },
      },
      {
        ...payload,
        receipts: [{
          ...payload.receipts[0]!,
          observedAt: "2026-08-11T00:04:01.000Z",
        }],
      },
    ];
    for (const invalid of invalidPayloads) {
      await expect(verifyEvidenceEnvelope(
        await envelope(invalid, pair.privateKey),
        trust,
        now,
      )).rejects.toThrow(/observation time/);
    }
  });

  test("rejects replay and rolls back a failed persistence marker", async () => {
    const { pair, trust, payload } = await fixture();
    const dir = mkdtempSync(join(tmpdir(), "evidence-"));
    const store = new ControllerStore(join(dir, "state.sqlite"));
    try {
      const verified = await verifyEvidenceEnvelope(
        await envelope(payload, pair.privateKey),
        trust,
        now,
        { deploymentId: "deploy-1", targetId: "controller", deployedSha: sha, deployedTree: tree, artifactDigest: digest },
      );
      const service = new DeliveryEvidenceAdmissionService(store);
      const bad = {
        ...verified.deployment,
        artifactDigest: "e".repeat(64),
      };
      expect(() => service.admit(
        bad,
        verified.receipts,
        "rollback-id",
      )).toThrow();
      service.admit(
        verified.deployment,
        verified.receipts,
        "rollback-id",
      );
      expect(() => service.admit(
        verified.deployment,
        verified.receipts,
        "rollback-id",
      )).toThrow(/replay/);
    } finally {
      store.close();
      rmSync(dir, { recursive: true, force: true });
    }
  });
});
