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 {
  CircularDelegationError,
  NotActiveAssigneeError,
  ParentRequestNotPendingError,
  SelfDecisionNotAllowedError,
  SelfDelegationNotAllowedError,
} from "../lib/errors.generated";
import {
  approvedRequest,
  inProgressStep1,
  pendingAssigneeUser,
  pendingRequest,
  rejectedRequest,
  withdrawnRequest,
} from "../testing/fixtures";
import { run } from "./delegateApprovalStep";

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

const baseInput = {
  approvalStepAssigneeId: pendingAssigneeUser.id,
  delegatedToUserId: "user-delegate-1",
};

describe("delegateApprovalStep", () => {
  it("delegates an assignee's seat to a peer and inserts a new PENDING StepAssignee for the delegate", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [pendingRequest], []);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

    expect(result.ok).toBe(true);
    // one decision insert + one new assignee insert
    expect(mock.inserts).toHaveLength(2);
    const newAssigneeRow = mock.inserts[1].insertValues();
    expect(newAssigneeRow.userId).toBe(baseInput.delegatedToUserId);
    expect(newAssigneeRow.status).toBe("PENDING");
    expect(newAssigneeRow.approvalStepId).toBe(inProgressStep1.id);
  });

  it("transitions the original assignee row to DELEGATED and stamps resolvedAt", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [pendingRequest], []);

    await mock.withTx((trx) => run(trx, baseInput, ctx));

    const setArgs = mock.updates[0].updateValues();
    expect(setArgs.status).toBe("DELEGATED");
    expect(setArgs.resolvedAt).toBeInstanceOf(Date);
  });

  it("propagates the originating row's required flag to the new delegate row verbatim", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const requiredAssignee = { ...pendingAssigneeUser, required: true };
    mock.enqueueResults([requiredAssignee], [inProgressStep1], [pendingRequest], []);

    await mock.withTx((trx) => run(trx, baseInput, ctx));

    const newAssigneeRow = mock.inserts[1].insertValues();
    expect(newAssigneeRow.required).toBe(true);
  });

  it("writes an ApprovalDecision row with decision = DELEGATE, both ids set, and delegatedToUserId populated", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [pendingRequest], []);

    await mock.withTx((trx) => run(trx, baseInput, ctx));

    const decisionRow = mock.inserts[0].insertValues();
    expect(decisionRow.decision).toBe("DELEGATE");
    expect(decisionRow.approvalRequestId).toBe(pendingRequest.id);
    expect(decisionRow.approvalStepId).toBe(inProgressStep1.id);
    expect(decisionRow.delegatedToUserId).toBe(baseInput.delegatedToUserId);
    expect(decisionRow.decidedByUserId).toBe(ctx.actorId);
  });

  it("accepts an optional comment and stores it on the decision row", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [pendingRequest], []);

    await mock.withTx((trx) => run(trx, { ...baseInput, comment: "out of office" }, ctx));

    const decisionRow = mock.inserts[0].insertValues();
    expect(decisionRow.comment).toBe("out of office");
  });

  it("throws SELF_DELEGATION_NOT_ALLOWED when delegatedToUserId equals ctx.actorId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [pendingRequest]);

    const result = await mock.withTx((trx) =>
      run(trx, { ...baseInput, delegatedToUserId: ctx.actorId }, ctx),
    );

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

  it("throws CIRCULAR_DELEGATION when delegatedToUserId is already PENDING on the same step", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [pendingAssigneeUser],
      [inProgressStep1],
      [pendingRequest],
      // existing PENDING assignee on the same step
      [
        {
          ...pendingAssigneeUser,
          id: "assignee-pending-user-2",
          userId: baseInput.delegatedToUserId,
        },
      ],
    );

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("throws SELF_DECISION_NOT_ALLOWED when ctx.actorId is the requester", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const actorIsRequester: CommandContext = {
      actorId: pendingRequest.requesterId,
      permissions: ctx.permissions,
    };
    const assigneeAsRequester = {
      ...pendingAssigneeUser,
      userId: pendingRequest.requesterId,
    };
    mock.enqueueResults([assigneeAsRequester], [inProgressStep1], [pendingRequest]);

    const result = await mock.withTx((trx) => run(trx, baseInput, actorIsRequester));

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

  it("throws NOT_ACTIVE_ASSIGNEE when the actor is not an active assignee (assignee not found)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("throws NOT_ACTIVE_ASSIGNEE when the assignee belongs to a different user", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([{ ...pendingAssigneeUser, userId: "user-other-1" }]);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("throws NOT_ACTIVE_ASSIGNEE when the assignee status is not PENDING", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([{ ...pendingAssigneeUser, status: "DELEGATED" }]);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("throws NOT_ACTIVE_ASSIGNEE when the parent step is not IN_PROGRESS", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [{ ...inProgressStep1, status: "PENDING" }]);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("throws PARENT_REQUEST_NOT_PENDING when the parent request is APPROVED", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [approvedRequest]);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("throws PARENT_REQUEST_NOT_PENDING when the parent request is REJECTED", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [rejectedRequest]);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("throws PARENT_REQUEST_NOT_PENDING when the parent request is WITHDRAWN", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [withdrawnRequest]);

    const result = await mock.withTx((trx) => run(trx, baseInput, ctx));

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

  it("does not update the parent step row — step remains IN_PROGRESS one-for-one", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([pendingAssigneeUser], [inProgressStep1], [pendingRequest], []);

    await mock.withTx((trx) => run(trx, baseInput, ctx));

    // only the original assignee row is updated (status -> DELEGATED). The step row is not.
    const updatedStatuses = mock.updates.map((u) => u.updateValues().status);
    expect(updatedStatuses).toEqual(["DELEGATED"]);
  });
});
