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 {
  ItemClosedError,
  ItemNotFoundError,
  InvalidPriorityError,
  InvalidTitleError,
  UserNotFoundError,
} from "../lib/errors.generated";
import { baseItem, closedItem } from "../testing/fixtures";
import { run } from "./updatePipelineItem";

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

  it("updates item title", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, title: "Updated title" }], []);

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

    expect(result.ok).toBe(true);
    expect(mock.updates[0].updateValues()).toEqual(
      expect.objectContaining({ title: "Updated title" }),
    );
  });

  it("passes consumer extension fields through to the update", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [baseItem], []);

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

    expect(result.ok).toBe(true);
    expect(mock.updates[0].updateValues()).toEqual(expect.objectContaining({ customValue: 5000 }));
  });

  it("updates item description", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, description: null }], []);

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

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

  it("updates item assignee", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, assigneeId: "user-9" }], []);

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, assigneeId: "user-9" }, ctx),
    );

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

  it("updates item priority", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, priority: "HIGH" }], []);

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

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

  it("updates item due date", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const dueDate = new Date("2024-01-20T00:00:00.000Z");
    mock.enqueueResults([baseItem], [{ ...baseItem, dueDate }], []);

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

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

  it("updates item position within stage", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, position: 3 }]);

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

    expect(result.ok).toBe(true);
    expect(mock.updates[0].updateValues()).toEqual(expect.objectContaining({ position: 3 }));
    // A position-only update is not an audited field change: no PipelineItemChange row is written.
    expect(mock.inserts).toHaveLength(0);
  });

  it("writes a PipelineItemChange when title changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, title: "Updated title" }], []);

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, title: "Updated title" }, ctx));

    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({
        itemId: baseItem.id,
        changedByUserId: ctx.actorId,
        changeType: "CONTENT",
        fieldName: "TITLE",
        prevValue: baseItem.title,
        newValue: "Updated title",
      }),
    ]);
  });

  it("writes a PipelineItemChange when description changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, description: "new description" }], []);

    await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, description: "new description" }, ctx),
    );

    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({
        changeType: "CONTENT",
        fieldName: "DESCRIPTION",
        prevValue: baseItem.description,
        newValue: "new description",
      }),
    ]);
  });

  it("does not write a PipelineItemChange when neither title nor description changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [baseItem]);

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

    expect(mock.inserts.length).toBe(0);
  });

  it("writes a PipelineItemChange when assignee changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, assigneeId: "user-9" }], []);

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, assigneeId: "user-9" }, ctx));

    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({
        itemId: baseItem.id,
        changedByUserId: ctx.actorId,
        changeType: "FIELD",
        fieldName: "ASSIGNEE",
        prevValue: baseItem.assigneeId,
        newValue: "user-9",
      }),
    ]);
  });

  it("writes a PipelineItemChange when priority changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, priority: "HIGH" }], []);

    await mock.withTx((trx) => run(trx, { itemId: baseItem.id, priority: "HIGH" }, ctx));

    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({
        changeType: "FIELD",
        fieldName: "PRIORITY",
        prevValue: baseItem.priority,
        newValue: "HIGH",
      }),
    ]);
  });

  it("writes a PipelineItemChange when due date changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const newDueDate = new Date("2024-02-15T00:00:00.000Z");
    mock.enqueueResults([baseItem], [{ ...baseItem, dueDate: newDueDate }], []);

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

    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({
        changeType: "FIELD",
        fieldName: "DUE_DATE",
        prevValue: baseItem.dueDate.toISOString(),
        newValue: newDueDate.toISOString(),
      }),
    ]);
  });

  it("does not write a PipelineItemChange when the field value is unchanged", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [baseItem]);

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

    expect(mock.inserts.length).toBe(0);
  });

  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" }, ctx));

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

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

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

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

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

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

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

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

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

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

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

    const result = await mock.withTx((trx) =>
      run(trx, { itemId: baseItem.id, assigneeId: "missing-user" }, ctx, {
        getUser: () => Promise.resolve(null),
      }),
    );

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

  it("clearing assignee via null skips the getUser check", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseItem], [{ ...baseItem, assigneeId: null }], []);

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

    expect(result.ok).toBe(true);
    expect(mock.updates[0].updateValues()).toEqual(expect.objectContaining({ assigneeId: null }));
  });

  it("passes extension fields through but strips reserved model columns", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const input = {
      itemId: baseItem.id,
      severity: "P1",
      lifecycle: "CLOSED",
      stageId: "spoofed-stage",
      pipelineId: "spoofed-pipeline",
      itemNumber: 999,
      id: "spoofed-id",
    };
    mock.enqueueResults([baseItem], [baseItem]);

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

    expect(result.ok).toBe(true);
    const values = mock.updates[0].updateValues();
    expect(values).toEqual(expect.objectContaining({ severity: "P1" }));
    expect(values).not.toHaveProperty("lifecycle");
    expect(values).not.toHaveProperty("stageId");
    expect(values).not.toHaveProperty("pipelineId");
    expect(values).not.toHaveProperty("itemNumber");
    expect(values).not.toHaveProperty("id");
  });
});
