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

vi.mock("@capakraken/application", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@capakraken/application")>();
  return {
    ...actual,
    isChargeabilityActualBooking: actual.isChargeabilityActualBooking,
    isChargeabilityRelevantProject: actual.isChargeabilityRelevantProject,
    listAssignmentBookings: vi.fn(),
    recomputeResourceValueScores: vi.fn(),
  };
});

vi.mock("../router/blueprint-validation.js", () => ({
  assertBlueprintDynamicFields: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../ai-client.js", () => ({
  createAiClient: vi.fn(() => ({
    chat: {
      completions: {
        create: vi.fn().mockResolvedValue({
          choices: [{ message: { content: "Generated summary" } }],
        }),
      },
    },
    responses: {
      create: vi.fn(),
    },
  })),
  isAiConfigured: vi.fn().mockReturnValue(true),
  loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn) => fn()),
}));

vi.mock("../lib/anonymization.js", () => ({
  anonymizeResource: vi.fn((r: Record<string, unknown>) => r),
  anonymizeResources: vi.fn((rs: unknown[]) => rs),
  anonymizeSearchMatches: vi.fn((rs: unknown[]) => rs),
  getAnonymizationDirectory: vi.fn().mockResolvedValue(null),
  resolveResourceIdsByDisplayedEids: vi.fn().mockResolvedValue(new Map()),
}));

vi.mock("../lib/logger.js", () => ({
  logger: {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));

import { resourceRouter } from "../router/resource.js";
import { createAiClient, isAiConfigured, loggedAiCall } from "../ai-client.js";
import { getAnonymizationDirectory } from "../lib/anonymization.js";
import { createCallerFactory } from "../trpc.js";

const createCaller = createCallerFactory(resourceRouter);

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: "mgr_1",
      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: "admin_1",
      systemRole: SystemRole.ADMIN,
      permissionOverrides: null,
    },
  });
}

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

const sampleResource = {
  id: "res_1",
  eid: "E-001",
  displayName: "Alice",
  email: "alice@example.com",
  chapter: "CGI",
  lcrCents: 5000,
  ucrCents: 9000,
  currency: "EUR",
  chargeabilityTarget: 80,
  availability: {
    monday: 8,
    tuesday: 8,
    wednesday: 8,
    thursday: 8,
    friday: 8,
  },
  skills: [],
  dynamicFields: {},
  blueprintId: null,
  blueprint: null,
  isActive: true,
  createdAt: new Date("2026-03-01"),
  updatedAt: new Date("2026-03-01"),
  roleId: null,
  portfolioUrl: null,
  postalCode: null,
  federalState: null,
  valueScore: null,
  valueScoreBreakdown: null,
  valueScoreUpdatedAt: null,
  userId: null,
  resourceRoles: [],
  areaRole: null,
  countryId: null,
  metroCityId: null,
  orgUnitId: null,
  managementLevelGroupId: null,
  managementLevelId: null,
  resourceType: null,
  chgResponsibility: null,
  rolledOff: false,
  departed: false,
  enterpriseId: null,
  clientUnitId: null,
  fte: 1,
};

describe("resource router CRUD", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  // ─── listStaff ────────────────────────────────────────────────────────────

  describe("listStaff", () => {
    it("returns paginated results with total count for staff callers", async () => {
      const db = {
        resource: {
          findMany: vi.fn().mockResolvedValue([sampleResource]),
          count: vi.fn().mockResolvedValue(1),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.listStaff({ limit: 50 });

      expect(result.resources).toHaveLength(1);
      expect(result.resources[0]?.displayName).toBe("Alice");
      expect(db.resource.findMany).toHaveBeenCalled();
    });

    it("applies search filter for staff callers", async () => {
      const db = {
        resource: {
          findMany: vi.fn().mockResolvedValue([]),
          count: vi.fn().mockResolvedValue(0),
        },
      };

      const caller = createManagerCaller(db);
      await caller.listStaff({ search: "Alice", limit: 50 });

      expect(db.resource.findMany).toHaveBeenCalled();
    });
  });

  // ─── getById ──────────────────────────────────────────────────────────────

  describe("getById", () => {
    it("returns correct resource", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi.fn().mockResolvedValue({ ...sampleResource, userId: "user_1" }),
          findMany: vi.fn().mockResolvedValue([]),
        },
        systemSettings: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getById({ id: "res_1" });

      expect(result.id).toBe("res_1");
      expect(result.displayName).toBe("Alice");
    });

    it("throws NOT_FOUND when resource does not exist", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi.fn().mockResolvedValue(null),
          findMany: vi.fn().mockResolvedValue([]),
        },
        systemSettings: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(caller.getById({ id: "missing" })).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("sets isOwnedByCurrentUser when userId matches", async () => {
      const ownedResource = { ...sampleResource, userId: "user_1" };
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi.fn().mockResolvedValue(ownedResource),
          findMany: vi.fn().mockResolvedValue([]),
        },
        systemSettings: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getById({ id: "res_1" });

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

    it("rejects foreign resources for regular users", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
          findUnique: vi.fn().mockResolvedValue(sampleResource),
          findMany: vi.fn().mockResolvedValue([]),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(caller.getById({ id: "res_1" })).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });
  });

  describe("getByEid", () => {
    it("returns the matching resource for staff callers", async () => {
      const db = {
        resource: {
          findUnique: vi.fn().mockResolvedValue(sampleResource),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.getByEid({ eid: "E-001" });

      expect(result).toMatchObject({
        id: "res_1",
        displayName: "Alice",
        eid: "E-001",
      });
      expect(db.resource.findUnique).toHaveBeenCalledWith({ where: { eid: "E-001" } });
    });

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

      const caller = createManagerCaller(db);
      await expect(caller.getByEid({ eid: "E-404" })).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("resolves anonymized alias eids through the directory fallback", async () => {
      vi.mocked(getAnonymizationDirectory).mockResolvedValueOnce({
        byAliasEid: new Map([["alias-e-001", "res_1"]]),
      } as Awaited<ReturnType<typeof getAnonymizationDirectory>>);

      const findUnique = vi
        .fn()
        .mockResolvedValueOnce(null)
        .mockResolvedValueOnce(sampleResource);
      const db = {
        resource: {
          findUnique,
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.getByEid({ eid: "alias-e-001" });

      expect(result).toMatchObject({
        id: "res_1",
        eid: "E-001",
      });
      expect(findUnique).toHaveBeenNthCalledWith(1, { where: { eid: "alias-e-001" } });
      expect(findUnique).toHaveBeenNthCalledWith(2, { where: { id: "res_1" } });
    });
  });

  // ─── create ───────────────────────────────────────────────────────────────

  describe("create", () => {
    it("creates a resource and returns it", async () => {
      const created = { ...sampleResource, id: "res_new", resourceRoles: [] };
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue(null),
          create: vi.fn().mockResolvedValue(created),
        },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.create({
        eid: "E-NEW",
        displayName: "New Resource",
        email: "new@example.com",
        lcrCents: 4000,
        ucrCents: 8000,
      });

      expect(result.id).toBe("res_new");
      expect(db.resource.create).toHaveBeenCalled();
      expect(db.auditLog.create).toHaveBeenCalled();
    });

    it("throws CONFLICT on duplicate eid or email", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue(sampleResource),
          create: vi.fn(),
        },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.create({
          eid: "E-001",
          displayName: "Duplicate",
          email: "alice@example.com",
          lcrCents: 5000,
          ucrCents: 9000,
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "CONFLICT" }),
      );
    });

    it("blocks USER role from creating resources", async () => {
      const db = {
        resource: { findFirst: vi.fn(), create: vi.fn() },
        auditLog: { create: vi.fn() },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.create({
          eid: "E-002",
          displayName: "Blocked",
          email: "blocked@example.com",
          lcrCents: 4000,
          ucrCents: 8000,
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });
  });

  // ─── update ───────────────────────────────────────────────────────────────

  describe("update", () => {
    it("updates resource fields", async () => {
      const updated = { ...sampleResource, displayName: "Alice Updated" };
      const db = {
        resource: {
          findUnique: vi.fn().mockResolvedValue(sampleResource),
          update: vi.fn().mockResolvedValue(updated),
        },
        resourceRole: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.update({
        id: "res_1",
        data: { displayName: "Alice Updated" },
      });

      expect(result.displayName).toBe("Alice Updated");
      expect(db.resource.update).toHaveBeenCalledWith(
        expect.objectContaining({ where: { id: "res_1" } }),
      );
    });

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

      const caller = createManagerCaller(db);
      await expect(
        caller.update({ id: "missing", data: { displayName: "X" } }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });
  });

  // ─── deactivate ───────────────────────────────────────────────────────────

  describe("deactivate", () => {
    it("sets isActive to false", async () => {
      const deactivated = { ...sampleResource, isActive: false };
      const db = {
        resource: {
          update: vi.fn().mockResolvedValue(deactivated),
        },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.deactivate({ id: "res_1" });

      expect(result.isActive).toBe(false);
      expect(db.resource.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "res_1" },
          data: { isActive: false },
        }),
      );
    });
  });

  describe("batchDeactivate", () => {
    it("deactivates all requested resources and records one audit entry", async () => {
      const update = vi
        .fn()
        .mockResolvedValueOnce({ ...sampleResource, id: "res_1", isActive: false })
        .mockResolvedValueOnce({ ...sampleResource, id: "res_2", isActive: false });
      const db = {
        resource: {
          update,
        },
        $transaction: vi.fn(async (operations: Promise<unknown>[]) => Promise.all(operations)),
        auditLog: { create: vi.fn().mockResolvedValue({}) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.batchDeactivate({ ids: ["res_1", "res_2"] });

      expect(result).toEqual({ count: 2 });
      expect(db.$transaction).toHaveBeenCalledTimes(1);
      expect(db.auditLog.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            entityType: "Resource",
            action: "UPDATE",
          }),
        }),
      );
    });
  });

  describe("chapters", () => {
    it("returns active chapter names in ascending order", async () => {
      const db = {
        resource: {
          findMany: vi.fn().mockResolvedValue([
            { chapter: "Animation" },
            { chapter: "CGI" },
          ]),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.chapters();

      expect(result).toEqual(["Animation", "CGI"]);
      expect(db.resource.findMany).toHaveBeenCalledWith({
        where: { isActive: true, chapter: { not: null } },
        select: { chapter: true },
        distinct: ["chapter"],
        orderBy: { chapter: "asc" },
      });
    });
  });

  // ─── getHoverCard ─────────────────────────────────────────────────────────

  describe("getHoverCard", () => {
    it("returns expected shape with key fields", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi.fn().mockResolvedValue({
            id: "res_1",
            displayName: "Alice",
            eid: "E-001",
            email: "alice@example.com",
            chapter: "CGI",
            lcrCents: 5000,
            ucrCents: 9000,
            currency: "EUR",
            chargeabilityTarget: 80,
            skills: [],
            availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
            isActive: true,
            areaRole: null,
            country: null,
            managementLevel: null,
            resourceType: null,
          }),
        },
        systemSettings: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getHoverCard({ id: "res_1" });

      expect(result).toMatchObject({
        id: "res_1",
        displayName: "Alice",
        chapter: "CGI",
        lcrCents: 5000,
        isActive: true,
      });
    });

    it("throws NOT_FOUND for missing resource", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi.fn().mockResolvedValue(null),
        },
        systemSettings: { findUnique: vi.fn().mockResolvedValue(null) },
      };

      const caller = createProtectedCaller(db);
      await expect(caller.getHoverCard({ id: "missing" })).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("rejects foreign hover-card access for regular users", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
          findUnique: vi.fn().mockResolvedValue({
            id: "res_1",
            displayName: "Alice",
            eid: "E-001",
            email: "alice@example.com",
            chapter: "CGI",
            lcrCents: 5000,
            ucrCents: 9000,
            currency: "EUR",
            chargeabilityTarget: 80,
            skills: [],
            availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
            isActive: true,
            areaRole: null,
            country: null,
            managementLevel: null,
            resourceType: null,
          }),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(caller.getHoverCard({ id: "res_1" })).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });
  });

  // ─── importSkillMatrix ────────────────────────────────────────────────────

  describe("importSkillMatrix", () => {
    it("imports skills for the current user resource", async () => {
      const updatedResource = {
        ...sampleResource,
        skills: [{ skill: "Maya", proficiency: 4 }],
        skillMatrixUpdatedAt: new Date(),
      };
      const db = {
        user: {
          findUnique: vi.fn().mockResolvedValue({
            id: "user_1",
            email: "user@example.com",
            resource: { id: "res_1" },
          }),
        },
        resource: {
          update: vi.fn().mockResolvedValue(updatedResource),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.importSkillMatrix({
        skills: [{ skill: "Maya", proficiency: 4 }],
      });

      expect(db.user.findUnique).toHaveBeenCalledWith({
        where: { id: "user_1" },
        include: { resource: true },
      });
      expect(db.resource.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "res_1" },
        }),
      );
    });

    it("throws NOT_FOUND when user has no linked resource", async () => {
      const db = {
        user: {
          findUnique: vi.fn().mockResolvedValue({
            id: "user_1",
            email: "user@example.com",
            resource: null,
          }),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.importSkillMatrix({
          skills: [{ skill: "Nuke", proficiency: 3 }],
        }),
      ).rejects.toThrow("No resource linked to your account");
      expect(db.user.findUnique).toHaveBeenCalledWith({
        where: { id: "user_1" },
        include: { resource: true },
      });
    });
  });

  describe("importSkillMatrixForResource", () => {
    it("updates skills for an existing resource", async () => {
      const db = {
        resource: {
          findUnique: vi.fn().mockResolvedValue(sampleResource),
          update: vi.fn().mockResolvedValue(sampleResource),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.importSkillMatrixForResource({
        resourceId: "res_1",
        skills: [{ skill: "Houdini", proficiency: 5 }],
      });

      expect(result).toEqual({ count: 1 });
      expect(db.resource.findUnique).toHaveBeenCalledWith({ where: { id: "res_1" } });
      expect(db.resource.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "res_1" },
        }),
      );
    });

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

      const caller = createManagerCaller(db);
      await expect(
        caller.importSkillMatrixForResource({
          resourceId: "missing",
          skills: [{ skill: "Houdini", proficiency: 5 }],
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
      expect(db.resource.update).not.toHaveBeenCalled();
    });
  });

  describe("batchImportSkillMatrices", () => {
    it("updates all matching resources in one transaction and reports missing eids", async () => {
      const update = vi
        .fn()
        .mockResolvedValueOnce({ ...sampleResource, id: "res_1" })
        .mockResolvedValueOnce({ ...sampleResource, id: "res_2", eid: "E-002" });
      const db = {
        resource: {
          findMany: vi.fn().mockResolvedValue([
            { id: "res_1", eid: "E-001" },
            { id: "res_2", eid: "E-002" },
          ]),
          update,
        },
        $transaction: vi.fn(async (operations: Promise<unknown>[]) => Promise.all(operations)),
      };

      const caller = createAdminCaller(db);
      const result = await caller.batchImportSkillMatrices({
        entries: [
          { eid: "E-001", skills: [{ skill: "Houdini", proficiency: 5 }] },
          { eid: "E-002", skills: [{ skill: "Nuke", proficiency: 4 }] },
          { eid: "E-404", skills: [{ skill: "Maya", proficiency: 3 }] },
        ],
      });

      expect(result).toEqual({ updated: 2, notFound: 1 });
      expect(db.resource.findMany).toHaveBeenCalledWith({
        where: { eid: { in: ["E-001", "E-002", "E-404"] } },
        select: { id: true, eid: true },
      });
      expect(db.$transaction).toHaveBeenCalledTimes(1);
      expect(db.resource.update).toHaveBeenCalledTimes(2);
    });
  });

  describe("generateAiSummary", () => {
    it("persists the generated summary for the requested resource", async () => {
      const update = vi.fn().mockResolvedValue({
        ...sampleResource,
        aiSummary: "Generated summary",
      });
      const db = {
        resource: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleResource,
            skills: [
              { skill: "Maya", proficiency: 5, isMainSkill: true },
              { skill: "Nuke", proficiency: 4 },
            ],
            areaRole: { name: "3D Artist" },
          }),
          update,
        },
        systemSettings: {
          findUnique: vi.fn().mockResolvedValue({
            id: "singleton",
            aiProvider: "openai",
            azureOpenAiDeployment: "gpt-test",
            aiSummaryPrompt: null,
            aiMaxCompletionTokens: 200,
            aiTemperature: 0.7,
          }),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.generateAiSummary({ resourceId: "res_1" });

      expect(isAiConfigured).toHaveBeenCalledWith(expect.objectContaining({
        azureOpenAiDeployment: "gpt-test",
      }));
      expect(createAiClient).toHaveBeenCalledWith(expect.objectContaining({
        azureOpenAiDeployment: "gpt-test",
      }));
      expect(loggedAiCall).toHaveBeenCalledOnce();
      expect(update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "res_1" },
          data: expect.objectContaining({
            aiSummary: "Generated summary",
            aiSummaryUpdatedAt: expect.any(Date),
          }),
        }),
      );
      expect(result).toEqual({ summary: "Generated summary" });
    });

    it("fails fast when AI is not configured", async () => {
      vi.mocked(isAiConfigured).mockReturnValueOnce(false);

      const db = {
        resource: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleResource,
            areaRole: { name: "3D Artist" },
          }),
        },
        systemSettings: {
          findUnique: vi.fn().mockResolvedValue({
            id: "singleton",
            aiProvider: "openai",
            azureOpenAiDeployment: null,
          }),
        },
      };

      const caller = createManagerCaller(db);

      await expect(caller.generateAiSummary({ resourceId: "res_1" })).rejects.toThrow(
        "AI is not configured. Please set credentials in Admin → Settings.",
      );
      expect(createAiClient).not.toHaveBeenCalled();
    });
  });
});
