import { afterEach, describe, expect, test } from "bun:test";
import { v } from "@wrnexus/validation";
import { defineService, procedure } from "../src/contract.ts";
import { RPC_ERROR_CODES } from "../src/errors.ts";
import { exportSubjectContext } from "../src/identity.ts";
import { implement } from "../src/server.ts";

const originalRpcSecret = process.env.WRNEXUS_RPC_SECRET;
const originalAppName = process.env.WRNEXUS_APP_NAME;
afterEach(() => {
  if (originalRpcSecret === undefined) delete process.env.WRNEXUS_RPC_SECRET;
  else process.env.WRNEXUS_RPC_SECRET = originalRpcSecret;
  if (originalAppName === undefined) delete process.env.WRNEXUS_APP_NAME;
  else process.env.WRNEXUS_APP_NAME = originalAppName;
});

function configure(appName = "web") {
  process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long";
  process.env.WRNEXUS_APP_NAME = appName;
}

const openContract = defineService({
  name: "demo",
  procedures: {
    add: procedure
      .input(v.object({ a: v.number() }))
      .output<{ a: number }>()
      .build(),
  },
});

const guardedContract = defineService({
  name: "billing",
  procedures: {
    createInvoice: procedure
      .input(v.object({ amountCents: v.number() }))
      .output<{ invoiceId: string }>()
      .permission("invoice:create")
      .build(),
  },
});

describe("implement()", () => {
  test("invokes with validated input", async () => {
    const service = implement(
      openContract,
      { add: async ({ a }) => ({ a: a + 1 }) },
      { selfApp: "demo" },
    );
    const result = await service.invoke("add", { a: 1 });
    expect(result).toEqual({ ok: true, value: { a: 2 } });
  });

  test("coerces through the schema", async () => {
    let received: unknown;
    const coercing = defineService({
      name: "demo2",
      procedures: {
        add: procedure
          .input(v.object({ a: v.number() }))
          .output<{ a: number }>()
          .build(),
      },
    });
    const service = implement(
      coercing,
      {
        add: async (input) => {
          received = input;
          return { a: (input as { a: number }).a };
        },
      },
      { selfApp: "demo2" },
    );
    await service.invoke("add", { a: "3" });
    expect(received).toEqual({ a: 3 });
  });

  test("rejects schema-invalid input without invoking the handler", async () => {
    let called = false;
    const service = implement(
      openContract,
      {
        add: async ({ a }) => {
          called = true;
          return { a };
        },
      },
      { selfApp: "demo" },
    );
    const result = await service.invoke("add", { a: "not-a-number" });
    expect(called).toBe(false);
    expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.invalid });
  });

  test("unknown procedure refused", async () => {
    const service = implement(openContract, { add: async ({ a }) => ({ a }) }, { selfApp: "demo" });
    const result = await service.invoke("subtract", {});
    expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.unknown });
  });

  test("handler throw becomes opaque; a secret in the message does not survive", async () => {
    const secret = "sk-super-secret-db-password";
    const service = implement(
      openContract,
      {
        add: async () => {
          throw new Error(`db error using ${secret}`);
        },
      },
      { selfApp: "demo" },
    );
    const result = await service.invoke("add", { a: 1 });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.code).toBe(RPC_ERROR_CODES.handler);
      expect(result.message).not.toContain(secret);
    }
  });

  test("a declared permission is enforced before the handler runs", async () => {
    let called = false;
    const service = implement(
      guardedContract,
      {
        createInvoice: async ({ amountCents }) => {
          called = true;
          return { invoiceId: `inv_${amountCents}` };
        },
      },
      { selfApp: "billing", checkPermission: async () => false },
    );
    const result = await service.invoke("createInvoice", { amountCents: 5 });
    expect(called).toBe(false);
    expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.denied });
  });

  test("a permission check that throws denies", async () => {
    let called = false;
    const service = implement(
      guardedContract,
      {
        createInvoice: async ({ amountCents }) => {
          called = true;
          return { invoiceId: `inv_${amountCents}` };
        },
      },
      {
        selfApp: "billing",
        checkPermission: async () => {
          throw new Error("permission store unavailable");
        },
      },
    );
    const result = await service.invoke("createInvoice", { amountCents: 5 });
    expect(called).toBe(false);
    expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.denied });
  });

  test("a declared permission with no checkPermission configured denies (fail closed)", async () => {
    let called = false;
    const service = implement(
      guardedContract,
      {
        createInvoice: async ({ amountCents }) => {
          called = true;
          return { invoiceId: `inv_${amountCents}` };
        },
      },
      { selfApp: "billing" },
    );
    const result = await service.invoke("createInvoice", { amountCents: 5 });
    expect(called).toBe(false);
    expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.denied });
  });

  test("a valid identity token reaches the handler as a subject", async () => {
    configure("web");
    const token = await exportSubjectContext(
      { user: { id: "u1" }, locals: {} } as never,
      "billing",
    );
    const service = implement(
      guardedContract,
      {
        createInvoice: async ({ amountCents }, ctx) => ({
          invoiceId: `${ctx.subject?.subjectId}_${amountCents}`,
        }),
      },
      { selfApp: "billing", checkPermission: async () => true },
    );
    const result = await service.invoke("createInvoice", { amountCents: 5 }, token);
    expect(result).toEqual({ ok: true, value: { invoiceId: "u1_5" } });
  });

  test("a bad identity token is refused rather than downgraded to anonymous", async () => {
    configure("web");
    let called = false;
    const service = implement(
      openContract,
      {
        add: async ({ a }, ctx) => {
          called = true;
          expect(ctx.subject).toBeUndefined();
          return { a };
        },
      },
      { selfApp: "demo" },
    );
    const result = await service.invoke("add", { a: 1 }, "garbage-token");
    expect(called).toBe(false);
    expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.identity });
  });

  describe("C1: prototype-chain procedure names cannot bypass the permission gate", () => {
    const PROTO_NAMES = [
      "constructor",
      "toString",
      "valueOf",
      "hasOwnProperty",
      "__proto__",
      "isPrototypeOf",
    ];

    for (const name of PROTO_NAMES) {
      test(`"${name}" resolves as unknown, not as a handler`, async () => {
        let permissionChecked = false;
        const service = implement(
          guardedContract,
          {
            createInvoice: async ({ amountCents }) => ({ invoiceId: `inv_${amountCents}` }),
          },
          {
            selfApp: "billing",
            checkPermission: async () => {
              permissionChecked = true;
              return false;
            },
          },
        );
        const result = await service.invoke(name, { amountCents: 1 });
        expect(result).toMatchObject({ ok: false, code: RPC_ERROR_CODES.unknown });
        expect(permissionChecked).toBe(false);
      });
    }
  });
});
