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

// Mock project-read-shared
const readProjectSummariesSnapshotMock = vi.fn();
const readProjectSummaryDetailsSnapshotMock = vi.fn();
const resolveProjectIdentifierSnapshotMock = vi.fn();
const readProjectByIdentifierDetailSnapshotMock = vi.fn();
const mapProjectSummaryMock = vi.fn().mockImplementation((p: unknown) => p);
const mapProjectSummaryDetailMock = vi.fn().mockImplementation((p: unknown) => p);
const mapProjectDetailMock = vi.fn().mockImplementation((p: unknown) => p);

vi.mock("../router/project-read-shared.js", () => ({
  readProjectSummariesSnapshot: (...args: unknown[]) => readProjectSummariesSnapshotMock(...args),
  readProjectSummaryDetailsSnapshot: (...args: unknown[]) =>
    readProjectSummaryDetailsSnapshotMock(...args),
  resolveProjectIdentifierSnapshot: (...args: unknown[]) =>
    resolveProjectIdentifierSnapshotMock(...args),
  readProjectByIdentifierDetailSnapshot: (...args: unknown[]) =>
    readProjectByIdentifierDetailSnapshotMock(...args),
  mapProjectSummary: (...args: unknown[]) => mapProjectSummaryMock(...args),
  mapProjectSummaryDetail: (...args: unknown[]) => mapProjectSummaryDetailMock(...args),
  mapProjectDetail: (...args: unknown[]) => mapProjectDetailMock(...args),
}));

// Mock application layer for project-cost-read
const listAssignmentBookingsMock = vi.fn().mockResolvedValue([]);
vi.mock("@capakraken/application", () => ({
  listAssignmentBookings: (...args: unknown[]) => listAssignmentBookingsMock(...args),
}));

// Mock pagination — use the real paginateCursor implementation
vi.mock("../db/pagination.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../db/pagination.js")>();
  return actual;
});

import { createCallerFactory, createTRPCRouter } from "../trpc.js";
import { projectIdentifierReadProcedures } from "../router/project-identifier-read.js";
import { projectCostReadProcedures } from "../router/project-cost-read.js";

const identifierRouter = createTRPCRouter(projectIdentifierReadProcedures);
const costRouter = createTRPCRouter(projectCostReadProcedures);
const createIdentifierCaller = createCallerFactory(identifierRouter);
const createCostCaller = createCallerFactory(costRouter);

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

function createControllerCaller(db: Record<string, unknown>) {
  return createCostCaller({
    session: {
      user: { email: "ctrl@example.com", name: "Controller", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: { id: "user_ctrl", systemRole: SystemRole.CONTROLLER, permissionOverrides: null },
  });
}

// ── Shared mock data ──────────────────────────────────────────────────────────

const mockProjectRecord = {
  id: "p1",
  name: "Test Project",
  shortCode: "TP",
  status: "ACTIVE",
  responsiblePerson: null,
  startDate: new Date("2026-01-01"),
  endDate: new Date("2026-12-31"),
};

const mockProject = {
  id: "p1",
  name: "Test Project",
  shortCode: "TP",
  budgetCents: 1_000_000,
  startDate: new Date("2026-01-01"),
  endDate: new Date("2026-12-31"),
  status: "ACTIVE",
};

const mockBooking = {
  projectId: "p1",
  resourceId: "r1",
  startDate: new Date("2026-01-01"),
  endDate: new Date("2026-01-10"),
  dailyCostCents: 5_000,
  hoursPerDay: 8,
  status: "ACTIVE",
};

// ─────────────────────────────────────────────────────────────────────────────
// resolveByIdentifier
// ─────────────────────────────────────────────────────────────────────────────

describe("resolveByIdentifier", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("finds a project by id on the first attempt", async () => {
    const db = {
      project: {
        findUnique: vi.fn().mockResolvedValueOnce(mockProjectRecord),
        findFirst: vi.fn(),
      },
    };
    const caller = createPlanningCaller(db);
    const result = await caller.resolveByIdentifier({ identifier: "p1" });

    expect(result).toEqual(mockProjectRecord);
    expect(db.project.findUnique).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: "p1" } }),
    );
    expect(db.project.findFirst).not.toHaveBeenCalled();
  });

  it("falls back to shortCode when id lookup returns null", async () => {
    const db = {
      project: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(null) // id lookup
          .mockResolvedValueOnce(mockProjectRecord), // shortCode lookup
        findFirst: vi.fn(),
      },
    };
    const caller = createPlanningCaller(db);
    const result = await caller.resolveByIdentifier({ identifier: "TP" });

    expect(result).toEqual(mockProjectRecord);
    expect(db.project.findUnique).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({ where: { shortCode: "TP" } }),
    );
    expect(db.project.findFirst).not.toHaveBeenCalled();
  });

  it("falls back to exact name match when shortCode lookup returns null", async () => {
    const db = {
      project: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(null) // id
          .mockResolvedValueOnce(null), // shortCode
        findFirst: vi.fn().mockResolvedValueOnce(mockProjectRecord), // exact name
      },
    };
    const caller = createPlanningCaller(db);
    const result = await caller.resolveByIdentifier({ identifier: "Test Project" });

    expect(result).toEqual(mockProjectRecord);
    expect(db.project.findFirst).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        where: { name: { equals: "Test Project", mode: "insensitive" } },
      }),
    );
  });

  it("falls back to contains name match when exact name returns null", async () => {
    const db = {
      project: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(null) // id
          .mockResolvedValueOnce(null), // shortCode
        findFirst: vi
          .fn()
          .mockResolvedValueOnce(null) // exact name
          .mockResolvedValueOnce(mockProjectRecord), // contains name
      },
    };
    const caller = createPlanningCaller(db);
    const result = await caller.resolveByIdentifier({ identifier: "Test" });

    expect(result).toEqual(mockProjectRecord);
    expect(db.project.findFirst).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        where: { name: { contains: "Test", mode: "insensitive" } },
      }),
    );
  });

  it("throws NOT_FOUND when all lookups fail", async () => {
    const db = {
      project: {
        findUnique: vi.fn().mockResolvedValue(null),
        findFirst: vi.fn().mockResolvedValue(null),
      },
    };
    const caller = createPlanningCaller(db);

    await expect(
      caller.resolveByIdentifier({ identifier: "does-not-exist" }),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// searchSummaries
// ─────────────────────────────────────────────────────────────────────────────

describe("searchSummaries", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mapProjectSummaryMock.mockImplementation((p: unknown) => p);
  });

  it("returns mapped results directly when exactMatch is true", async () => {
    const item = { id: "p1", name: "Alpha" };
    readProjectSummariesSnapshotMock.mockResolvedValueOnce({ items: [item], exactMatch: true });

    const caller = createPlanningCaller({});
    const result = await caller.searchSummaries({ search: "Alpha" });

    expect(result).toEqual([item]);
  });

  it("returns suggestions with a note when search provided but no exactMatch", async () => {
    const item = { id: "p2", name: "Alpha Beta" };
    readProjectSummariesSnapshotMock.mockResolvedValueOnce({ items: [item], exactMatch: false });

    const caller = createPlanningCaller({});
    const result = await caller.searchSummaries({ search: "Alpha" });

    expect(result).toMatchObject({
      suggestions: [item],
      note: expect.stringContaining("Alpha"),
    });
  });

  it("returns an empty array when there are no results", async () => {
    readProjectSummariesSnapshotMock.mockResolvedValueOnce({ items: [], exactMatch: false });

    const caller = createPlanningCaller({});
    const result = await caller.searchSummaries({ search: "nothing" });

    expect(result).toEqual([]);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// getByIdentifier
// ─────────────────────────────────────────────────────────────────────────────

describe("getByIdentifier", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("delegates to resolveProjectIdentifierSnapshot", async () => {
    resolveProjectIdentifierSnapshotMock.mockResolvedValueOnce(mockProjectRecord);

    const caller = createPlanningCaller({});
    const result = await caller.getByIdentifier({ identifier: "p1" });

    expect(result).toEqual(mockProjectRecord);
    expect(resolveProjectIdentifierSnapshotMock).toHaveBeenCalledOnce();
  });

  it("passes the identifier correctly to the snapshot function", async () => {
    resolveProjectIdentifierSnapshotMock.mockResolvedValueOnce(mockProjectRecord);

    const caller = createPlanningCaller({});
    await caller.getByIdentifier({ identifier: "TP" });

    expect(resolveProjectIdentifierSnapshotMock).toHaveBeenCalledWith(expect.anything(), "TP");
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// listWithCosts
// ─────────────────────────────────────────────────────────────────────────────

describe("listWithCosts", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    listAssignmentBookingsMock.mockResolvedValue([]);
  });

  it("returns projects with calculated cost metrics", async () => {
    const db = {
      project: {
        findMany: vi.fn().mockResolvedValue([mockProject]),
      },
    };
    listAssignmentBookingsMock.mockResolvedValue([mockBooking]);

    const caller = createControllerCaller(db);
    const result = await caller.listWithCosts({});

    expect(result.projects).toHaveLength(1);
    const project = result.projects[0]!;
    expect(project.id).toBe("p1");
    expect(project.totalCostCents).toBeGreaterThan(0);
    expect(project.totalPersonDays).toBeGreaterThan(0);
    expect(project.utilizationPercent).toBeGreaterThan(0);
  });

  it("applies a status filter to the query", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const db = { project: { findMany } };

    const caller = createControllerCaller(db);
    await caller.listWithCosts({ status: ProjectStatus.ACTIVE });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({ status: ProjectStatus.ACTIVE }),
      }),
    );
  });

  it("applies a search filter with OR on name and shortCode", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const db = { project: { findMany } };

    const caller = createControllerCaller(db);
    await caller.listWithCosts({ search: "test" });

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

  it("calculates utilizationPercent as (totalCostCents / budgetCents) * 100", async () => {
    // 10 days @ 5000 = 50_000 cost, budget = 1_000_000 → 5%
    const db = {
      project: {
        findMany: vi.fn().mockResolvedValue([{ ...mockProject, budgetCents: 1_000_000 }]),
      },
    };
    listAssignmentBookingsMock.mockResolvedValue([mockBooking]);

    const caller = createControllerCaller(db);
    const result = await caller.listWithCosts({});

    const days = 10; // Jan 1–10 inclusive
    const expectedCost = mockBooking.dailyCostCents * days;
    const expectedUtilization = Math.round((expectedCost / 1_000_000) * 100);
    expect(result.projects[0]!.utilizationPercent).toBe(expectedUtilization);
  });

  it("returns 0 utilizationPercent when budgetCents is 0", async () => {
    const db = {
      project: {
        findMany: vi.fn().mockResolvedValue([{ ...mockProject, budgetCents: 0 }]),
      },
    };
    listAssignmentBookingsMock.mockResolvedValue([mockBooking]);

    const caller = createControllerCaller(db);
    const result = await caller.listWithCosts({});

    expect(result.projects[0]!.utilizationPercent).toBe(0);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// listWithCosts — cost calculation details
// ─────────────────────────────────────────────────────────────────────────────

describe("listWithCosts cost calculations", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("computes totalCostCents as dailyCostCents * days for each booking", async () => {
    // Jan 1 to Jan 10: 10 days inclusive
    const db = {
      project: {
        findMany: vi.fn().mockResolvedValue([mockProject]),
      },
    };
    listAssignmentBookingsMock.mockResolvedValue([mockBooking]);

    const caller = createControllerCaller(db);
    const result = await caller.listWithCosts({});

    const days = 10;
    expect(result.projects[0]!.totalCostCents).toBe(mockBooking.dailyCostCents * days);
  });

  it("computes totalPersonDays as (hoursPerDay * days) / 8", async () => {
    const db = {
      project: {
        findMany: vi.fn().mockResolvedValue([mockProject]),
      },
    };
    listAssignmentBookingsMock.mockResolvedValue([mockBooking]);

    const caller = createControllerCaller(db);
    const result = await caller.listWithCosts({});

    // (8 hours/day * 10 days) / 8 = 10 person-days
    const days = 10;
    const expectedPersonDays = (mockBooking.hoursPerDay * days) / 8;
    expect(result.projects[0]!.totalPersonDays).toBe(expectedPersonDays);
  });

  it("rounds totalPersonDays to 1 decimal place", async () => {
    // Use 6 hoursPerDay over 10 days: (6 * 10) / 8 = 7.5 → already 1dp
    // Use 3 days with 4h: (4 * 3) / 8 = 1.5 — use an odd combo to check rounding
    // Jan 1–3 = 3 days, 7h/day: (7 * 3) / 8 = 2.625 → rounded to 2.6
    const partialBooking = {
      ...mockBooking,
      startDate: new Date("2026-01-01"),
      endDate: new Date("2026-01-03"),
      hoursPerDay: 7,
    };
    const db = {
      project: {
        findMany: vi.fn().mockResolvedValue([mockProject]),
      },
    };
    listAssignmentBookingsMock.mockResolvedValue([partialBooking]);

    const caller = createControllerCaller(db);
    const result = await caller.listWithCosts({});

    const days = 3;
    const rawPersonDays = (7 * days) / 8; // 2.625
    const expected = Math.round(rawPersonDays * 10) / 10; // 2.6
    expect(result.projects[0]!.totalPersonDays).toBe(expected);
  });
});
