import { describe, expect, test } from "bun:test";
import { v } from "@wrnexus/validation";
import { defineService, procedure } from "../src/contract.ts";

describe("defineService", () => {
  test("captures a procedure's input schema, permission and idempotency", () => {
    const billing = defineService({
      name: "billing",
      procedures: {
        createInvoice: procedure
          .input(v.object({ userId: v.string(), amountCents: v.number() }))
          .output<{ invoiceId: string }>()
          .permission("invoice:create")
          .build(),
        getInvoice: procedure
          .input(v.object({ invoiceId: v.string() }))
          .output<{ amountCents: number }>()
          .idempotent()
          .build(),
      },
    });

    expect(billing.name).toBe("billing");
    expect(Object.keys(billing.procedures).sort()).toEqual(["createInvoice", "getInvoice"]);
    expect(billing.procedures.createInvoice.permission).toBe("invoice:create");
    expect(billing.procedures.createInvoice.idempotent).toBeUndefined();
    expect(billing.procedures.getInvoice.idempotent).toBe(true);
    expect(billing.procedures.getInvoice.permission).toBeUndefined();
  });

  test("the contract is frozen so it cannot drift after definition", () => {
    const contract = defineService({ name: "demo", procedures: { ping: procedure.build() } });
    expect(Object.isFrozen(contract)).toBe(true);
    expect(Object.isFrozen(contract.procedures)).toBe(true);
  });

  test("rejects a service name that is not a safe path segment", () => {
    // The name lands in a URL path, so it must not need escaping.
    for (const name of ["", "has space", "has/slash", "has.dot", "UPPER"]) {
      expect(() => defineService({ name, procedures: {} })).toThrow(/service name/i);
    }
    expect(() => defineService({ name: "billing-v2", procedures: {} })).not.toThrow();
  });

  test("rejects a procedure name that is not a safe path segment", () => {
    expect(() =>
      defineService({ name: "demo", procedures: { "bad name": procedure.build() } }),
    ).toThrow(/procedure name/i);
  });

  test("a hand-built procedure is frozen too, not just builder output", () => {
    // AnyProcedures accepts any ProcedureDef shape; the guarantee must not
    // depend on the caller having used procedure.build().
    const contract = defineService({
      name: "demo",
      procedures: { ping: { permission: "demo:read" } },
    });
    expect(Object.isFrozen(contract.procedures.ping)).toBe(true);
    expect(() => {
      (contract.procedures.ping as { permission?: string }).permission = "hacked";
    }).toThrow();
    expect(contract.procedures.ping.permission).toBe("demo:read");
  });

  test("the builder is immutable — reusing a base does not cross-contaminate", () => {
    const base = procedure.permission("a:read");
    const one = base.idempotent().build();
    const two = base.build();
    expect(one.idempotent).toBe(true);
    expect(two.idempotent).toBeUndefined();
    expect(two.permission).toBe("a:read");
  });
});
