import { ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it, vi } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AssigneeFlagsIncompatibleError,
  AssigneeUserRoleXorViolatedError,
  CreationModeAmbiguousError,
  DuplicateRoleAssigneeError,
  DuplicateUserAssigneeError,
  DuplicateStepOrderError,
  InvalidMinimumApprovalsError,
  MissingRequiredFieldError,
  PolicyHasNoStepsError,
  PolicyNotActiveError,
  PolicyNotFoundError,
  RoleHasNoActiveMembersError,
  StepHasNoAssigneesError,
  StepQuorumUnreachableError,
} from "../lib/errors.generated";
import {
  activePolicy,
  draftPolicy,
  inactivePolicy,
  pendingRequest,
  policyAssigneeRole,
  policyAssigneeUser,
  policyStep1,
  policyStep2,
} from "../testing/fixtures";
import { run, type CreateApprovalRequestInput } from "./createApprovalRequest";

const ctx: CommandContext = {
  actorId: "user-requester-1",
  permissions: ["approval:request:create"],
};

const baseInput = {
  name: "Activate Product P-100",
  purpose: "PRODUCT_ACTIVATION",
  targetEntityType: "Product",
  targetEntityId: "product-100",
};

const makeListUsersByRole = (users: Array<{ id: string }>) =>
  vi.fn().mockResolvedValue(ok({ users }));

describe("createApprovalRequest", () => {
  it("creates a request in policy mode and copies steps and assignees from the source policy", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const insertedStep1 = {
      ...policyStep1,
      id: "step-new-1",
      approvalRequestId: pendingRequest.id,
      status: "IN_PROGRESS" as const,
      resolvedAt: null,
    };
    const insertedStep2 = {
      ...policyStep2,
      id: "step-new-2",
      approvalRequestId: pendingRequest.id,
      status: "PENDING" as const,
      resolvedAt: null,
    };
    mock.enqueueResults(
      [activePolicy], // policy lookup
      [policyStep1, policyStep2], // policy steps
      [pendingRequest], // ApprovalRequest insert
      [insertedStep1], // ApprovalStep insert (step1)
      [policyAssigneeUser], // step1 assignees
      [], // ApprovalStepAssignee insert (step1)
      [insertedStep2], // ApprovalStep insert (step2)
      [policyAssigneeRole], // step2 assignees
      [], // ApprovalStepAssignee insert (step2)
      // re-select activating step assignees
      [
        {
          ...policyAssigneeUser,
          id: "assignee-new-1",
          approvalStepId: insertedStep1.id,
        },
      ],
    );

    const result = await mock.withTx((trx) =>
      run(trx, { ...baseInput, policyId: activePolicy.id }, ctx, {
        listUsersByRole: makeListUsersByRole([]),
      }),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.approvalRequest).toEqual(pendingRequest);
    }

    // ApprovalRequest values
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        name: baseInput.name,
        purpose: baseInput.purpose,
        targetEntityType: baseInput.targetEntityType,
        targetEntityId: baseInput.targetEntityId,
        sourcePolicyId: activePolicy.id,
        status: "PENDING",
      }),
    );
    // ApprovalStep #1 snapshot — activating step starts IN_PROGRESS
    expect(mock.inserts[1].insertValues()).toEqual(
      expect.objectContaining({
        approvalRequestId: pendingRequest.id,
        stepOrder: policyStep1.stepOrder,
        name: policyStep1.name,
        minimumApprovals: policyStep1.minimumApprovals,
        status: "IN_PROGRESS",
      }),
    );
    // ApprovalStepAssignee for step #1 (user-based)
    expect(mock.inserts[2].insertRows()).toEqual([
      expect.objectContaining({
        approvalStepId: insertedStep1.id,
        userId: policyAssigneeUser.userId,
        roleId: null,
        required: policyAssigneeUser.required,
        status: "PENDING",
      }),
    ]);
    // ApprovalStep #2 snapshot — trailing step stays PENDING
    expect(mock.inserts[3].insertValues()).toEqual(
      expect.objectContaining({
        approvalRequestId: pendingRequest.id,
        stepOrder: policyStep2.stepOrder,
        name: policyStep2.name,
        status: "PENDING",
      }),
    );
    // ApprovalStepAssignee for step #2 (role-based) — roleId carried, NOT expanded
    expect(mock.inserts[4].insertRows()).toEqual([
      expect.objectContaining({
        approvalStepId: insertedStep2.id,
        userId: null,
        roleId: policyAssigneeRole.roleId,
        status: "PENDING",
      }),
    ]);
  });

  it("creates a request in direct mode and persists supplied steps verbatim with sourcePolicyId null", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const created = { ...pendingRequest, sourcePolicyId: null };
    const insertedStep = {
      id: "step-direct-1",
      approvalRequestId: created.id,
      stepOrder: 1,
      name: "Manager review",
      minimumApprovals: 1,
      status: "IN_PROGRESS" as const,
      resolvedAt: null,
      createdAt: new Date(),
      updatedAt: null,
    };
    mock.enqueueResults(
      [created],
      [insertedStep],
      [],
      [
        {
          id: "assignee-direct-1",
          approvalStepId: insertedStep.id,
          userId: "user-manager-1",
          roleId: null,
          required: false,
          roleQuorum: null,
        },
      ],
    );

    const input: CreateApprovalRequestInput = {
      ...baseInput,
      steps: [
        {
          stepOrder: 1,
          name: "Manager review",
          minimumApprovals: 1,
          assignees: [{ userId: "user-manager-1", required: false }],
        },
      ],
    };
    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, { listUsersByRole: makeListUsersByRole([]) }),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.approvalRequest).toEqual(created);
    }
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        status: "PENDING",
      }),
    );
    expect(mock.inserts[1].insertValues()).toEqual(
      expect.objectContaining({
        approvalRequestId: created.id,
        stepOrder: 1,
        name: "Manager review",
        minimumApprovals: 1,
        status: "IN_PROGRESS",
      }),
    );
    expect(mock.inserts[2].insertRows()).toEqual([
      expect.objectContaining({
        approvalStepId: insertedStep.id,
        userId: "user-manager-1",
        required: false,
        status: "PENDING",
      }),
    ]);
  });

  it("sets requesterId from ctx.actorId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [
        {
          id: "step-1",
          approvalRequestId: pendingRequest.id,
        },
      ],
      [],
      [{ id: "a1", userId: "user-x", roleId: null, required: false, roleQuorum: null }],
    );

    const input: CreateApprovalRequestInput = {
      ...baseInput,
      steps: [
        {
          stepOrder: 1,
          name: "Step 1",
          assignees: [{ userId: "user-x" }],
        },
      ],
    };
    await mock.withTx((trx) =>
      run(
        trx,
        input,
        { actorId: "actor-from-ctx", permissions: [] },
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );

    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({ requesterId: "actor-from-ctx" }),
    );
  });

  it("accepts targetEntityType and targetEntityId as opaque strings/UUIDs with no FK enforcement", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [{ id: "step-1" }],
      [],
      [{ id: "a1", userId: "user-x", roleId: null, required: false, roleQuorum: null }],
    );

    const input: CreateApprovalRequestInput = {
      ...baseInput,
      targetEntityType: "ArbitraryType",
      targetEntityId: "arbitrary-id-not-a-uuid",
      steps: [
        {
          stepOrder: 1,
          name: "Step 1",
          assignees: [{ userId: "user-x" }],
        },
      ],
    };
    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, { listUsersByRole: makeListUsersByRole([]) }),
    );

    expect(result.ok).toBe(true);
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        targetEntityType: "ArbitraryType",
        targetEntityId: "arbitrary-id-not-a-uuid",
      }),
    );
  });

  it("accepts purpose as a free string with no enum validation", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [{ id: "step-1" }],
      [],
      [{ id: "a1", userId: "user-x", roleId: null, required: false, roleQuorum: null }],
    );

    const input: CreateApprovalRequestInput = {
      ...baseInput,
      purpose: "TOTALLY_CUSTOM_PURPOSE_VALUE",
      steps: [
        {
          stepOrder: 1,
          name: "Step 1",
          assignees: [{ userId: "user-x" }],
        },
      ],
    };
    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, { listUsersByRole: makeListUsersByRole([]) }),
    );

    expect(result.ok).toBe(true);
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({ purpose: "TOTALLY_CUSTOM_PURPOSE_VALUE" }),
    );
  });

  it("creates the request with status PENDING and the lowest-stepOrder step set to IN_PROGRESS", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [{ id: "step-1" }],
      [],
      [{ id: "a1", userId: "user-x", roleId: null, required: false, roleQuorum: null }],
    );

    const input: CreateApprovalRequestInput = {
      ...baseInput,
      steps: [
        {
          stepOrder: 1,
          name: "Step 1",
          assignees: [{ userId: "user-x" }],
        },
      ],
    };
    await mock.withTx((trx) => run(trx, input, ctx, { listUsersByRole: makeListUsersByRole([]) }));

    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        status: "PENDING",
      }),
    );
    expect(mock.inserts[1].insertValues()).toEqual(
      expect.objectContaining({ status: "IN_PROGRESS" }),
    );
  });

  it("expands a role-based assignee on the activating step into per-ACTIVE-member rows", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const insertedStep = { id: "step-role-1", approvalRequestId: pendingRequest.id };
    const listUsersByRole = makeListUsersByRole([
      { id: "user-director-1" },
      { id: "user-director-2" },
    ]);
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [insertedStep],
      [], // initial seed-row insert
      [
        {
          id: "assignee-role-seed",
          approvalStepId: insertedStep.id,
          userId: null,
          roleId: "role-director",
          required: false,
          roleQuorum: null,
        },
      ],
      [], // expansion insert
      [], // delete seed row
    );

    const input: CreateApprovalRequestInput = {
      ...baseInput,
      steps: [
        {
          stepOrder: 1,
          name: "Director review",
          assignees: [{ roleId: "role-director" }],
        },
      ],
    };
    const result = await mock.withTx((trx) => run(trx, input, ctx, { listUsersByRole }));

    expect(result.ok).toBe(true);
    expect(listUsersByRole).toHaveBeenCalledWith(
      expect.anything(),
      { roleId: "role-director" },
      ctx,
    );
    expect(mock.inserts.at(-1)!.insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          userId: "user-director-1",
          roleId: "role-director",
        }),
        expect.objectContaining({
          userId: "user-director-2",
          roleId: "role-director",
        }),
      ]),
    );
    expect(mock.deletes).toHaveLength(1);
  });

  it("propagates the originating roleId and roleQuorum verbatim to every materialized row", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const insertedStep = { id: "step-role-2" };
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [insertedStep],
      [],
      [
        {
          id: "assignee-role-seed",
          approvalStepId: insertedStep.id,
          userId: null,
          roleId: "role-director",
          required: false,
          roleQuorum: "ALL",
        },
      ],
      [],
      [],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Director review",
              assignees: [{ roleId: "role-director", roleQuorum: "ALL" }],
            },
          ],
        },
        ctx,
        {
          listUsersByRole: makeListUsersByRole([
            { id: "user-director-1" },
            { id: "user-director-2" },
          ]),
        },
      ),
    );

    const expansionCall = mock.inserts.at(-1)!.insertRows() as Array<{
      roleId: string | null;
      roleQuorum: "ALL" | "ANY" | null;
    }>;
    expect(expansionCall).toHaveLength(2);
    for (const row of expansionCall) {
      expect(row.roleId).toBe("role-director");
      expect(row.roleQuorum).toBe("ALL");
    }
  });

  it("preserves user-only assignees on the activating step verbatim (no expansion needed)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const listUsersByRole = makeListUsersByRole([]);
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [{ id: "step-1" }],
      [],
      [
        {
          id: "a1",
          approvalStepId: "step-1",
          userId: "user-manager-1",
          roleId: null,
          required: false,
          roleQuorum: null,
        },
      ],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Manager review",
              assignees: [{ userId: "user-manager-1" }],
            },
          ],
        },
        ctx,
        { listUsersByRole },
      ),
    );

    expect(result.ok).toBe(true);
    expect(listUsersByRole).not.toHaveBeenCalled();
    expect(mock.deletes).toHaveLength(0);
  });

  it("does not expand role-based assignees on trailing steps; they expand at their own activation moment", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const listUsersByRole = makeListUsersByRole([{ id: "user-director-1" }]);
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [{ id: "step-1" }], // activating step
      [], // step1 user assignee
      [{ id: "step-2" }], // trailing step
      [], // step2 role assignee (no expansion)
      [
        {
          id: "a1",
          approvalStepId: "step-1",
          userId: "user-manager-1",
          roleId: null,
          required: false,
          roleQuorum: null,
        },
      ],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            { stepOrder: 1, name: "Manager review", assignees: [{ userId: "user-manager-1" }] },
            {
              stepOrder: 2,
              name: "Director review",
              assignees: [{ roleId: "role-director" }],
            },
          ],
        },
        ctx,
        { listUsersByRole },
      ),
    );

    // listUsersByRole only invoked when a role appears on the activating step
    expect(listUsersByRole).not.toHaveBeenCalled();
  });

  it("throws CREATION_MODE_AMBIGUOUS when both policyId and steps are supplied", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const input = {
      ...baseInput,
      policyId: activePolicy.id,
      steps: [{ stepOrder: 1, name: "Step 1", assignees: [{ userId: "user-x" }] }],
    } as unknown as CreateApprovalRequestInput;

    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, { listUsersByRole: makeListUsersByRole([]) }),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CreationModeAmbiguousError);
    }
  });

  it("throws CREATION_MODE_AMBIGUOUS when neither policyId nor steps are supplied", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const input = { ...baseInput } as unknown as CreateApprovalRequestInput;

    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, { listUsersByRole: makeListUsersByRole([]) }),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CreationModeAmbiguousError);
    }
  });

  it("throws POLICY_NOT_FOUND for a non-existent policyId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) =>
      run(trx, { ...baseInput, policyId: "missing-policy" }, ctx, {
        listUsersByRole: makeListUsersByRole([]),
      }),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(PolicyNotFoundError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws POLICY_NOT_ACTIVE when the source policy is in DRAFT", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy]);

    const result = await mock.withTx((trx) =>
      run(trx, { ...baseInput, policyId: draftPolicy.id }, ctx, {
        listUsersByRole: makeListUsersByRole([]),
      }),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(PolicyNotActiveError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws POLICY_NOT_ACTIVE when the source policy is INACTIVE", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([inactivePolicy]);

    const result = await mock.withTx((trx) =>
      run(trx, { ...baseInput, policyId: inactivePolicy.id }, ctx, {
        listUsersByRole: makeListUsersByRole([]),
      }),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(PolicyNotActiveError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws POLICY_HAS_NO_STEPS when direct-mode steps array is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(trx, { ...baseInput, steps: [] }, ctx, {
        listUsersByRole: makeListUsersByRole([]),
      }),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(PolicyHasNoStepsError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws DUPLICATE_STEP_ORDER when two direct-mode steps share the same stepOrder", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            { stepOrder: 1, name: "A", assignees: [{ userId: "user-a" }] },
            { stepOrder: 1, name: "B", assignees: [{ userId: "user-b" }] },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(DuplicateStepOrderError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws STEP_HAS_NO_ASSIGNEES when a direct-mode step has no assignees", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [{ stepOrder: 1, name: "Empty step", assignees: [] }],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(StepHasNoAssigneesError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws INVALID_MINIMUM_APPROVALS when minimumApprovals is zero or negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Bad step",
              minimumApprovals: 0,
              assignees: [{ userId: "user-a" }],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidMinimumApprovalsError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws ASSIGNEE_USER_ROLE_XOR_VIOLATED when an assignee sets neither userId nor roleId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Bad assignee step",
              assignees: [{}],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(AssigneeUserRoleXorViolatedError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("throws MISSING_REQUIRED_FIELD when name is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          name: "",
          steps: [
            {
              stepOrder: 1,
              name: "Step 1",
              assignees: [{ userId: "user-x" }],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingRequiredFieldError);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("accepts a direct-mode role assignee with roleQuorum = ALL or ANY as a valid configuration", async () => {
    for (const quorum of ["ALL", "ANY"] as const) {
      const mock = createKyselyMock<Namespace["main-db"]>();
      mock.enqueueResults(
        [pendingRequest],
        [{ id: "step-1" }],
        [],
        [
          {
            id: "a-role",
            approvalStepId: "step-1",
            userId: null,
            roleId: "r1",
            required: false,
            roleQuorum: quorum,
          },
        ],
        [], // expansion insert
        [], // delete seed
      );
      const result = await mock.withTx((trx) =>
        run(
          trx,
          {
            ...baseInput,
            steps: [
              {
                stepOrder: 1,
                name: "Step 1",
                assignees: [{ roleId: "r1", roleQuorum: quorum }],
              },
            ],
          },
          ctx,
          { listUsersByRole: makeListUsersByRole([{ id: "u1" }]) },
        ),
      );
      expect(result.ok).toBe(true);
    }
  });

  it("throws ASSIGNEE_FLAGS_INCOMPATIBLE when a direct-mode role assignee sets required = true", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Step 1",
              assignees: [{ roleId: "r1", required: true }],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(AssigneeFlagsIncompatibleError);
    }
  });

  it("throws ASSIGNEE_FLAGS_INCOMPATIBLE when a direct-mode user assignee sets roleQuorum", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Step 1",
              assignees: [{ userId: "u1", roleQuorum: "ALL" }],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(AssigneeFlagsIncompatibleError);
    }
  });

  it("throws DUPLICATE_ROLE_ASSIGNEE when two direct-mode role assignees within the same step share the same roleId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Step 1",
              assignees: [
                { roleId: "r1", roleQuorum: "ALL" },
                { roleId: "r1", roleQuorum: "ANY" },
              ],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(DuplicateRoleAssigneeError);
    }
  });

  it("throws DUPLICATE_USER_ASSIGNEE when two direct-mode user assignees within the same step share the same userId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Step 1",
              assignees: [{ userId: "u1" }, { userId: "u1" }],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) },
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(DuplicateUserAssigneeError);
    }
  });

  it("throws ROLE_HAS_NO_ACTIVE_MEMBERS when a role on the activating step has zero current ACTIVE members", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [{ id: "step-1" }],
      [],
      [
        {
          id: "a-role",
          approvalStepId: "step-1",
          userId: null,
          roleId: "role-director",
          required: false,
          roleQuorum: null,
        },
      ],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Director review",
              assignees: [{ roleId: "role-director" }],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([]) }, // empty membership
      ),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(RoleHasNoActiveMembersError);
    }
  });

  it("throws STEP_QUORUM_UNREACHABLE when role expansion yields fewer runtime assignees than the activating step's minimumApprovals", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [{ ...pendingRequest, sourcePolicyId: null }],
      [{ id: "step-1" }],
      [],
      [
        {
          id: "a-role",
          approvalStepId: "step-1",
          userId: null,
          roleId: "role-director",
          required: false,
          roleQuorum: null,
        },
      ],
      [], // expansion insert
      [], // delete seed
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...baseInput,
          steps: [
            {
              stepOrder: 1,
              name: "Director review",
              minimumApprovals: 3,
              assignees: [{ roleId: "role-director" }],
            },
          ],
        },
        ctx,
        { listUsersByRole: makeListUsersByRole([{ id: "u1" }, { id: "u2" }]) },
      ),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(StepQuorumUnreachableError);
    }
  });
});
