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 {
  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 mock = createKyselyMock<Namespace["main-db"]>();
    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"),
    };
    mock.enqueueResults(
      [draftPolicy],
      [policyStep1, policyStep2],
      [{ approvalPolicyStepId: policyStep1.id }, { approvalPolicyStepId: policyStep2.id }],
      [],
      [activated],
    );

    const result = await mock.withTx((trx) => run(trx, { 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(mock.updates.length).toBeGreaterThan(0);
    const setArgs = mock.updates[0].updateValues();
    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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [draftPolicy],
      [policyStep1],
      [{ approvalPolicyStepId: policyStep1.id }],
      [],
      [
        {
          ...draftPolicy,
          status: "ACTIVE" as const,
          activatedAt: new Date(),
          updatedAt: new Date(),
        },
      ],
    );

    const result = await mock.withTx((trx) => run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    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"),
    };
    mock.enqueueResults(
      [draftPolicy],
      [policyStep1],
      [{ approvalPolicyStepId: policyStep1.id }],
      [],
      [activated],
    );

    const result = await mock.withTx((trx) => run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [draftPolicy],
      [policyStep1],
      [{ approvalPolicyStepId: policyStep1.id }],
      [activePolicy],
    );

    const result = await mock.withTx((trx) => run(trx, { id: draftPolicy.id }, ctx));

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

  it("allows activation after the prior ACTIVE policy is deactivated", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [draftPolicy],
      [policyStep1],
      [{ approvalPolicyStepId: policyStep1.id }],
      [],
      [
        {
          ...draftPolicy,
          status: "ACTIVE" as const,
          activatedAt: new Date(),
          updatedAt: new Date(),
        },
      ],
    );

    const result = await mock.withTx((trx) => run(trx, { id: draftPolicy.id }, ctx));

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

  it("throws INVALID_STATUS_TRANSITION when the policy is already ACTIVE", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([activePolicy]);

    const result = await mock.withTx((trx) => run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([inactivePolicy]);

    const result = await mock.withTx((trx) => run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) => run(trx, { 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftPolicy], []);

    const result = await mock.withTx((trx) => run(trx, { id: draftPolicy.id }, ctx));

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

  it("throws POLICY_NOT_READY_TO_ACTIVATE when any step has zero assignees", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [draftPolicy],
      [policyStep1, policyStep2],
      [{ approvalPolicyStepId: policyStep1.id }],
    );

    const result = await mock.withTx((trx) => run(trx, { id: draftPolicy.id }, ctx));

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

  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 mock = createKyselyMock<Namespace["main-db"]>();
    const firstActivatedAt = new Date("2024-01-01T00:00:00.000Z");
    const secondActivatedAt = new Date("2024-06-01T00:00:00.000Z");

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

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

    const second = await mock.withTx((trx) => run(trx, { 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);
    }
  });
});
