import { SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { experienceMultiplierRouter } from "../router/experience-multiplier.js";
import { createCallerFactory } from "../trpc.js";

// Mock the engine — we focus on the router/DB layer, not the pure engine logic
vi.mock("@capakraken/engine", () => ({
  applyExperienceMultipliers: vi.fn().mockReturnValue({
    adjustedCostRateCents: 12000,
    adjustedBillRateCents: 18000,
    adjustedHours: 110,
    appliedRules: ["Rate multiplied (chapter=VFX): cost x1.2, bill x1.2"],
  }),
  applyExperienceMultipliersBatch: vi.fn().mockReturnValue({
    results: [
      {
        adjustedCostRateCents: 12000,
        adjustedBillRateCents: 18000,
        adjustedHours: 110,
        appliedRules: ["Rate multiplied (chapter=VFX): cost x1.2, bill x1.2"],
      },
    ],
    totalOriginalHours: 100,
    totalAdjustedHours: 110,
    linesAdjusted: 1,
  }),
}));

const createCaller = createCallerFactory(experienceMultiplierRouter);

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

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,
    },
  });
}

// ── Sample data factories ────────────────────────────────────────────────────

function sampleMultiplierSet(overrides: Record<string, unknown> = {}) {
  return {
    id: "ems_1",
    name: "Standard Multipliers",
    description: null,
    isDefault: true,
    createdAt: new Date(),
    updatedAt: new Date(),
    rules: [
      {
        id: "emr_1",
        multiplierSetId: "ems_1",
        chapter: "VFX",
        location: null,
        level: null,
        costMultiplier: 1.2,
        billMultiplier: 1.2,
        shoringRatio: null,
        additionalEffortRatio: null,
        description: null,
        sortOrder: 0,
        createdAt: new Date(),
        updatedAt: new Date(),
      },
    ],
    ...overrides,
  };
}

function sampleDemandLine(overrides: Record<string, unknown> = {}) {
  return {
    id: "dl_1",
    name: "Compositing Senior",
    chapter: "VFX",
    costRateCents: 10000,
    billRateCents: 15000,
    hours: 100,
    costTotalCents: 1000000,
    priceTotalCents: 1500000,
    metadata: null,
    staffingAttributes: null,
    createdAt: new Date(),
    ...overrides,
  };
}

// ─── list ────────────────────────────────────────────────────────────────────

describe("experienceMultiplier.list", () => {
  it("returns sets ordered by isDefault desc, name asc", async () => {
    const sets = [
      sampleMultiplierSet(),
      sampleMultiplierSet({ id: "ems_2", name: "Custom", isDefault: false }),
    ];
    const db = {
      experienceMultiplierSet: {
        findMany: vi.fn().mockResolvedValue(sets),
      },
    };

    const caller = createControllerCaller(db);
    const result = await caller.list();

    expect(result).toHaveLength(2);
    expect(db.experienceMultiplierSet.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        orderBy: [{ isDefault: "desc" }, { name: "asc" }],
      }),
    );
  });
});

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

describe("experienceMultiplier.getById", () => {
  it("returns the multiplier set when found", async () => {
    const set = sampleMultiplierSet();
    const db = {
      experienceMultiplierSet: {
        findUnique: vi.fn().mockResolvedValue(set),
      },
    };

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

    expect(result.id).toBe("ems_1");
    expect(result.rules).toHaveLength(1);
  });

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

    const caller = createControllerCaller(db);
    await expect(caller.getById({ id: "nonexistent" })).rejects.toThrow(
      "Experience multiplier set not found",
    );
  });
});

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

describe("experienceMultiplier.create", () => {
  it("creates a set with rules", async () => {
    const created = sampleMultiplierSet();
    const db = {
      experienceMultiplierSet: {
        updateMany: vi.fn().mockResolvedValue({ count: 0 }),
        create: vi.fn().mockResolvedValue(created),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    const result = await caller.create({
      name: "Standard Multipliers",
      isDefault: false,
      rules: [
        {
          chapter: "VFX",
          costMultiplier: 1.2,
          billMultiplier: 1.2,
          sortOrder: 0,
        },
      ],
    });

    expect(result.id).toBe("ems_1");
    expect(db.experienceMultiplierSet.create).toHaveBeenCalledTimes(1);
    // isDefault was false, so updateMany should NOT have been called
    expect(db.experienceMultiplierSet.updateMany).not.toHaveBeenCalled();
  });

  it("unsets other defaults when creating a new default set", async () => {
    const created = sampleMultiplierSet({ isDefault: true });
    const db = {
      experienceMultiplierSet: {
        updateMany: vi.fn().mockResolvedValue({ count: 1 }),
        create: vi.fn().mockResolvedValue(created),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    await caller.create({
      name: "Standard Multipliers",
      isDefault: true,
      rules: [],
    });

    expect(db.experienceMultiplierSet.updateMany).toHaveBeenCalledWith({
      where: { isDefault: true },
      data: { isDefault: false },
    });
  });
});

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

describe("experienceMultiplier.update", () => {
  it("updates name and description without touching rules", async () => {
    const existing = sampleMultiplierSet();
    const updated = { ...existing, name: "Updated Name" };
    const db = {
      experienceMultiplierSet: {
        findUnique: vi.fn().mockResolvedValue(existing),
        updateMany: vi.fn().mockResolvedValue({ count: 0 }),
        update: vi.fn().mockResolvedValue(updated),
      },
      experienceMultiplierRule: {
        deleteMany: vi.fn(),
        createMany: vi.fn(),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    const result = await caller.update({ id: "ems_1", name: "Updated Name" });

    expect(result.name).toBe("Updated Name");
    // No rules provided, so rule replacement should not happen
    expect(db.experienceMultiplierRule.deleteMany).not.toHaveBeenCalled();
    expect(db.experienceMultiplierRule.createMany).not.toHaveBeenCalled();
  });

  it("replaces rules when rules array is provided", async () => {
    const existing = sampleMultiplierSet();
    const updated = sampleMultiplierSet({ name: "Updated" });
    const db = {
      experienceMultiplierSet: {
        findUnique: vi.fn().mockResolvedValue(existing),
        updateMany: vi.fn().mockResolvedValue({ count: 0 }),
        update: vi.fn().mockResolvedValue(updated),
      },
      experienceMultiplierRule: {
        deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
        createMany: vi.fn().mockResolvedValue({ count: 2 }),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    await caller.update({
      id: "ems_1",
      rules: [
        { chapter: "VFX", costMultiplier: 1.3, billMultiplier: 1.3, sortOrder: 0 },
        { location: "India", costMultiplier: 0.7, billMultiplier: 0.9, shoringRatio: 0.5, sortOrder: 1 },
      ],
    });

    expect(db.experienceMultiplierRule.deleteMany).toHaveBeenCalledWith({
      where: { multiplierSetId: "ems_1" },
    });
    expect(db.experienceMultiplierRule.createMany).toHaveBeenCalledWith({
      data: expect.arrayContaining([
        expect.objectContaining({ multiplierSetId: "ems_1", chapter: "VFX" }),
        expect.objectContaining({ multiplierSetId: "ems_1", location: "India" }),
      ]),
    });
  });

  it("throws NOT_FOUND when set does not exist", async () => {
    const db = {
      experienceMultiplierSet: {
        findUnique: vi.fn().mockResolvedValue(null),
        updateMany: vi.fn(),
        update: vi.fn(),
      },
      experienceMultiplierRule: { deleteMany: vi.fn(), createMany: vi.fn() },
    };

    const caller = createManagerCaller(db);
    await expect(caller.update({ id: "nonexistent", name: "X" })).rejects.toThrow(
      "Experience multiplier set not found",
    );
  });
});

// ─── delete ──────────────────────────────────────────────────────────────────

describe("experienceMultiplier.delete", () => {
  it("deletes the set and returns its id", async () => {
    const existing = sampleMultiplierSet();
    const db = {
      experienceMultiplierSet: {
        findUnique: vi.fn().mockResolvedValue(existing),
        delete: vi.fn().mockResolvedValue(existing),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

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

    expect(result).toEqual({ id: "ems_1" });
    expect(db.experienceMultiplierSet.delete).toHaveBeenCalledWith({ where: { id: "ems_1" } });
  });

  it("throws NOT_FOUND when set does not exist", async () => {
    const db = {
      experienceMultiplierSet: {
        findUnique: vi.fn().mockResolvedValue(null),
        delete: vi.fn(),
      },
    };

    const caller = createManagerCaller(db);
    await expect(caller.delete({ id: "nonexistent" })).rejects.toThrow(
      "Experience multiplier set not found",
    );
  });
});

// ─── preview ─────────────────────────────────────────────────────────────────

describe("experienceMultiplier.preview", () => {
  function makeEstimate(demandLines: unknown[] = [sampleDemandLine()]) {
    return {
      id: "est_1",
      versions: [
        {
          id: "v_1",
          versionNumber: 1,
          status: "WORKING",
          demandLines,
        },
      ],
    };
  }

  it("returns preview results with summary stats", async () => {
    const estimate = makeEstimate();
    const multiplierSet = sampleMultiplierSet();

    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
    };

    const caller = createControllerCaller(db);
    const result = await caller.preview({ estimateId: "est_1", multiplierSetId: "ems_1" });

    expect(result.previews).toHaveLength(1);
    expect(result.demandLineCount).toBe(1);
    expect(result.multiplierSetName).toBe("Standard Multipliers");
    expect(result.ruleCount).toBe(1);
    // The mock returns adjusted values different from original, so hasChanges = true
    expect(result.previews[0].hasChanges).toBe(true);
    expect(result.previews[0].originalCostRateCents).toBe(10000);
    expect(result.previews[0].adjustedCostRateCents).toBe(12000);
    expect(result.linesChanged).toBe(1);
  });

  it("throws NOT_FOUND when estimate does not exist", async () => {
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(null) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(sampleMultiplierSet()) },
    };

    const caller = createControllerCaller(db);
    await expect(caller.preview({ estimateId: "nope", multiplierSetId: "ems_1" })).rejects.toThrow(
      "Estimate not found",
    );
  });

  it("throws NOT_FOUND when multiplier set does not exist", async () => {
    const estimate = makeEstimate();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(null) },
    };

    const caller = createControllerCaller(db);
    await expect(caller.preview({ estimateId: "est_1", multiplierSetId: "nope" })).rejects.toThrow(
      "Experience multiplier set not found",
    );
  });

  it("throws NOT_FOUND when estimate has no versions", async () => {
    const estimate = { id: "est_1", versions: [] };
    const multiplierSet = sampleMultiplierSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
    };

    const caller = createControllerCaller(db);
    await expect(caller.preview({ estimateId: "est_1", multiplierSetId: "ems_1" })).rejects.toThrow(
      "Estimate has no versions",
    );
  });

  it("reports no changes when rates are unchanged", async () => {
    // Import the mock to override for this test
    const { applyExperienceMultipliers } = await import("@capakraken/engine");
    const mockFn = applyExperienceMultipliers as ReturnType<typeof vi.fn>;
    mockFn.mockReturnValueOnce({
      adjustedCostRateCents: 10000,
      adjustedBillRateCents: 15000,
      adjustedHours: 100,
      appliedRules: ["No matching rule found -- values unchanged."],
    });

    const estimate = makeEstimate();
    const multiplierSet = sampleMultiplierSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
    };

    const caller = createControllerCaller(db);
    const result = await caller.preview({ estimateId: "est_1", multiplierSetId: "ems_1" });

    expect(result.linesChanged).toBe(0);
    expect(result.previews[0].hasChanges).toBe(false);
  });
});

// ─── applyRules ──────────────────────────────────────────────────────────────

describe("experienceMultiplier.applyRules", () => {
  function makeEstimate(versionStatus: string, demandLines: unknown[] = [sampleDemandLine()]) {
    return {
      id: "est_1",
      versions: [
        {
          id: "v_1",
          versionNumber: 1,
          status: versionStatus,
          demandLines,
        },
      ],
    };
  }

  it("updates demand lines with adjusted rates and creates audit log", async () => {
    const estimate = makeEstimate("WORKING");
    const multiplierSet = sampleMultiplierSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
      estimateDemandLine: {
        update: vi.fn().mockResolvedValue({}),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    const result = await caller.applyRules({
      estimateId: "est_1",
      multiplierSetId: "ems_1",
    });

    expect(result.linesUpdated).toBe(1);
    expect(result.totalOriginalHours).toBe(100);
    expect(result.totalAdjustedHours).toBe(110);
    expect(db.estimateDemandLine.update).toHaveBeenCalledTimes(1);

    // Verify the update call contains the adjusted rates and metadata
    const updateCall = db.estimateDemandLine.update.mock.calls[0][0];
    expect(updateCall.where).toEqual({ id: "dl_1" });
    expect(updateCall.data.costRateCents).toBe(12000);
    expect(updateCall.data.billRateCents).toBe(18000);
    expect(updateCall.data.hours).toBe(110);
    expect(updateCall.data.costTotalCents).toBe(Math.round(12000 * 110));
    expect(updateCall.data.priceTotalCents).toBe(Math.round(18000 * 110));
    expect(updateCall.data.metadata.experienceMultiplier).toEqual(
      expect.objectContaining({
        setId: "ems_1",
        setName: "Standard Multipliers",
        originalCostRateCents: 10000,
        originalBillRateCents: 15000,
        originalHours: 100,
      }),
    );

    // Audit log should be created
    expect(db.auditLog.create).toHaveBeenCalledTimes(1);
    const auditCall = db.auditLog.create.mock.calls[0][0];
    expect(auditCall.data.entityType).toBe("Estimate");
    expect(auditCall.data.entityId).toBe("est_1");
    expect(auditCall.data.action).toBe("UPDATE");
    expect(auditCall.data.userId).toBe("user_mgr");
  });

  it("skips unchanged lines (no update call)", async () => {
    const { applyExperienceMultipliersBatch } = await import("@capakraken/engine");
    const mockFn = applyExperienceMultipliersBatch as ReturnType<typeof vi.fn>;
    mockFn.mockReturnValueOnce({
      results: [
        {
          adjustedCostRateCents: 10000,
          adjustedBillRateCents: 15000,
          adjustedHours: 100,
          appliedRules: ["No matching rule found."],
        },
      ],
      totalOriginalHours: 100,
      totalAdjustedHours: 100,
      linesAdjusted: 0,
    });

    const estimate = makeEstimate("WORKING");
    const multiplierSet = sampleMultiplierSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
      estimateDemandLine: {
        update: vi.fn(),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    const result = await caller.applyRules({
      estimateId: "est_1",
      multiplierSetId: "ems_1",
    });

    expect(result.linesUpdated).toBe(0);
    expect(db.estimateDemandLine.update).not.toHaveBeenCalled();
  });

  it("rejects applying to a non-WORKING version", async () => {
    const estimate = makeEstimate("SUBMITTED");
    const multiplierSet = sampleMultiplierSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
      estimateDemandLine: { update: vi.fn() },
      auditLog: { create: vi.fn() },
    };

    const caller = createManagerCaller(db);
    await expect(
      caller.applyRules({ estimateId: "est_1", multiplierSetId: "ems_1" }),
    ).rejects.toThrow("Can only apply multipliers to a WORKING version");
  });

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

    const caller = createManagerCaller(db);
    await expect(
      caller.applyRules({ estimateId: "nope", multiplierSetId: "ems_1" }),
    ).rejects.toThrow("Estimate not found");
  });

  it("throws NOT_FOUND when multiplier set does not exist", async () => {
    const estimate = makeEstimate("WORKING");
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(null) },
      estimateDemandLine: { update: vi.fn() },
      auditLog: { create: vi.fn() },
    };

    const caller = createManagerCaller(db);
    await expect(
      caller.applyRules({ estimateId: "est_1", multiplierSetId: "nope" }),
    ).rejects.toThrow("Experience multiplier set not found");
  });

  it("throws NOT_FOUND when estimate has no versions", async () => {
    const estimate = { id: "est_1", versions: [] };
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(sampleMultiplierSet()) },
      estimateDemandLine: { update: vi.fn() },
      auditLog: { create: vi.fn() },
    };

    const caller = createManagerCaller(db);
    await expect(
      caller.applyRules({ estimateId: "est_1", multiplierSetId: "ems_1" }),
    ).rejects.toThrow("Estimate has no versions");
  });

  it("preserves existing metadata when updating demand lines", async () => {
    const lineWithMetadata = sampleDemandLine({
      metadata: { someField: "existing-value", anotherField: 42 },
    });
    const estimate = makeEstimate("WORKING", [lineWithMetadata]);
    const multiplierSet = sampleMultiplierSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      experienceMultiplierSet: { findUnique: vi.fn().mockResolvedValue(multiplierSet) },
      estimateDemandLine: {
        update: vi.fn().mockResolvedValue({}),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    await caller.applyRules({ estimateId: "est_1", multiplierSetId: "ems_1" });

    const updateCall = db.estimateDemandLine.update.mock.calls[0][0];
    // Existing metadata fields should be preserved alongside experienceMultiplier
    expect(updateCall.data.metadata.someField).toBe("existing-value");
    expect(updateCall.data.metadata.anotherField).toBe(42);
    expect(updateCall.data.metadata.experienceMultiplier).toBeDefined();
  });
});
