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 {
  ItemNotFoundError,
  ItemNotOpenError,
  SameStageError,
  StageNotFoundError,
  StageNotOnPipelineError,
} from "../lib/errors.generated";
import {
  inProgressStage,
  baseItem,
  baseStage,
  canceledStage,
  closedItem,
  doneStage,
  draftItem,
} from "../testing/fixtures";
import { run } from "./movePipelineItem";

describe("movePipelineItem", () => {
  const ctx: CommandContext = {
    actorId: "test-actor",
    permissions: ["pipeline-management:item:movePipelineItem"],
  };

  const setValues = (mock: ReturnType<typeof createKyselyMock<Namespace["main-db"]>>) =>
    mock.updates.map((u) => u.updateValues());

  it("moves item to a different stage on the same pipeline", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [],
      [{ ...baseItem, stageId: inProgressStage.id }],
      [{ id: "transition-1" }],
    );

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: inProgressStage.id }, ctx),
    );

    expect(result.ok).toBe(true);
    expect(setValues(mock)).toEqual(
      expect.arrayContaining([expect.objectContaining({ stageId: inProgressStage.id })]),
    );
  });

  it("creates a PipelineStageTransition record", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [],
      [{ ...baseItem, stageId: inProgressStage.id }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, stageId: inProgressStage.id }, ctx));

    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        itemId: baseItem.id,
        fromStageId: baseItem.stageId,
        toStageId: inProgressStage.id,
        movedByUserId: ctx.actorId,
      }),
    );
  });

  it("keeps lifecycle OPEN when target category is DONE (no auto-close)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseItem],
      [doneStage],
      [],
      [{ ...baseItem, stageId: doneStage.id }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, stageId: doneStage.id }, ctx));

    // movePipelineItem no longer touches lifecycle directly — the existing OPEN value
    // is preserved by leaving the column out of the update set.
    expect(setValues(mock)).not.toContainEqual(expect.objectContaining({ lifecycle: "CLOSED" }));
  });

  it("keeps lifecycle OPEN when target category is CANCELED (no auto-close)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseItem],
      [canceledStage],
      [],
      [{ ...baseItem, stageId: canceledStage.id }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, stageId: canceledStage.id }, ctx));

    expect(setValues(mock)).not.toContainEqual(expect.objectContaining({ lifecycle: "CLOSED" }));
  });

  it("keeps lifecycle OPEN when target category is NOT_STARTED or IN_PROGRESS", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [],
      [{ ...baseItem, stageId: inProgressStage.id }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, stageId: inProgressStage.id }, ctx));

    expect(setValues(mock)).not.toContainEqual(expect.objectContaining({ lifecycle: "CLOSED" }));
  });

  it("returns error when item not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: "missing-item", stageId: inProgressStage.id }, ctx),
    );

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

  it("returns error when item is not OPEN (CLOSED)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([closedItem]);

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: closedItem.id, stageId: inProgressStage.id }, ctx),
    );

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

  it("returns error when item is not OPEN (DRAFT)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([draftItem]);

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: draftItem.id, stageId: inProgressStage.id }, ctx),
    );

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

  it("returns error when target stage not found", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], []);

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: "missing-stage" }, ctx),
    );

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

  it("returns error when target stage is on a different pipeline", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...inProgressStage, pipelineId: "other-pipeline" }]);

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: inProgressStage.id }, ctx),
    );

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

  it("appends to end when position is not provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const sibling1 = { ...baseItem, id: "sibling-1", stageId: inProgressStage.id, position: 1 };
    const sibling2 = { ...baseItem, id: "sibling-2", stageId: inProgressStage.id, position: 2 };
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [sibling1, sibling2],
      [{ ...baseItem, stageId: inProgressStage.id, position: 3 }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, stageId: inProgressStage.id }, ctx));

    expect(setValues(mock)).toEqual(
      expect.arrayContaining([expect.objectContaining({ position: 3 })]),
    );
  });

  it("inserts at the requested 1-based position when provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const sibling1 = { ...baseItem, id: "sibling-1", stageId: inProgressStage.id, position: 1 };
    const sibling2 = { ...baseItem, id: "sibling-2", stageId: inProgressStage.id, position: 2 };
    const sibling3 = { ...baseItem, id: "sibling-3", stageId: inProgressStage.id, position: 3 };
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [sibling1, sibling2, sibling3],
      [{ id: sibling2.id }],
      [{ id: sibling3.id }],
      [{ ...baseItem, stageId: inProgressStage.id, position: 2 }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: inProgressStage.id, position: 2 }, ctx),
    );

    const values = setValues(mock);
    expect(values).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ position: 2 }),
        expect.objectContaining({ position: 3 }),
        expect.objectContaining({ position: 4 }),
      ]),
    );
  });

  it("clamps an out-of-range position to the end of the target stage", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const sibling1 = { ...baseItem, id: "sibling-1", stageId: inProgressStage.id, position: 1 };
    const sibling2 = { ...baseItem, id: "sibling-2", stageId: inProgressStage.id, position: 2 };
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [sibling1, sibling2],
      [{ ...baseItem, stageId: inProgressStage.id, position: 3 }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: inProgressStage.id, position: 99 }, ctx),
    );

    expect(setValues(mock)).toEqual(
      expect.arrayContaining([expect.objectContaining({ position: 3 })]),
    );
  });

  it("clamps a zero or negative position to the top of the target stage", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const sibling1 = { ...baseItem, id: "sibling-1", stageId: inProgressStage.id, position: 1 };
    const sibling2 = { ...baseItem, id: "sibling-2", stageId: inProgressStage.id, position: 2 };
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [sibling1, sibling2],
      [{ id: sibling1.id }],
      [{ id: sibling2.id }],
      [{ ...baseItem, stageId: inProgressStage.id, position: 1 }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: inProgressStage.id, position: 0 }, ctx),
    );

    // Moved card takes slot 1; both siblings shift down to 2 and 3.
    const values = setValues(mock);
    expect(values).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ position: 1 }),
        expect.objectContaining({ position: 2 }),
        expect.objectContaining({ position: 3 }),
      ]),
    );
  });

  it("inserts at the chosen slot even when sibling positions are sparse (no top-snap)", async () => {
    // Regression: cross-lane positions are not guaranteed to be a clean 1..N run
    // (createPipelineItem assigns max+1, lane moves leave gaps). Sparse positions whose
    // values all exceed the requested slot used to make the card snap to the top.
    const mock = createKyselyMock<Namespace["main-db"]>();
    const sibling1 = { ...baseItem, id: "sibling-1", stageId: inProgressStage.id, position: 10 };
    const sibling2 = { ...baseItem, id: "sibling-2", stageId: inProgressStage.id, position: 20 };
    const sibling3 = { ...baseItem, id: "sibling-3", stageId: inProgressStage.id, position: 30 };
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [sibling1, sibling2, sibling3],
      [{ id: sibling1.id }],
      [{ id: sibling2.id }],
      [{ id: sibling3.id }],
      [{ ...baseItem, stageId: inProgressStage.id, position: 2 }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: inProgressStage.id, position: 2 }, ctx),
    );

    // Lane renormalized to 1..N: sibling1 -> 1, moved card -> 2, sibling2 -> 3, sibling3 -> 4.
    const values = setValues(mock);
    expect(values).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ position: 1 }),
        expect.objectContaining({ position: 2 }),
        expect.objectContaining({ position: 3 }),
        expect.objectContaining({ position: 4 }),
      ]),
    );
  });

  it("inserts at the chosen slot even when sibling positions are duplicated (no bottom-snap)", async () => {
    // Regression: when stored positions are all equal (e.g. legacy 0s), shifting
    // only siblings at/after the slot left none to shift, so the card landed last.
    const mock = createKyselyMock<Namespace["main-db"]>();
    const sibling1 = { ...baseItem, id: "sibling-1", stageId: inProgressStage.id, position: 0 };
    const sibling2 = { ...baseItem, id: "sibling-2", stageId: inProgressStage.id, position: 0 };
    const sibling3 = { ...baseItem, id: "sibling-3", stageId: inProgressStage.id, position: 0 };
    mock.enqueueResults(
      [baseItem],
      [inProgressStage],
      [sibling1, sibling2, sibling3],
      [{ id: sibling1.id }],
      [{ id: sibling2.id }],
      [{ id: sibling3.id }],
      [{ ...baseItem, stageId: inProgressStage.id, position: 2 }],
      [{ id: "transition-1" }],
    );

    await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: inProgressStage.id, position: 2 }, ctx),
    );

    // Moved card takes slot 2, and the siblings at/after it are pushed to 3 and 4.
    const values = setValues(mock);
    expect(values).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ position: 2 }),
        expect.objectContaining({ position: 3 }),
        expect.objectContaining({ position: 4 }),
      ]),
    );
  });

  it("returns error when target stage is the same as current stage", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [baseStage]);

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, stageId: baseStage.id }, ctx),
    );

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