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 {
  PipelineArchivedError,
  PipelineNotFoundError,
  InvalidPriorityError,
  InvalidTitleError,
  StageNotFoundError,
  UserNotFoundError,
} from "../lib/errors.generated";
import { archivedPipeline, basePipeline, baseItem, baseStage } from "../testing/fixtures";
import { run } from "./createPipelineItem";

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

  const input = {
    pipelineId: basePipeline.id,
    title: baseItem.title,
    description: baseItem.description ?? undefined,
    assigneeId: baseItem.assigneeId ?? undefined,
    priority: baseItem.priority,
    dueDate: baseItem.dueDate ?? undefined,
  };

  it("creates item on active pipeline with lifecycle OPEN by default", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline], [baseStage], [], [{ maxNum: 0 }], [], [baseItem]);

    const result = await mock.withTx((trx) => run(trx, input, ctx));

    expect(result.ok).toBe(true);
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({ title: baseItem.title, lifecycle: "OPEN" }),
    );
  });

  it("places item in first stage automatically", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline], [baseStage], [], [{ maxNum: 0 }], [], [baseItem]);

    await mock.withTx((trx) => run(trx, input, ctx));

    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({ stageId: baseStage.id }),
    );
  });

  it("sets position to last in stage", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePipeline],
      [baseStage],
      [{ ...baseItem, id: "item-2", position: 4 }],
      [{ maxNum: 0 }],
      [],
      [{ ...baseItem, position: 5 }],
    );

    await mock.withTx((trx) => run(trx, input, ctx));

    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ position: 5 }));
  });

  it("assigns itemNumber from the pipeline's monotonic counter", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const seqPipeline = { ...basePipeline, itemNumberSeq: 41 };
    mock.enqueueResults([seqPipeline], [baseStage], [], [], [baseItem]);

    await mock.withTx((trx) => run(trx, input, ctx));

    expect(mock.updates[0].updateValues()).toEqual(expect.objectContaining({ itemNumberSeq: 42 }));
    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ itemNumber: 42 }));
  });

  it("falls back to MAX(itemNumber) when the counter is unset", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline], [baseStage], [], [{ maxNum: 7 }], [], [baseItem]);

    await mock.withTx((trx) => run(trx, input, ctx));

    expect(mock.updates[0].updateValues()).toEqual(expect.objectContaining({ itemNumberSeq: 8 }));
    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ itemNumber: 8 }));
  });

  it("passes consumer extension fields through to the insert", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline], [baseStage], [], [{ maxNum: 0 }], [], [baseItem]);

    await mock.withTx((trx) => run(trx, { ...input, customValue: 5000 }, ctx));

    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ customValue: 5000 }));
  });

  it("defaults priority to MEDIUM", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline], [baseStage], [], [{ maxNum: 0 }], [], [baseItem]);

    await mock.withTx((trx) => run(trx, { ...input, priority: undefined }, ctx));

    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ priority: "MEDIUM" }));
  });

  it("returns error when priority is invalid", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline]);

    const result = await mock.withTx((trx) =>
      run(trx, { ...input, priority: "NOT_A_PRIORITY" }, ctx),
    );

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

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

    const result = await mock.withTx((trx) => run(trx, input, ctx));

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

  it("returns error when pipeline is archived", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([archivedPipeline]);

    const result = await mock.withTx((trx) => run(trx, input, ctx));

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

  it("creates item with lifecycle DRAFT when asDraft is true", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePipeline],
      [baseStage],
      [],
      [{ maxNum: 0 }],
      [],
      [{ ...baseItem, lifecycle: "DRAFT" }],
    );

    const result = await mock.withTx((trx) => run(trx, { ...input, asDraft: true }, ctx));

    expect(result.ok).toBe(true);
    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ lifecycle: "DRAFT" }));
  });

  it("returns error when title is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline]);

    const result = await mock.withTx((trx) => run(trx, { ...input, title: "" }, ctx));

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

  it("returns STAGE_NOT_FOUND when the pipeline has no stages", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline], []);

    const result = await mock.withTx((trx) => run(trx, input, ctx));

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

  it("returns USER_NOT_FOUND when getUser dep returns null for assigneeId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline]);

    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, { getUser: () => Promise.resolve(null) }),
    );

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

  it("accepts assigneeId when getUser dep resolves the user", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([basePipeline], [baseStage], [], [{ maxNum: 0 }], [], [baseItem]);

    const result = await mock.withTx((trx) =>
      run(trx, input, ctx, { getUser: (id) => Promise.resolve({ id }) }),
    );

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