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 {
  ActivePolicyConflictError,
  InvalidStatusTransitionError,
  PolicyNotFoundError,
  PolicyNotReadyToActivateError,
} from "../lib/errors.generated";
import {
  activePolicy,
  draftPolicy,
  inactivePolicy,
  policyStep1,
  policyStep2,
} from "../testing/fixtures";
import { run } from "./activateApprovalPolicy";

const ctx: CommandContext = {
  actorId: "test-actor",
  permissions: ["approval:policy:activate"],
};

describe("activateApprovalPolicy", () => {
  it("activates a DRAFT policy, transitions status to ACTIVE, and stamps activatedAt", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const activated = {
      ...draftPolicy,
      status: "ACTIVE" as const,
      activatedAt: new Date("2024-02-01T00:00:00.000Z"),
      updatedAt: new Date("2024-02-01T00:00:00.000Z"),
    };
    spies.select
      // policy lookup
      .mockReturnValueOnce(draftPolicy)
      // steps for policy
      .mockReturnValueOnce([policyStep1, policyStep2])
      // assignees: one row per step that has at least one assignee
      .mockReturnValueOnce([
        { approvalPolicyStepId: policyStep1.id },
        { approvalPolicyStepId: policyStep2.id },
      ])
      // ACTIVE conflict check
      .mockReturnValueOnce(undefined);
    spies.update.mockReturnValueOnce(activated);

    const result = await run(db, { id: draftPolicy.id }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.approvalPolicy.status).toBe("ACTIVE");
      expect(result.value.approvalPolicy.activatedAt).not.toBeNull();
    }
    expect(spies.update).toHaveBeenCalled();
    const setArgs = spies.set.mock.calls[0]?.[0] as Record<string, unknown>;
    expect(setArgs.status).toBe("ACTIVE");
    expect(setArgs.activatedAt).toBeInstanceOf(Date);
  });

  it("activates a DRAFT policy when no prior row exists for (purpose, name)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(draftPolicy)
      .mockReturnValueOnce([policyStep1])
      .mockReturnValueOnce([{ approvalPolicyStepId: policyStep1.id }])
      // no prior row at all -> conflict check returns undefined
      .mockReturnValueOnce(undefined);
    spies.update.mockReturnValueOnce({
      ...draftPolicy,
      status: "ACTIVE" as const,
      activatedAt: new Date(),
      updatedAt: new Date(),
    });

    const result = await run(db, { id: draftPolicy.id }, ctx);

    expect(result.ok).toBe(true);
  });

  it("activates a DRAFT policy when one or more prior INACTIVE rows exist for (purpose, name) (lineage continues)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(draftPolicy)
      .mockReturnValueOnce([policyStep1])
      .mockReturnValueOnce([{ approvalPolicyStepId: policyStep1.id }])
      // prior INACTIVE rows exist but no ACTIVE row matches
      .mockReturnValueOnce(undefined);
    const activated = {
      ...draftPolicy,
      status: "ACTIVE" as const,
      activatedAt: new Date("2024-03-01T00:00:00.000Z"),
      updatedAt: new Date("2024-03-01T00:00:00.000Z"),
    };
    spies.update.mockReturnValueOnce(activated);

    const result = await run(db, { id: draftPolicy.id }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      // successive activations stamp a fresh activatedAt
      expect(result.value.approvalPolicy.activatedAt).toEqual(new Date("2024-03-01T00:00:00.000Z"));
    }
  });

  it("throws ACTIVE_POLICY_CONFLICT when another ACTIVE policy holds the same (purpose, name)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(draftPolicy)
      .mockReturnValueOnce([policyStep1])
      .mockReturnValueOnce([{ approvalPolicyStepId: policyStep1.id }])
      // existing ACTIVE row with matching (purpose, name) but different id
      .mockReturnValueOnce(activePolicy);

    const result = await run(db, { id: draftPolicy.id }, ctx);

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

  it("allows activation after the prior ACTIVE policy is deactivated", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(draftPolicy)
      .mockReturnValueOnce([policyStep1])
      .mockReturnValueOnce([{ approvalPolicyStepId: policyStep1.id }])
      // prior ACTIVE was deactivated -> only INACTIVE rows remain, no ACTIVE conflict
      .mockReturnValueOnce(undefined);
    spies.update.mockReturnValueOnce({
      ...draftPolicy,
      status: "ACTIVE" as const,
      activatedAt: new Date(),
      updatedAt: new Date(),
    });

    const result = await run(db, { id: draftPolicy.id }, ctx);

    expect(result.ok).toBe(true);
  });

  it("throws INVALID_STATUS_TRANSITION when the policy is already ACTIVE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(activePolicy);

    const result = await run(db, { id: activePolicy.id }, ctx);

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

  it("throws INVALID_STATUS_TRANSITION when the policy is INACTIVE", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(inactivePolicy);

    const result = await run(db, { id: inactivePolicy.id }, ctx);

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

  it("throws POLICY_NOT_FOUND for a non-existent policyId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, { id: "nonexistent-policy" }, ctx);

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

  it("throws POLICY_NOT_READY_TO_ACTIVATE when the policy has zero steps", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(draftPolicy)
      // zero steps
      .mockReturnValueOnce([]);

    const result = await run(db, { id: draftPolicy.id }, ctx);

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

  it("throws POLICY_NOT_READY_TO_ACTIVATE when any step has zero assignees", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(draftPolicy)
      .mockReturnValueOnce([policyStep1, policyStep2])
      // step 1 has assignees but step 2 has none — only step 1 appears in the result
      .mockReturnValueOnce([{ approvalPolicyStepId: policyStep1.id }]);

    const result = await run(db, { id: draftPolicy.id }, ctx);

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

  it("successive activations under the same (purpose, name) each stamp a fresh activatedAt; lineage is recoverable by ordering rows by activatedAt", async () => {
    // Each activation stamps activatedAt = now on its own row; the prior ACTIVE
    // row is presumed already deactivated, so the conflict check sees no ACTIVE
    // peer. Two successive activations therefore yield two distinct activatedAt
    // values that read-side ordering can use to recover lineage.
    const { db, spies } = createMockDb<Transaction>();
    const firstActivatedAt = new Date("2024-01-01T00:00:00.000Z");
    const secondActivatedAt = new Date("2024-06-01T00:00:00.000Z");

    // First activation
    spies.select
      .mockReturnValueOnce(draftPolicy)
      .mockReturnValueOnce([policyStep1])
      .mockReturnValueOnce([{ approvalPolicyStepId: policyStep1.id }])
      .mockReturnValueOnce(undefined);
    spies.update.mockReturnValueOnce({
      ...draftPolicy,
      status: "ACTIVE" as const,
      activatedAt: firstActivatedAt,
      updatedAt: firstActivatedAt,
    });

    const first = await run(db, { id: draftPolicy.id }, ctx);
    expect(first.ok).toBe(true);

    // Second activation of a different DRAFT row under the same (purpose, name)
    const secondDraft = { ...draftPolicy, id: "policy-draft-2" };
    spies.select
      .mockReturnValueOnce(secondDraft)
      .mockReturnValueOnce([policyStep1])
      .mockReturnValueOnce([{ approvalPolicyStepId: policyStep1.id }])
      .mockReturnValueOnce(undefined);
    spies.update.mockReturnValueOnce({
      ...secondDraft,
      status: "ACTIVE" as const,
      activatedAt: secondActivatedAt,
      updatedAt: secondActivatedAt,
    });

    const second = await run(db, { id: secondDraft.id }, ctx);
    expect(second.ok).toBe(true);

    if (first.ok && second.ok) {
      expect(first.value.approvalPolicy.activatedAt).toEqual(firstActivatedAt);
      expect(second.value.approvalPolicy.activatedAt).toEqual(secondActivatedAt);
      // Lineage: ordering by activatedAt ASC recovers (first, second).
      expect(
        (second.value.approvalPolicy.activatedAt?.getTime() ?? 0) >
          (first.value.approvalPolicy.activatedAt?.getTime() ?? 0),
      ).toBe(true);
    }
  });
});
