import { SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";

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

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

import { clientRouter } from "../router/client.js";
import { createCallerFactory } from "../trpc.js";

const createCaller = createCallerFactory(clientRouter);

function createPlanningCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "planning@example.com", name: "Planning", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_planning",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: null,
    },
    permissions: new Set(["view:planning"]),
  });
}

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "manager@example.com", name: "Manager", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_manager",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: null,
    },
  });
}

function createAdminCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "admin@example.com", name: "Admin", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_admin",
      systemRole: SystemRole.ADMIN,
      permissionOverrides: null,
    },
  });
}

// ─── client.list ──────────────────────────────────────────────────────────────

describe("client.list", () => {
  it("returns all clients with count includes when no filter is provided", async () => {
    const clients = [
      { id: "client_1", name: "Acme" },
      { id: "client_2", name: "Beta" },
    ];
    const findMany = vi.fn().mockResolvedValue(clients);
    const caller = createPlanningCaller({ client: { findMany } });

    const result = await caller.list(undefined);

    expect(result).toEqual(clients);
    expect(findMany).toHaveBeenCalledWith({
      where: {},
      include: { _count: { select: { children: true, projects: true } } },
      orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
    });
  });

  it("filters by isActive and parentId when both are provided", async () => {
    const findMany = vi.fn().mockResolvedValue([{ id: "client_1", name: "Acme" }]);
    const caller = createPlanningCaller({ client: { findMany } });

    const result = await caller.list({ parentId: "parent_1", isActive: true, search: "Acme" });

    expect(findMany).toHaveBeenCalledWith({
      where: {
        parentId: "parent_1",
        isActive: true,
        OR: [
          { name: { contains: "Acme", mode: "insensitive" } },
          { code: { contains: "Acme", mode: "insensitive" } },
        ],
      },
      include: { _count: { select: { children: true, projects: true } } },
      orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
    });
    expect(result).toHaveLength(1);
  });

  it("filters by parentId: null to return only root clients", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createPlanningCaller({ client: { findMany } });

    await caller.list({ parentId: null });

    expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { parentId: null } }));
  });

  it("applies case-insensitive name/code search without other filters", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createPlanningCaller({ client: { findMany } });

    await caller.list({ search: "acme" });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          OR: [
            { name: { contains: "acme", mode: "insensitive" } },
            { code: { contains: "acme", mode: "insensitive" } },
          ],
        },
      }),
    );
  });
});

// ─── client.getById ───────────────────────────────────────────────────────────

describe("client.getById", () => {
  it("returns the client with parent, children, and counts when found", async () => {
    const client = {
      id: "client_1",
      name: "Acme",
      code: "ACME",
      parentId: null,
      parent: null,
      isActive: true,
      sortOrder: 0,
      tags: [],
      createdAt: new Date("2026-01-01T00:00:00.000Z"),
      updatedAt: new Date("2026-01-01T00:00:00.000Z"),
      children: [],
      _count: { projects: 2, children: 0 },
    };
    const findUnique = vi.fn().mockResolvedValue(client);
    const caller = createAdminCaller({ client: { findUnique } });

    const result = await caller.getById({ id: "client_1" });

    expect(result).toEqual(client);
    expect(findUnique).toHaveBeenCalledWith({
      where: { id: "client_1" },
      include: {
        parent: true,
        children: { orderBy: { sortOrder: "asc" } },
        _count: { select: { projects: true, children: true } },
      },
    });
  });

  it("throws NOT_FOUND when no client matches the given id", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);
    const caller = createAdminCaller({ client: { findUnique } });

    await expect(caller.getById({ id: "missing" })).rejects.toMatchObject({
      code: "NOT_FOUND",
      message: "Client not found",
    });
  });

  it("passes the id through to the DB query unchanged", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);
    const caller = createAdminCaller({ client: { findUnique } });

    await expect(caller.getById({ id: "client_xyz_123" })).rejects.toBeDefined();
    expect(findUnique).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: "client_xyz_123" } }),
    );
  });

  it("calls the DB exactly once for a successful lookup", async () => {
    const client = {
      id: "client_2",
      name: "Beta",
      parent: null,
      children: [],
      _count: { projects: 0, children: 0 },
    };
    const findUnique = vi.fn().mockResolvedValue(client);
    const caller = createAdminCaller({ client: { findUnique } });

    await caller.getById({ id: "client_2" });

    expect(findUnique).toHaveBeenCalledTimes(1);
  });
});

// ─── client.create ────────────────────────────────────────────────────────────

describe("client.create", () => {
  it("creates a client without a code and returns the new record", async () => {
    const created = {
      id: "c_new",
      name: "New Client",
      code: null,
      parentId: null,
      isActive: true,
      sortOrder: 0,
      tags: [],
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    const create = vi.fn().mockResolvedValue(created);
    const caller = createManagerCaller({ client: { create } });

    const result = await caller.create({ name: "New Client", sortOrder: 0 });

    expect(result).toEqual(created);
    expect(create).toHaveBeenCalledWith({ data: { name: "New Client", sortOrder: 0 } });
    expect(createAuditEntry).toHaveBeenCalled();
  });

  it("checks code availability and creates the client when code is unique", async () => {
    const created = {
      id: "c_coded",
      name: "Coded Client",
      code: "CLI01",
      parentId: null,
      isActive: true,
      sortOrder: 5,
      tags: [],
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    // findUnique for assertClientCodeAvailable returns null = code available
    const findUnique = vi.fn().mockResolvedValue(null);
    const create = vi.fn().mockResolvedValue(created);
    const caller = createManagerCaller({ client: { findUnique, create } });

    const result = await caller.create({ name: "Coded Client", code: "CLI01", sortOrder: 5 });

    expect(result).toEqual(created);
    expect(findUnique).toHaveBeenCalledWith({ where: { code: "CLI01" } });
    expect(create).toHaveBeenCalledWith({
      data: { name: "Coded Client", code: "CLI01", sortOrder: 5 },
    });
  });

  it("rejects creation with CONFLICT when the code is already taken", async () => {
    const existingWithSameCode = { id: "c_other", code: "DUP" };
    const findUnique = vi.fn().mockResolvedValue(existingWithSameCode);
    const create = vi.fn();
    const caller = createManagerCaller({ client: { findUnique, create } });

    await expect(
      caller.create({ name: "Duplicate Code Client", code: "DUP", sortOrder: 0 }),
    ).rejects.toMatchObject({
      code: "CONFLICT",
      message: 'Client code "DUP" already exists',
    });

    expect(create).not.toHaveBeenCalled();
  });

  it("rejects creation with NOT_FOUND when parentId references a missing parent", async () => {
    // findUnique for parent lookup returns null
    const findUnique = vi.fn().mockResolvedValue(null);
    const create = vi.fn();
    const caller = createManagerCaller({ client: { findUnique, create } });

    await expect(
      caller.create({ name: "Orphan", parentId: "missing_parent", sortOrder: 0 }),
    ).rejects.toMatchObject({
      code: "NOT_FOUND",
      message: "Parent client not found",
    });

    expect(create).not.toHaveBeenCalled();
  });
});

// ─── client.update ────────────────────────────────────────────────────────────

describe("client.update", () => {
  it("updates the name of an existing client and returns the updated record", async () => {
    const existing = {
      id: "client_1",
      name: "Old Name",
      code: null,
      isActive: true,
      sortOrder: 0,
      tags: [],
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    const updated = { ...existing, name: "New Name" };
    const findUnique = vi.fn().mockResolvedValue(existing);
    const update = vi.fn().mockResolvedValue(updated);
    const caller = createManagerCaller({ client: { findUnique, update } });

    const result = await caller.update({ id: "client_1", data: { name: "New Name" } });

    expect(result).toEqual(updated);
    expect(update).toHaveBeenCalledWith({
      where: { id: "client_1" },
      data: { name: "New Name" },
    });
    expect(createAuditEntry).toHaveBeenCalled();
  });

  it("throws NOT_FOUND when the target client does not exist", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);
    const update = vi.fn();
    const caller = createManagerCaller({ client: { findUnique, update } });

    await expect(caller.update({ id: "missing", data: { name: "Ghost" } })).rejects.toMatchObject({
      code: "NOT_FOUND",
      message: "Client not found",
    });

    expect(update).not.toHaveBeenCalled();
  });

  it("rejects update with CONFLICT when the new code is already taken by another client", async () => {
    const existing = {
      id: "client_1",
      name: "Acme",
      code: "OLD_CODE",
      isActive: true,
      sortOrder: 0,
      tags: [],
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    const conflicting = { id: "client_2", code: "NEW_CODE" };
    const findUnique = vi
      .fn()
      .mockResolvedValueOnce(existing) // findUniqueOrThrow for existing client
      .mockResolvedValueOnce(conflicting); // assertClientCodeAvailable
    const update = vi.fn();
    const caller = createManagerCaller({ client: { findUnique, update } });

    await expect(
      caller.update({ id: "client_1", data: { code: "NEW_CODE" } }),
    ).rejects.toMatchObject({
      code: "CONFLICT",
      message: 'Client code "NEW_CODE" already exists',
    });

    expect(update).not.toHaveBeenCalled();
  });

  it("skips code availability check when the code is unchanged", async () => {
    const existing = {
      id: "client_1",
      name: "Acme",
      code: "SAME_CODE",
      isActive: true,
      sortOrder: 0,
      tags: [],
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    const updated = { ...existing, sortOrder: 10 };
    // Only one findUnique call expected — no assertClientCodeAvailable call
    const findUnique = vi.fn().mockResolvedValue(existing);
    const update = vi.fn().mockResolvedValue(updated);
    const caller = createManagerCaller({ client: { findUnique, update } });

    const result = await caller.update({
      id: "client_1",
      data: { code: "SAME_CODE", sortOrder: 10 },
    });

    expect(result).toEqual(updated);
    // findUnique called exactly once: for the existence check, not for code conflict
    expect(findUnique).toHaveBeenCalledTimes(1);
    expect(update).toHaveBeenCalledWith({
      where: { id: "client_1" },
      data: { code: "SAME_CODE", sortOrder: 10 },
    });
  });
});
