import { describe, expect, it } from "vitest";
import { createReadOnlyProxy } from "../lib/read-only-prisma.js";

/**
 * Ticket #47 — read-only proxy must survive the scoped-caller indirection.
 *
 * assistant-tools.ts::executeTool swaps `ctx.db` for a read-only proxy when
 * dispatching non-mutation tools. Tool executors then call
 * `createScopedCallerContext(ctx)` which forwards `ctx.db` to a tRPC caller.
 * If the proxy were not preserved through that forwarding, an LLM-invoked
 * "read" tool could smuggle writes via the caller path.
 *
 * This suite asserts the proxy is not unwrapped on forwarding, and that
 * every write-flavoured client method (model writes, raw SQL, interactive
 * transactions, runCommandRaw) is still blocked after forwarding.
 */
describe("read-only proxy survives scoped-caller forwarding (#47)", () => {
  function makeFakeClient() {
    // Minimal shape that passes the Proxy's model detection (has findMany).
    const user = {
      findUnique: async () => ({ id: "u1" }),
      findMany: async () => [],
      create: async () => ({ id: "u1" }),
      update: async () => ({ id: "u1" }),
    };
    return {
      user,
      $queryRaw: async () => [],
      $queryRawUnsafe: async () => [],
      $executeRaw: async () => 0,
      $executeRawUnsafe: async () => 0,
      $transaction: async () => [],
      $runCommandRaw: async () => ({ ok: 1 }),
    };
  }

  // Simulate what createScopedCallerContext does: construct a NEW object
  // whose `db` key is assigned from the incoming ctx.db. This is the exact
  // forwarding pattern used by helpers.ts::createScopedCallerContext.
  function forwardToCaller(ctx: { db: unknown }): { db: unknown } {
    return { db: ctx.db };
  }

  it("ctx.db retains proxy identity after forwarding", () => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const client = makeFakeClient() as any;
    const proxied = createReadOnlyProxy(client);
    const forwarded = forwardToCaller({ db: proxied });
    // Writes through the forwarded db must still throw.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect(() => (forwarded.db as any).user.create({ data: {} })).toThrow(
      /not permitted on read-only/,
    );
  });

  it("raw/tx escape hatches still blocked after forwarding", () => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const client = makeFakeClient() as any;
    const proxied = createReadOnlyProxy(client);
    const forwarded = forwardToCaller({ db: proxied }) as { db: Record<string, Function> };

    expect(() => forwarded.db.$executeRaw!`DELETE FROM users`).toThrow(
      /Raw\/escape operation "\$executeRaw" not permitted/,
    );
    expect(() => forwarded.db.$executeRawUnsafe!("DELETE FROM users")).toThrow(
      /Raw\/escape operation "\$executeRawUnsafe" not permitted/,
    );
    expect(() => forwarded.db.$queryRawUnsafe!("SELECT 1")).toThrow(
      /Raw\/escape operation "\$queryRawUnsafe" not permitted/,
    );
    expect(() => forwarded.db.$transaction!([])).toThrow(
      /Raw\/escape operation "\$transaction" not permitted/,
    );
    expect(() => forwarded.db.$runCommandRaw!({})).toThrow(
      /Raw\/escape operation "\$runCommandRaw" not permitted/,
    );
  });

  it("reads still succeed after forwarding (positive control)", async () => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const client = makeFakeClient() as any;
    const proxied = createReadOnlyProxy(client);
    const forwarded = forwardToCaller({ db: proxied }) as {
      db: { user: { findUnique: (a: unknown) => Promise<unknown> } };
    };

    await expect(forwarded.db.user.findUnique({ where: { id: "u1" } })).resolves.toEqual({
      id: "u1",
    });
  });
});
