import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } 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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest)
      .mockReturnValueOnce(undefined);

    const result = await run(db, baseInput, ctx);

    expect(result.ok).toBe(true);
    // one decision insert + one new assignee insert
    expect(spies.insert).toHaveBeenCalledTimes(2);
    const newAssigneeRow = spies.values.mock.calls[1]?.[0] as Record<string, unknown>;
    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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest)
      .mockReturnValueOnce(undefined);

    await run(db, baseInput, ctx);

    const setArgs = spies.set.mock.calls[0]?.[0] as Record<string, unknown>;
    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 { db, spies } = createMockDb<Transaction>();
    const requiredAssignee = { ...pendingAssigneeUser, required: true };
    spies.select
      .mockReturnValueOnce(requiredAssignee)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest)
      .mockReturnValueOnce(undefined);

    await run(db, baseInput, ctx);

    const newAssigneeRow = spies.values.mock.calls[1]?.[0] as Record<string, unknown>;
    expect(newAssigneeRow.required).toBe(true);
  });

  it("writes an ApprovalDecision row with decision = DELEGATE, both ids set, and delegatedToUserId populated", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest)
      .mockReturnValueOnce(undefined);

    await run(db, baseInput, ctx);

    const decisionRow = spies.values.mock.calls[0]?.[0] as Record<string, unknown>;
    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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest)
      .mockReturnValueOnce(undefined);

    await run(db, { ...baseInput, comment: "out of office" }, ctx);

    const decisionRow = spies.values.mock.calls[0]?.[0] as Record<string, unknown>;
    expect(decisionRow.comment).toBe("out of office");
  });

  it("throws SELF_DELEGATION_NOT_ALLOWED when delegatedToUserId equals ctx.actorId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest);

    const result = await run(db, { ...baseInput, delegatedToUserId: ctx.actorId }, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SelfDelegationNotAllowedError);
    }
    expect(spies.insert).not.toHaveBeenCalled();
    expect(spies.update).not.toHaveBeenCalled();
  });

  it("throws CIRCULAR_DELEGATION when delegatedToUserId is already PENDING on the same step", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest)
      // existing PENDING assignee on the same step
      .mockReturnValueOnce({
        ...pendingAssigneeUser,
        id: "assignee-pending-user-2",
        userId: baseInput.delegatedToUserId,
      });

    const result = await run(db, baseInput, ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(CircularDelegationError);
    }
    expect(spies.insert).not.toHaveBeenCalled();
    expect(spies.update).not.toHaveBeenCalled();
  });

  it("throws SELF_DECISION_NOT_ALLOWED when ctx.actorId is the requester", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const actorIsRequester: CommandContext = {
      actorId: pendingRequest.requesterId,
      permissions: ctx.permissions,
    };
    const assigneeAsRequester = {
      ...pendingAssigneeUser,
      userId: pendingRequest.requesterId,
    };
    spies.select
      .mockReturnValueOnce(assigneeAsRequester)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest);

    const result = await run(db, baseInput, actorIsRequester);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SelfDecisionNotAllowedError);
    }
    expect(spies.insert).not.toHaveBeenCalled();
    expect(spies.update).not.toHaveBeenCalled();
  });

  it("throws NOT_ACTIVE_ASSIGNEE when the actor is not an active assignee (assignee not found)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, 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 { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...pendingAssigneeUser, userId: "user-other-1" });

    const result = await run(db, 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 { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...pendingAssigneeUser, status: "DELEGATED" });

    const result = await run(db, 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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce({ ...inProgressStep1, status: "PENDING" });

    const result = await run(db, 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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(approvedRequest);

    const result = await run(db, 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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(rejectedRequest);

    const result = await run(db, 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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(withdrawnRequest);

    const result = await run(db, 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 { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(pendingAssigneeUser)
      .mockReturnValueOnce(inProgressStep1)
      .mockReturnValueOnce(pendingRequest)
      .mockReturnValueOnce(undefined);

    await run(db, baseInput, ctx);

    // only the original assignee row is updated (status -> DELEGATED). The step row is not.
    const updatedStatuses = spies.set.mock.calls.map(
      (c) => (c[0] as Record<string, unknown>).status,
    );
    expect(updatedStatuses).toEqual(["DELEGATED"]);
  });
});
