import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AssigneeFlagsIncompatibleError,
  AssigneeUserRoleXorViolatedError,
  DuplicateRoleAssigneeError,
  DuplicateUserAssigneeError,
  DuplicateStepOrderError,
  InvalidMinimumApprovalsError,
  MissingRequiredFieldError,
  PolicyHasNoStepsError,
  StepHasNoAssigneesError,
} from "../lib/errors.generated";
import { draftPolicy } from "../testing/fixtures";
import { run, type CreateApprovalPolicyInput } from "./createApprovalPolicy";

describe("createApprovalPolicy", () => {
  const ctx: CommandContext = {
    actorId: "test-actor",
    permissions: ["approval:policy:create"],
  };

  const validInput: CreateApprovalPolicyInput = {
    purpose: "PRODUCT_ACTIVATION",
    name: "Standard Product",
    description: "Standard 2-step product activation chain",
    steps: [
      {
        stepOrder: 1,
        name: "Manager review",
        minimumApprovals: 1,
        assignees: [{ userId: "user-manager-1" }],
      },
    ],
  };

  it("creates a policy with status DRAFT, activatedAt null, and one step with one assignee", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);

    const result = await mock.withTx((trx) => run(trx, validInput, ctx));
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.approvalPolicy.status).toBe("DRAFT");
      expect(result.value.approvalPolicy.activatedAt).toBeNull();
    }
    // Three inserts: policy, steps (bulk), assignees (bulk)
    expect(mock.inserts).toHaveLength(3);
  });

  it("creates a policy with multiple steps in distinct stepOrder values", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [draftPolicy],
      [
        { id: "policy-step-1", stepOrder: 1 },
        { id: "policy-step-2", stepOrder: 2 },
      ],
      [],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Manager",
              minimumApprovals: 1,
              assignees: [{ userId: "u1" }],
            },
            {
              stepOrder: 2,
              name: "Director",
              minimumApprovals: 1,
              assignees: [{ roleId: "r1" }],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
    expect(mock.inserts).toHaveLength(3);
  });

  it("creates a policy with multiple assignees per step", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Reviewers",
              minimumApprovals: 1,
              assignees: [{ userId: "u1" }, { userId: "u2" }, { roleId: "r1" }],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
    // Assignee insert payload should carry 3 rows
    expect(mock.inserts[2].insertRows()).toHaveLength(3);
  });

  it("defaults minimumApprovals to 1 when not supplied", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Manager",
              assignees: [{ userId: "u1" }],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
    // The step insert payload should have minimumApprovals = 1
    const stepValues = mock.inserts[1].insertRows();
    expect(stepValues[0]?.minimumApprovals).toBe(1);
  });

  it("accepts a step with minimumApprovals = 2 and one required = true assignee as a valid configuration", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Reviewers",
              minimumApprovals: 2,
              assignees: [{ userId: "u1", required: true }, { userId: "u2" }],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
    const assigneeValues = mock.inserts[2].insertRows();
    expect(assigneeValues[0]?.required).toBe(true);
    expect(assigneeValues[1]?.required).toBe(false);
  });

  it(`accepts purpose values such as "PRODUCT_ACTIVATION" or "PO_CONFIRMATION" verbatim with no enum validation`, async () => {
    for (const purpose of ["PRODUCT_ACTIVATION", "PO_CONFIRMATION"]) {
      const mock = createKyselyMock<Namespace["main-db"]>();
      mock.enqueueResults(
        [{ ...draftPolicy, purpose }],
        [{ id: "policy-step-1", stepOrder: 1 }],
        [],
      );

      const result = await mock.withTx((trx) => run(trx, { ...validInput, purpose }, ctx));
      expect(result.ok).toBe(true);
      expect(mock.inserts[0].insertValues().purpose).toBe(purpose);
    }
  });

  it("allows multiple DRAFT policies under the same (purpose, name) pair", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);

    const result = await mock.withTx((trx) => run(trx, validInput, ctx));
    expect(result.ok).toBe(true);
    // No SELECT should be issued — uniqueness is not enforced at create time
    expect(mock.selects).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, { ...validInput, name: "" }, ctx));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingRequiredFieldError);
    }
  });

  it("throws MISSING_REQUIRED_FIELD when purpose is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) => run(trx, { ...validInput, purpose: "" }, ctx));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingRequiredFieldError);
    }
  });

  it("throws POLICY_HAS_NO_STEPS when steps array is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) => run(trx, { ...validInput, steps: [] }, ctx));
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(PolicyHasNoStepsError);
    }
  });

  it("throws DUPLICATE_STEP_ORDER when two steps share a stepOrder", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            { stepOrder: 1, name: "a", assignees: [{ userId: "u1" }] },
            { stepOrder: 1, name: "b", assignees: [{ userId: "u2" }] },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(DuplicateStepOrderError);
    }
  });

  it("throws STEP_HAS_NO_ASSIGNEES when a step has no assignees", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [{ stepOrder: 1, name: "Manager", assignees: [] }],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(StepHasNoAssigneesError);
    }
  });

  it("accepts minimumApprovals greater than the policy assignee row count (the relationship is validated at step activation after role expansion)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Managers",
              minimumApprovals: 3,
              assignees: [{ roleId: "role-1" }],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
  });

  it("throws INVALID_MINIMUM_APPROVALS when minimumApprovals is zero or negative", async () => {
    for (const minimumApprovals of [0, -1]) {
      const mock = createKyselyMock<Namespace["main-db"]>();
      const result = await mock.withTx((trx) =>
        run(
          trx,
          {
            ...validInput,
            steps: [
              {
                stepOrder: 1,
                name: "Manager",
                minimumApprovals,
                assignees: [{ userId: "u1" }],
              },
            ],
          },
          ctx,
        ),
      );
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error).toBeInstanceOf(InvalidMinimumApprovalsError);
      }
    }
  });

  it("throws ASSIGNEE_USER_ROLE_XOR_VIOLATED when an assignee row sets both userId and roleId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Manager",
              assignees: [{ userId: "u1", roleId: "r1" }],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(AssigneeUserRoleXorViolatedError);
    }
  });

  it("throws ASSIGNEE_USER_ROLE_XOR_VIOLATED when an assignee row sets neither userId nor roleId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [{ stepOrder: 1, name: "Manager", assignees: [{}] }],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(AssigneeUserRoleXorViolatedError);
    }
  });

  it("accepts a role assignee with roleQuorum = ALL as a valid configuration", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            { stepOrder: 1, name: "Managers", assignees: [{ roleId: "r1", roleQuorum: "ALL" }] },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
  });

  it("accepts a role assignee with roleQuorum = ANY as a valid configuration", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [
            { stepOrder: 1, name: "Managers", assignees: [{ roleId: "r1", roleQuorum: "ANY" }] },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
  });

  it("accepts a role assignee with roleQuorum = null (only step.minimumApprovals applies)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], [{ id: "policy-step-1", stepOrder: 1 }], []);
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...validInput,
          steps: [{ stepOrder: 1, name: "Managers", assignees: [{ roleId: "r1" }] }],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(true);
  });

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

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

  it("throws DUPLICATE_ROLE_ASSIGNEE when two 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,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Managers",
              assignees: [
                { roleId: "r1", roleQuorum: "ALL" },
                { roleId: "r1", roleQuorum: "ANY" },
              ],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(DuplicateRoleAssigneeError);
    }
  });

  it("throws DUPLICATE_USER_ASSIGNEE when two 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,
        {
          ...validInput,
          steps: [
            {
              stepOrder: 1,
              name: "Managers",
              assignees: [{ userId: "u1" }, { userId: "u1" }],
            },
          ],
        },
        ctx,
      ),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(DuplicateUserAssigneeError);
    }
  });
});
