import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";

const { createAuditEntry } = vi.hoisted(() => ({
  createAuditEntry: vi.fn(),
}));

vi.mock("../lib/audit.js", () => ({
  createAuditEntry,
}));

import {
  batchUpdateClientSortOrder,
  createClient,
  deleteClient,
  updateClient,
} from "../router/client-procedure-support.js";

function createContext(db: Record<string, unknown>) {
  return {
    db: db as never,
    dbUser: { id: "user_1" } as never,
  };
}

describe("client procedure support", () => {
  beforeEach(() => {
    createAuditEntry.mockReset();
  });

  it("creates a client after validating parent and code uniqueness", async () => {
    const findUnique = vi
      .fn()
      .mockResolvedValueOnce({ id: "parent_1", name: "Parent" })
      .mockResolvedValueOnce(null);
    const create = vi.fn().mockResolvedValue({
      id: "client_1",
      name: "Acme",
      code: "ACME",
      parentId: "parent_1",
      sortOrder: 12,
      tags: ["enterprise"],
      isActive: true,
    });

    const result = await createClient(
      createContext({
        client: {
          findUnique,
          create,
        },
      }),
      {
        name: "Acme",
        code: "ACME",
        parentId: "parent_1",
        sortOrder: 12,
        tags: ["enterprise"],
      },
    );

    expect(findUnique).toHaveBeenNthCalledWith(1, {
      where: { id: "parent_1" },
    });
    expect(findUnique).toHaveBeenNthCalledWith(2, {
      where: { code: "ACME" },
    });
    expect(create).toHaveBeenCalledWith({
      data: {
        name: "Acme",
        code: "ACME",
        parentId: "parent_1",
        sortOrder: 12,
        tags: ["enterprise"],
      },
    });
    expect(result.id).toBe("client_1");
    expect(createAuditEntry).toHaveBeenCalledWith(
      expect.objectContaining({
        entityType: "Client",
        action: "CREATE",
        entityId: "client_1",
        userId: "user_1",
      }),
    );
  });

  it("updates a client without rechecking an unchanged code", async () => {
    const findUnique = vi.fn().mockResolvedValue({
      id: "client_1",
      name: "Acme",
      code: "ACME",
      isActive: true,
    });
    const update = vi.fn().mockResolvedValue({
      id: "client_1",
      name: "Acme Updated",
      code: "ACME",
      isActive: true,
    });

    const result = await updateClient(
      createContext({
        client: {
          findUnique,
          update,
        },
      }),
      {
        id: "client_1",
        data: {
          name: "Acme Updated",
          code: "ACME",
        },
      },
    );

    expect(findUnique).toHaveBeenCalledTimes(1);
    expect(findUnique).toHaveBeenCalledWith({
      where: { id: "client_1" },
    });
    expect(update).toHaveBeenCalledWith({
      where: { id: "client_1" },
      data: {
        name: "Acme Updated",
        code: "ACME",
      },
    });
    expect(result.name).toBe("Acme Updated");
    expect(createAuditEntry).toHaveBeenCalledWith(
      expect.objectContaining({
        entityType: "Client",
        action: "UPDATE",
        entityId: "client_1",
        before: expect.objectContaining({ code: "ACME" }),
        after: expect.objectContaining({ name: "Acme Updated" }),
      }),
    );
  });

  it("checks code availability when the code changes", async () => {
    const findUnique = vi
      .fn()
      .mockResolvedValueOnce({
        id: "client_1",
        name: "Acme",
        code: "ACME",
        isActive: true,
      })
      .mockResolvedValueOnce(null);
    const update = vi.fn().mockResolvedValue({
      id: "client_1",
      name: "Acme",
      code: "ACME-2",
      isActive: true,
    });

    await updateClient(
      createContext({
        client: {
          findUnique,
          update,
        },
      }),
      {
        id: "client_1",
        data: {
          code: "ACME-2",
        },
      },
    );

    expect(findUnique).toHaveBeenNthCalledWith(2, {
      where: { code: "ACME-2" },
    });
    expect(update).toHaveBeenCalledWith({
      where: { id: "client_1" },
      data: { code: "ACME-2" },
    });
  });

  it("rejects deletion while projects or child clients still exist", async () => {
    const findUnique = vi.fn().mockResolvedValue({
      id: "client_1",
      name: "Acme",
      _count: { projects: 1, children: 0 },
    });
    const remove = vi.fn();

    await expect(
      deleteClient(
        createContext({
          client: {
            findUnique,
            delete: remove,
          },
        }),
        {
          id: "client_1",
        },
      ),
    ).rejects.toMatchObject({
      code: "PRECONDITION_FAILED",
      message: "Cannot delete client with 1 project(s). Deactivate instead.",
    });

    expect(remove).not.toHaveBeenCalled();
    expect(createAuditEntry).not.toHaveBeenCalled();
  });

  it("deletes a valid client and audits the deletion", async () => {
    const findUnique = vi.fn().mockResolvedValue({
      id: "client_1",
      name: "Acme",
      _count: { projects: 0, children: 0 },
    });
    const remove = vi.fn().mockResolvedValue({ id: "client_1" });

    const result = await deleteClient(
      createContext({
        client: {
          findUnique,
          delete: remove,
        },
      }),
      {
        id: "client_1",
      },
    );

    expect(remove).toHaveBeenCalledWith({
      where: { id: "client_1" },
    });
    expect(result).toEqual({
      id: "client_1",
      name: "Acme",
      _count: { projects: 0, children: 0 },
    });
    expect(createAuditEntry).toHaveBeenCalledWith(
      expect.objectContaining({
        entityType: "Client",
        action: "DELETE",
        entityId: "client_1",
        before: expect.objectContaining({ name: "Acme" }),
      }),
    );
  });

  it("batch-updates sort order and writes one audit per item", async () => {
    const update = vi.fn(
      ({ where, data }: { where: { id: string }; data: { sortOrder: number } }) =>
        Promise.resolve({ id: where.id, sortOrder: data.sortOrder }),
    );
    const $transaction = vi.fn().mockResolvedValue([]);

    const result = await batchUpdateClientSortOrder(
      createContext({
        $transaction,
        client: {
          update,
        },
      }),
      [
        { id: "client_1", sortOrder: 10 },
        { id: "client_2", sortOrder: 20 },
      ],
    );

    expect(update).toHaveBeenNthCalledWith(1, {
      where: { id: "client_1" },
      data: { sortOrder: 10 },
    });
    expect(update).toHaveBeenNthCalledWith(2, {
      where: { id: "client_2" },
      data: { sortOrder: 20 },
    });
    expect($transaction).toHaveBeenCalledTimes(1);
    expect($transaction.mock.calls[0]?.[0]).toHaveLength(2);
    expect(createAuditEntry).toHaveBeenCalledTimes(2);
    expect(createAuditEntry).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        entityId: "client_1",
        summary: "Updated sort order",
      }),
    );
    expect(createAuditEntry).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        entityId: "client_2",
        summary: "Updated sort order",
      }),
    );
    expect(result).toEqual({ ok: true });
  });

  it("surfaces precondition failures as TRPC errors", async () => {
    const findUnique = vi.fn().mockResolvedValue({
      id: "client_1",
      name: "Acme",
      _count: { projects: 0, children: 2 },
    });

    await expect(
      deleteClient(
        createContext({
          client: {
            findUnique,
            delete: vi.fn(),
          },
        }),
        {
          id: "client_1",
        },
      ),
    ).rejects.toBeInstanceOf(TRPCError);
  });
});
