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

describe("sendBackApprovalStep", () => {
  const requiredAssignee = { ...pendingAssigneeUser, required: true };
  const optionalAssignee = { ...pendingAssigneeUser, required: false };

  const validInput = {
    approvalStepAssigneeId: pendingAssigneeUser.id,
    reason: "Attach the signed vendor quote",
  };

  const actorCtx: CommandContext = {
    actorId: pendingAssigneeUser.userId!,
    permissions: [],
  };

  // Happy-path result queue, one entry per statement in run().
  function enqueueSendBack(
    mock: ReturnType<typeof createKyselyMock<Namespace["main-db"]>>,
    assignee = requiredAssignee,
  ) {
    mock.enqueueResults(
      [assignee], // load assignee
      [inProgressStep1], // load step
      [pendingRequest], // load request
      [{}], // insert decision
      [{ ...pendingRequest, status: "REVISION_REQUESTED" as const }], // update request, returningAll
    );
  }

  it("sends a step back from a required = true assignee and transitions the request to REVISION_REQUESTED", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueSendBack(mock);

    const result = await mock.withTx((trx) => run(trx, validInput, actorCtx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.approvalRequest.status).toBe("REVISION_REQUESTED");
    }
  });

  it("sends a step back from a required = false (optional) assignee and transitions the request to REVISION_REQUESTED", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueSendBack(mock, optionalAssignee);

    const result = await mock.withTx((trx) => run(trx, validInput, actorCtx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.approvalRequest.status).toBe("REVISION_REQUESTED");
    }
  });

  it("writes one ApprovalDecision row with decision = SEND_BACK, both ids set, and comment = reason", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueSendBack(mock);

    await mock.withTx((trx) => run(trx, validInput, actorCtx));

    expect(mock.inserts.length).toBe(1);
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        decision: "SEND_BACK",
        approvalRequestId: pendingRequest.id,
        approvalStepId: inProgressStep1.id,
        decidedByUserId: actorCtx.actorId,
        comment: validInput.reason,
      }),
    );
  });

  it("leaves ApprovalStep and ApprovalStepAssignee rows unchanged and only updates the request", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueSendBack(mock);

    await mock.withTx((trx) => run(trx, validInput, actorCtx));

    expect(mock.updates.length).toBe(1);
    expect(mock.updates[0].updateValues()).toEqual(
      expect.objectContaining({ status: "REVISION_REQUESTED" }),
    );
  });

  it("does not set resolvedAt on the request because REVISION_REQUESTED is non-terminal", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueSendBack(mock);

    await mock.withTx((trx) => run(trx, validInput, actorCtx));

    expect(mock.updates[0].updateValues()).not.toHaveProperty("resolvedAt");
  });

  it("throws MISSING_REQUIRED_FIELD when reason is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([requiredAssignee], [inProgressStep1], [pendingRequest]);

    const result = await mock.withTx((trx) =>
      run(trx, { approvalStepAssigneeId: pendingAssigneeUser.id, reason: "   " }, actorCtx),
    );

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

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

    const result = await mock.withTx((trx) => run(trx, validInput, actorCtx));

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

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

    const result = await mock.withTx((trx) =>
      run(trx, validInput, { actorId: "different-user", permissions: [] }),
    );

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

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

    const result = await mock.withTx((trx) => run(trx, validInput, actorCtx));

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

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

    const result = await mock.withTx((trx) => run(trx, validInput, actorCtx));

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

  it("throws PARENT_REQUEST_NOT_PENDING when the parent request is in any non-PENDING status", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([requiredAssignee], [inProgressStep1], [approvedRequest]);

    const result = await mock.withTx((trx) => run(trx, validInput, actorCtx));

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

  it("throws SELF_DECISION_NOT_ALLOWED when the actor is the requester", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const selfAssignee = { ...requiredAssignee, userId: pendingRequest.requesterId };
    mock.enqueueResults([selfAssignee], [inProgressStep1], [pendingRequest]);

    const result = await mock.withTx((trx) =>
      run(trx, validInput, {
        actorId: pendingRequest.requesterId,
        permissions: [],
      }),
    );

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