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

// Mock the vacation read support
const buildVacationPreviewMock = vi
  .fn()
  .mockReturnValue({ workingDays: 5, holidays: 1, totalDays: 7 });
const findVacationResourceChapterMock = vi.fn();
const listChapterVacationOverlapsMock = vi.fn().mockResolvedValue([]);

vi.mock("../router/vacation-read-support.js", () => ({
  buildVacationPreview: (...args: unknown[]) => buildVacationPreviewMock(...args),
  findVacationResourceChapter: (...args: unknown[]) => findVacationResourceChapterMock(...args),
  listChapterVacationOverlaps: (...args: unknown[]) => listChapterVacationOverlapsMock(...args),
}));

// Mock resource owned read access
vi.mock("../router/resource-owned-read-access.js", () => ({
  assertCanReadOwnedResource: vi.fn().mockResolvedValue(undefined),
  canManageOwnedResourceReads: vi.fn().mockReturnValue(true),
  resolveOwnedResourceReadFilter: vi.fn().mockResolvedValue("resource_1"),
}));

// Mock resource holiday context
vi.mock("../lib/resource-holiday-context.js", () => ({
  loadResourceHolidayContext: vi.fn().mockResolvedValue({ holidays: [] }),
}));

// Mock anonymization (pass-through)
vi.mock("../lib/anonymization.js", () => ({
  anonymizeResource: (r: unknown) => r,
  anonymizeUser: (u: unknown) => u,
  getAnonymizationDirectory: vi.fn().mockResolvedValue(null),
}));

// Mock db helpers
vi.mock("../db/helpers.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../db/helpers.js")>();
  return actual;
});

// Mock selects
vi.mock("../db/selects.js", () => ({
  RESOURCE_BRIEF_SELECT: { id: true, displayName: true, eid: true, lcrCents: true, chapter: true },
}));

import { isSameUtcDay } from "../router/vacation-read.js";
import { createCallerFactory, createTRPCRouter } from "../trpc.js";
import { vacationReadProcedures } from "../router/vacation-read.js";

const router = createTRPCRouter(vacationReadProcedures);
const createCaller = createCallerFactory(router);

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    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 },
  });
}

const mockVacation = {
  id: "vac_1",
  resourceId: "resource_1",
  type: VacationType.VACATION,
  status: VacationStatus.APPROVED,
  startDate: new Date("2026-06-01"),
  endDate: new Date("2026-06-05"),
  requestedById: "user_mgr",
  resource: {
    id: "resource_1",
    displayName: "Test User",
    eid: "E001",
    lcrCents: 5000,
    chapter: "Engineering",
    userId: "user_mgr",
  },
  requestedBy: { id: "user_mgr", name: "Manager", email: "mgr@example.com" },
  approvedBy: null,
};

describe("isSameUtcDay", () => {
  it("returns true for the same UTC day", () => {
    const a = new Date("2026-06-15T00:00:00.000Z");
    const b = new Date("2026-06-15T00:00:00.000Z");
    expect(isSameUtcDay(a, b)).toBe(true);
  });

  it("returns false for different UTC days", () => {
    const a = new Date("2026-06-15T00:00:00.000Z");
    const b = new Date("2026-06-16T00:00:00.000Z");
    expect(isSameUtcDay(a, b)).toBe(false);
  });

  it("returns true for same UTC day with different times", () => {
    const a = new Date("2026-06-15T08:00:00.000Z");
    const b = new Date("2026-06-15T23:59:59.999Z");
    expect(isSameUtcDay(a, b)).toBe(true);
  });
});

describe("list", () => {
  beforeEach(async () => {
    vi.clearAllMocks();
    // Re-apply stable defaults after clearAllMocks
    const { resolveOwnedResourceReadFilter, canManageOwnedResourceReads } = vi.mocked(
      await import("../router/resource-owned-read-access.js"),
    );
    resolveOwnedResourceReadFilter.mockResolvedValue("resource_1");
    canManageOwnedResourceReads.mockReturnValue(true);
    const { getAnonymizationDirectory } = vi.mocked(await import("../lib/anonymization.js"));
    getAnonymizationDirectory.mockResolvedValue(null);
  });

  it("returns vacations filtered by resource with date range and status filter", async () => {
    const findMany = vi.fn().mockResolvedValue([mockVacation]);
    const caller = createManagerCaller({ vacation: { findMany } });

    const result = await caller.list({
      resourceId: "resource_1",
      status: VacationStatus.APPROVED,
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-30"),
    });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          resourceId: "resource_1",
          status: VacationStatus.APPROVED,
          endDate: { gte: new Date("2026-06-01") },
          startDate: { lte: new Date("2026-06-30") },
        }),
        orderBy: { startDate: "asc" },
      }),
    );
    expect(result).toHaveLength(1);
    expect(result[0]!.id).toBe("vac_1");
  });

  it("applies the default limit of 100 when limit is not specified", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createManagerCaller({ vacation: { findMany } });

    await caller.list({});

    expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 100 }));
  });

  it("respects an explicit limit parameter", async () => {
    const findMany = vi.fn().mockResolvedValue([mockVacation]);
    const caller = createManagerCaller({ vacation: { findMany } });

    await caller.list({ limit: 10 });

    expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 10 }));
  });
});

describe("getById", () => {
  beforeEach(async () => {
    vi.clearAllMocks();
    const { canManageOwnedResourceReads } = vi.mocked(
      await import("../router/resource-owned-read-access.js"),
    );
    canManageOwnedResourceReads.mockReturnValue(true);
    const { getAnonymizationDirectory } = vi.mocked(await import("../lib/anonymization.js"));
    getAnonymizationDirectory.mockResolvedValue(null);
  });

  it("returns vacation with anonymized data when found", async () => {
    const findUnique = vi.fn().mockResolvedValue(mockVacation);
    const caller = createManagerCaller({ vacation: { findUnique } });

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

    expect(findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "vac_1" } }));
    expect(result.id).toBe("vac_1");
    expect(result.resource).toMatchObject({
      id: "resource_1",
      displayName: "Test User",
      eid: "E001",
      lcrCents: 5000,
      chapter: "Engineering",
    });
  });

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

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

  it("strips userId from the returned resource shape", async () => {
    const findUnique = vi.fn().mockResolvedValue(mockVacation);
    const caller = createManagerCaller({ vacation: { findUnique } });

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

    expect(result.resource).not.toHaveProperty("userId");
  });
});

describe("getForResource", () => {
  beforeEach(async () => {
    vi.clearAllMocks();
    const { assertCanReadOwnedResource } = vi.mocked(
      await import("../router/resource-owned-read-access.js"),
    );
    assertCanReadOwnedResource.mockResolvedValue(undefined);
  });

  it("returns approved vacations in the requested date range", async () => {
    const findMany = vi
      .fn()
      .mockResolvedValue([
        {
          id: "vac_1",
          startDate: new Date("2026-06-01"),
          endDate: new Date("2026-06-05"),
          type: VacationType.VACATION,
          status: VacationStatus.APPROVED,
        },
      ]);
    const caller = createManagerCaller({ vacation: { findMany } });

    const result = await caller.getForResource({
      resourceId: "resource_1",
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-30"),
    });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          resourceId: "resource_1",
          startDate: { lte: new Date("2026-06-30") },
          endDate: { gte: new Date("2026-06-01") },
        }),
      }),
    );
    expect(result).toHaveLength(1);
  });

  it("filters by APPROVED status only", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createManagerCaller({ vacation: { findMany } });

    await caller.getForResource({
      resourceId: "resource_1",
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-30"),
    });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          status: VacationStatus.APPROVED,
        }),
      }),
    );
  });
});

describe("getTeamOverlap", () => {
  beforeEach(async () => {
    vi.clearAllMocks();
    const { assertCanReadOwnedResource } = vi.mocked(
      await import("../router/resource-owned-read-access.js"),
    );
    assertCanReadOwnedResource.mockResolvedValue(undefined);
    listChapterVacationOverlapsMock.mockResolvedValue([]);
  });

  it("returns empty array when resource has no chapter", async () => {
    findVacationResourceChapterMock.mockResolvedValue(null);
    const caller = createManagerCaller({});

    const result = await caller.getTeamOverlap({
      resourceId: "resource_1",
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-05"),
    });

    expect(result).toEqual([]);
    expect(listChapterVacationOverlapsMock).not.toHaveBeenCalled();
  });

  it("calls listChapterVacationOverlaps when chapter exists", async () => {
    findVacationResourceChapterMock.mockResolvedValue("Engineering");
    const overlapData = [
      {
        id: "vac_2",
        resourceId: "resource_2",
        startDate: new Date("2026-06-02"),
        endDate: new Date("2026-06-03"),
        type: VacationType.VACATION,
        status: VacationStatus.APPROVED,
      },
    ];
    listChapterVacationOverlapsMock.mockResolvedValue(overlapData);
    const caller = createManagerCaller({});

    const result = await caller.getTeamOverlap({
      resourceId: "resource_1",
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-05"),
    });

    expect(listChapterVacationOverlapsMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        chapter: "Engineering",
        resourceId: "resource_1",
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-05"),
      }),
    );
    expect(result).toEqual(overlapData);
  });
});

describe("getTeamOverlapDetail", () => {
  beforeEach(async () => {
    vi.clearAllMocks();
    const { assertCanReadOwnedResource } = vi.mocked(
      await import("../router/resource-owned-read-access.js"),
    );
    assertCanReadOwnedResource.mockResolvedValue(undefined);
    listChapterVacationOverlapsMock.mockResolvedValue([]);
  });

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

    await expect(
      caller.getTeamOverlapDetail({
        resourceId: "resource_missing",
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-05"),
      }),
    ).rejects.toMatchObject({
      code: "NOT_FOUND",
      message: "Resource not found",
    });
  });

  it("returns formatted detail with empty overlaps when resource has no chapter", async () => {
    const findUnique = vi.fn().mockResolvedValue({ displayName: "Test User", chapter: null });
    const caller = createManagerCaller({ resource: { findUnique } });

    const result = await caller.getTeamOverlapDetail({
      resourceId: "resource_1",
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-05"),
    });

    expect(result).toMatchObject({
      resource: "Test User",
      chapter: null,
      period: "2026-06-01 to 2026-06-05",
      overlappingVacations: [],
      overlapCount: 0,
    });
    expect(listChapterVacationOverlapsMock).not.toHaveBeenCalled();
  });

  it("returns formatted detail with overlaps when chapter exists", async () => {
    const findUnique = vi
      .fn()
      .mockResolvedValue({ displayName: "Test User", chapter: "Engineering" });
    const overlap = {
      type: VacationType.VACATION,
      status: VacationStatus.APPROVED,
      startDate: new Date("2026-06-02"),
      endDate: new Date("2026-06-03"),
      resource: { displayName: "Colleague A" },
    };
    listChapterVacationOverlapsMock.mockResolvedValue([overlap]);
    const caller = createManagerCaller({ resource: { findUnique } });

    const result = await caller.getTeamOverlapDetail({
      resourceId: "resource_1",
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-05"),
    });

    expect(result).toMatchObject({
      resource: "Test User",
      chapter: "Engineering",
      period: "2026-06-01 to 2026-06-05",
      overlapCount: 1,
      overlappingVacations: [
        {
          resource: "Colleague A",
          type: VacationType.VACATION,
          status: VacationStatus.APPROVED,
          start: "2026-06-02",
          end: "2026-06-03",
        },
      ],
    });
    expect(listChapterVacationOverlapsMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ chapter: "Engineering", resourceId: "resource_1" }),
    );
  });
});
