import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import generatedRegistry from "../../generated/route-registry.json" assert { type: "json" };
import generatedOpenApi from "../../generated/openapi.json" assert { type: "json" };
import {
  RedisX402ReceiptStore,
  hasValidReceiptAuditIntegrity,
  type ReceiptChainObservation,
  type StoredX402Receipt,
  type V3ReceiptRequirement,
  type X402ReceiptBinding,
} from "../../../repos/theloopbreaker-site/app/api/v3/x402/_lib/receipt-store";
import { createTestReceiptCapabilities, createTestReceiptRedis } from "../../../repos/theloopbreaker-site/app/api/v3/x402/_lib/test-receipt-adapter";
import { assertV3RouteRegistry, getV3Route, V3_ROUTES } from "../../../repos/theloopbreaker-site/app/api/v3/x402/_lib/registry";
import {
  canonicalEvidencePayload,
  paymentRequirementDigest,
  type V3CapabilityAuthorization,
} from "../../../repos/theloopbreaker-site/app/api/v3/x402/_lib/capabilities";
import { assertV3DiscoveryDocument, createV3DiscoveryDocument } from "../../../repos/theloopbreaker-site/app/api/v3/x402/discovery/route";
import { isValidV3RequestId } from "../../../repos/theloopbreaker-site/app/api/v3/_lib/validation";
import { assertV3ReceiptStatusRelationships } from "../../../repos/theloopbreaker-site/app/api/v3/x402/_lib/receipt-status";
import { validateSchema } from "./json-schema-2020-subset";

const START = 1_800_000_000_000;
const ASSET = "0x1111111111111111111111111111111111111111";
const PAYEE = "0x2222222222222222222222222222222222222222";
const HASH_A = `0x${"aa".repeat(32)}`;

function requirement(finality: "soft" | "safe" | "finalized" = "safe"): V3ReceiptRequirement {
  return {
    id: "mandate-compat",
    route: "/api/v3/mandate/:mandateHash",
    method: "GET",
    network: "eip155:8453",
    chainId: 8453,
    asset: ASSET,
    amount: "5000",
    scheme: "exact",
    payee: PAYEE,
    deploymentAddress: "0x3333333333333333333333333333333333333333",
    deploymentCodeDigest: "ab".repeat(32),
    requiredFinality: finality,
    minConfirmations: 5,
  };
}

function binding(finality: "soft" | "safe" | "finalized" = "safe"): X402ReceiptBinding {
  return {
    schemaVersion: "1.0.0",
    requestId: finality === "soft" ? "v3r_0123456789abcdef0123456789abcdef" :
      finality === "safe" ? "v3r_123456789abcdef0123456789abcdef0" : "v3r_23456789abcdef0123456789abcdef01",
    paymentId: `payment-${finality}-0123456789`,
    authorizationId: `authorization-${finality}-0123456789`,
    routeId: "mandate-compat",
    route: "/api/v3/mandate/:mandateHash",
    resource: `/api/v3/mandate/${HASH_A}`,
    method: "GET",
    network: "eip155:8453",
    chainId: 8453,
    asset: ASSET,
    amount: "5000",
    scheme: "exact",
    payee: PAYEE,
    nonce: `nonce-${finality}-0123456789`,
    validAfter: START - 1,
    expiresAt: START + 60_000,
    requiredFinality: finality,
    minConfirmations: 5,
  };
}

function observation(overrides: Partial<ReceiptChainObservation> = {}): ReceiptChainObservation {
  return {
    network: "eip155:8453",
    chainId: 8453,
    txHash: `0x${"33".repeat(32)}`,
    blockNumber: 100,
    blockHash: `0x${"44".repeat(32)}`,
    headBlockNumber: 104,
    observedFinality: "safe",
    observedAt: START,
    canonicalBlock: true,
    ...overrides,
  };
}

function newStore(finality: "soft" | "safe" | "finalized" = "safe", clock = () => START) {
  const configured = requirement(finality);
  const bound = binding(finality);
  return new RedisX402ReceiptStore(createTestReceiptRedis({ NODE_ENV: "test" }), {
    now: clock,
    requirements: [configured],
    ...createTestReceiptCapabilities(START, configured, bound.resource, bound.validAfter, bound.expiresAt),
  });
}

describe("V3 x402 adversarial audit regressions", () => {
  it("keeps generated and website registry security semantics identical", () => {
    const fields = ["id", "method", "path", "free", "finality", "dataSource", "privacy", "responseSchema", "idempotencyRule"] as const;
    const generated = generatedRegistry.routes.filter((route) => route.version === "v3");
    expect(generated).toHaveLength(V3_ROUTES.length);
    for (const siteRoute of V3_ROUTES) {
      const sourceRoute = generated.find((route) => route.method === siteRoute.method && route.path === siteRoute.path);
      expect(sourceRoute, siteRoute.id).toBeDefined();
      expect(Object.fromEntries(fields.map((field) => [field, sourceRoute?.[field]])), siteRoute.id)
        .toEqual(Object.fromEntries(fields.map((field) => [field, siteRoute[field]])));
      const openApiPath = siteRoute.path.replace(/:([A-Za-z][A-Za-z0-9]*)/g, "{$1}");
      const operation = (generatedOpenApi.paths as Record<string, Record<string, { operationId?: string }>>)[openApiPath]?.[siteRoute.method.toLowerCase()];
      expect(operation?.operationId, `OpenAPI ${siteRoute.method} ${siteRoute.path}`).toBe(siteRoute.id);
      expect(getV3Route(siteRoute.id)).toMatchObject({ id: siteRoute.id, method: siteRoute.method, path: siteRoute.path });
    }
    const duplicateId = V3_ROUTES.map((route) => ({ ...route }));
    duplicateId[1].id = duplicateId[0].id;
    expect(() => assertV3RouteRegistry(duplicateId)).toThrow(/invalid V3 route|duplicate/);
  });
  it("rejects unrelated templates/resources, arbitrary methods, malformed identifiers, query tricks, and noncanonical addresses", async () => {
    const mutations: Array<[string, (candidate: any) => void]> = [
      ["route", (x) => { x.route = "/not-the-resource/:id"; }],
      ["resource", (x) => { x.resource = "/api/v3/unrelated/concrete"; }],
      ["method", (x) => { x.method = "BREW"; }],
      ["lowercase method", (x) => { x.method = "get"; }],
      ["query", (x) => { x.resource += "?admin=true"; }],
      ["fragment", (x) => { x.resource += "#fragment"; }],
      ["encoded slash", (x) => { x.resource = "/api/v3/mandate/0x12%2f34"; }],
      ["dot segment", (x) => { x.resource = `/api/v3/mandate/../mandate/${HASH_A}`; }],
      ["asset", (x) => { x.asset = "not-an-address"; }],
      ["mixed-case asset", (x) => { x.asset = "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; }],
      ["payee", (x) => { x.payee = "also-not-an-address"; }],
      ["network", (x) => { x.network = "eip155:08453"; }],
      ["chain", (x) => { x.chainId = 1; }],
      ["price", (x) => { x.amount = "05000"; }],
      ["short request", (x) => { x.requestId = "req-a"; }],
      ["repeated request", (x) => { x.requestId = `v3r_${"a".repeat(32)}`; }],
    ];
    for (const [name, mutate] of mutations) {
      const candidate: any = { ...binding() };
      mutate(candidate);
      await expect(newStore().claim(candidate), name).rejects.toThrow();
    }
  });

  it("requires one canonical route-bound authorization bundle and rejects coherent-but-unrelated mixing", async () => {
    expect(() => new RedisX402ReceiptStore(createTestReceiptRedis({ NODE_ENV: "test" }), {
      requirements: [{ ...requirement(), id: "evil-route", route: "/api/v3/evil/:requestId", method: "POST" }],
    })).toThrow(/canonical V3 route registry/);

    const configured = requirement();
    const bound = binding();
    const authorized = createTestReceiptCapabilities(START, configured, bound.resource, bound.validAfter, bound.expiresAt);
    for (const [name, mutate] of [
      ["route", (x: V3CapabilityAuthorization) => { x.routeId = "mandate"; x.routeTemplate = "/api/v3/mandates/:mandateHash"; }],
      ["resource", (x: V3CapabilityAuthorization) => { x.concreteResource = `/api/v3/mandate/0x${"99".repeat(32)}`; }],
      ["network", (x: V3CapabilityAuthorization) => { x.network = "eip155:1"; x.chainId = 1; }],
      ["asset", (x: V3CapabilityAuthorization) => { x.asset = "0x4444444444444444444444444444444444444444"; }],
      ["payee", (x: V3CapabilityAuthorization) => { x.payee = "0x5555555555555555555555555555555555555555"; }],
      ["deployment", (x: V3CapabilityAuthorization) => { x.deploymentAddress = "0x6666666666666666666666666666666666666666"; }],
      ["code", (x: V3CapabilityAuthorization) => { x.deploymentCodeDigest = "cd".repeat(32); }],
      ["price", (x: V3CapabilityAuthorization) => { x.amount = "1"; }],
    ] as const) {
      const mixed = structuredClone(authorized.capabilityEvidence);
      if (!mixed.bundle) throw new Error("missing bundle");
      mutate(mixed.bundle);
      mixed.bundle.paymentRequirementDigest = paymentRequirementDigest(mixed.bundle);
      mixed.bundle.bundleDigest = createHash("sha256").update(canonicalEvidencePayload(mixed.bundle)).digest("hex");
      const store = new RedisX402ReceiptStore(createTestReceiptRedis({ NODE_ENV: "test" }), {
        now: () => START,
        requirements: [configured],
        capabilityEvidence: mixed,
        evidenceVerifier: (candidate) => Object.freeze({ ...candidate }),
      });
      await expect(store.claim(bound), name).rejects.toThrow(/authenticated capability evidence/);
    }
  });

  it("enforces semantic uniqueness, strict RFC3339 date-time, route identities, and nonrepeating 128-bit locators", () => {
    const objectSchema = {
      type: "array", uniqueItems: true,
      items: { type: "object", additionalProperties: false, required: ["a", "b"], properties: { a: { type: "integer" }, b: { type: "integer" } } },
    };
    expect(validateSchema(objectSchema, [{ a: 1, b: 2 }, { b: 2, a: 1 }])).not.toEqual([]);
    expect(validateSchema({ type: "string", format: "date-time" }, "2026-08-25")).not.toEqual([]);
    expect(validateSchema({ type: "string", format: "date-time" }, "2026-08-25T17:19:00Z")).toEqual([]);
    expect(isValidV3RequestId(`v3r_${"a".repeat(32)}`)).toBe(false);
    expect(isValidV3RequestId("v3r_89abcdef0123456789abcdef01234567")).toBe(true);

    const duplicateId = structuredClone(createV3DiscoveryDocument());
    duplicateId.endpoints[1].id = duplicateId.endpoints[0].id;
    expect(() => assertV3DiscoveryDocument(duplicateId)).toThrow(/invalid V3 route/);
    const duplicateIdentity = structuredClone(createV3DiscoveryDocument());
    duplicateIdentity.endpoints[1].method = duplicateIdentity.endpoints[0].method;
    duplicateIdentity.endpoints[1].path = duplicateIdentity.endpoints[0].path;
    expect(() => assertV3DiscoveryDocument(duplicateIdentity)).toThrow(/duplicate V3 route/);
  });

  it("enforces observation schema, chain, canonicality, time bounds, depth, and required finality", async () => {
    for (const candidate of [
      observation({ observedFinality: "garbage" as any }),
      observation({ observedAt: START + 1 }),
      observation({ observedAt: START - 300_001 }),
      observation({ network: "eip155:1" }),
      observation({ chainId: 1 }),
      { ...observation(), canonicalBlock: false as any },
      observation({ txHash: `0x${"AA".repeat(32)}` }),
    ]) {
      const store = newStore();
      await store.claim(binding());
      await expect(store.reconcile(binding().requestId, candidate)).rejects.toThrow(/invalid/);
    }

    const safe = newStore();
    await safe.claim(binding());
    expect(await safe.reconcile(binding().requestId, observation({ headBlockNumber: 102 })))
      .toMatchObject({ outcome: "updated", receipt: { state: "pending_confirmation" } });

    const finalized = newStore("finalized");
    await finalized.claim(binding("finalized"));
    expect(await finalized.reconcile(binding("finalized").requestId, observation()))
      .toMatchObject({ outcome: "updated", receipt: { state: "pending_confirmation" } });
    expect(await finalized.reconcile(binding("finalized").requestId, observation({ observedFinality: "finalized" })))
      .toMatchObject({ outcome: "updated", receipt: { state: "finalized" } });
  });

  it("atomically expires reconciliation, prevents manual settlement transitions, and is idempotent", async () => {
    let now = START;
    const store = newStore("safe", () => now);
    const claimed = await store.claim(binding());
    expect(claimed.outcome).toBe("claimed");
    expect(await store.transition(binding().requestId, ["claimed"], "confirmed"))
      .toMatchObject({ outcome: "conflict", code: "state_conflict" });
    const first = await store.reconcile(binding().requestId, observation());
    expect(first).toMatchObject({ outcome: "updated", receipt: { state: "confirmed", version: 2 } });
    const repeat = await store.reconcile(binding().requestId, observation());
    expect(repeat).toMatchObject({ outcome: "existing", receipt: { state: "confirmed", version: 2 } });

    const expiring = newStore("safe", () => now);
    const expiringBinding = { ...binding(), requestId: "v3r_3456789abcdef0123456789abcdef012", paymentId: "payment-expiry-0123456789",
      authorizationId: "authorization-expiry-0123456789", nonce: "nonce-expiry-0123456789" };
    await expiring.claim(expiringBinding);
    now = expiringBinding.expiresAt;
    expect(await expiring.reconcile(expiringBinding.requestId, observation({ observedAt: START })))
      .toMatchObject({ outcome: "updated", receipt: { state: "expired", failureCode: "timeout" } });
  });

  it("verifies every persisted audit event and every material binding/evidence field", async () => {
    const store = newStore();
    await store.claim(binding());
    await store.reconcile(binding().requestId, observation());
    const receipt = await store.get(binding().requestId);
    if (!receipt) throw new Error("missing receipt");
    expect(hasValidReceiptAuditIntegrity(receipt)).toBe(true);
    expect(() => assertV3ReceiptStatusRelationships(receipt)).not.toThrow();
    const crossChainPublicReceipt = structuredClone(receipt);
    if (!crossChainPublicReceipt.observation) throw new Error("missing observation");
    crossChainPublicReceipt.observation.network = "eip155:1";
    crossChainPublicReceipt.observation.chainId = 1;
    expect(() => assertV3ReceiptStatusRelationships(crossChainPublicReceipt)).toThrow(/does not match/);

    const mutations: Array<[string, (candidate: any) => void]> = [
      ["binding payee", (x) => { x.binding.payee = "0x9999999999999999999999999999999999999999"; }],
      ["binding amount", (x) => { x.binding.amount = "1"; }],
      ["binding resource", (x) => { x.binding.resource = `/api/v3/mandate/0x${"bb".repeat(32)}`; }],
      ["block hash", (x) => { x.observation.blockHash = `0x${"88".repeat(32)}`; }],
      ["block number", (x) => { x.observation.blockNumber += 1; }],
      ["head", (x) => { x.observation.headBlockNumber += 1; }],
      ["finality", (x) => { x.observation.observedFinality = "finalized"; }],
      ["observed time", (x) => { x.observation.observedAt += 1; }],
      ["created time", (x) => { x.createdAt += 1; }],
      ["updated time", (x) => { x.updatedAt += 1; }],
      ["failure code", (x) => { x.failureCode = "verification_failed"; }],
      ["omit event", (x) => { x.auditHistory.shift(); }],
      ["reorder events", (x) => { x.auditHistory.reverse(); }],
      ["event payload", (x) => { x.auditHistory[0].snapshot.state = "failed"; }],
      ["event chronology", (x) => { x.auditHistory[1].occurredAt = x.auditHistory[0].occurredAt - 1; }],
      ["event type", (x) => { x.auditHistory[1].event = "receipt_claimed"; }],
    ];
    for (const [name, mutate] of mutations) {
      const candidate = structuredClone(receipt) as StoredX402Receipt;
      mutate(candidate);
      expect(hasValidReceiptAuditIntegrity(candidate), name).toBe(false);
    }
  });
});
