import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it, vi } from "vitest";
import { defineCommand } from "./defineCommand";
import { InsufficientPermissionError } from "./errors";
import type { CommandContext } from "./types";

describe("defineCommand", () => {
  const ctx: CommandContext = {
    actorId: "user-1",
    permissions: ["mod:doThing"],
  };

  it("calls impl with db and input when permission is present", async () => {
    const impl = vi.fn().mockResolvedValue({ result: "ok" });
    const command = defineCommand("doThing", "mod:doThing", impl)();
    const mock = createKyselyMock();

    let trx: unknown;
    const result = await mock.withTx((db) => {
      trx = db;
      return command(db, { foo: "bar" }, ctx);
    });

    expect(result).toEqual({ result: "ok" });
    expect(impl).toHaveBeenCalledWith(trx, { foo: "bar" }, ctx);
  });

  it("returns InsufficientPermissionError when permission is missing", async () => {
    const impl = vi.fn();
    const command = defineCommand("doThing", "mod:doThing", impl)();
    const denied: CommandContext = { actorId: "user-1", permissions: [] };
    const mock = createKyselyMock();

    const result = await mock.withTx((db) => command(db, {}, denied));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientPermissionError);
    }
    expect(impl).not.toHaveBeenCalled();
  });

  it("passes ctx to impl", async () => {
    const impl = vi.fn().mockResolvedValue({});
    const command = defineCommand("doThing", "mod:doThing", impl)();
    const mock = createKyselyMock();

    await mock.withTx((db) => command(db, { x: 1 }, ctx));

    expect(impl).toHaveBeenCalledTimes(1);
    expect(impl.mock.calls[0]).toHaveLength(3);
    expect(impl.mock.calls[0][2]).toEqual(ctx);
  });

  it("passes deps to impl when provided", async () => {
    const impl = vi.fn().mockResolvedValue({ result: "ok" });
    const deps = { maxDepth: 10 };
    const command = defineCommand("doThing", "mod:doThing", impl)(deps);
    const mock = createKyselyMock();

    let trx: unknown;
    await mock.withTx((db) => {
      trx = db;
      return command(db, { x: 1 }, ctx);
    });

    expect(impl).toHaveBeenCalledWith(trx, { x: 1 }, ctx, deps);
  });

  it("throws when db is not a transaction", async () => {
    const impl = vi.fn().mockResolvedValue({ result: "ok" });
    const command = defineCommand("doThing", "mod:doThing", impl)();
    const nonTransactionDb = { isTransaction: false } as never;

    await expect(command(nonTransactionDb, {}, ctx)).rejects.toThrow(
      "Commands require a transaction database",
    );
    expect(impl).not.toHaveBeenCalled();
  });
});
