import { SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { effortRuleRouter } from "../router/effort-rule.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", () => ({
  expandScopeToEffort: vi.fn().mockReturnValue({
    lines: [
      {
        scopeItemName: "Shot_001",
        scopeType: "shot",
        discipline: "Compositing",
        chapter: null,
        hours: 16,
        unitMode: "per_frame" as const,
        unitCount: 200,
        hoursPerUnit: 0.08,
      },
    ],
    warnings: [],
    unmatchedScopeItems: [],
  }),
  aggregateByDiscipline: vi.fn().mockReturnValue({
    Compositing: { totalHours: 16, lineCount: 1 },
  }),
}));

const createCaller = createCallerFactory(effortRuleRouter);

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 sampleRuleSet(overrides: Record<string, unknown> = {}) {
  return {
    id: "ers_1",
    name: "VFX Standard",
    description: null,
    isDefault: true,
    createdAt: new Date(),
    updatedAt: new Date(),
    rules: [
      {
        id: "er_1",
        ruleSetId: "ers_1",
        scopeType: "shot",
        discipline: "Compositing",
        chapter: null,
        unitMode: "per_frame",
        hoursPerUnit: 0.08,
        description: null,
        sortOrder: 0,
        createdAt: new Date(),
        updatedAt: new Date(),
      },
    ],
    ...overrides,
  };
}

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

describe("effortRule.list", () => {
  it("returns rule sets ordered by isDefault desc, name asc", async () => {
    const sets = [sampleRuleSet(), sampleRuleSet({ id: "ers_2", name: "Animation", isDefault: false })];
    const db = {
      effortRuleSet: {
        findMany: vi.fn().mockResolvedValue(sets),
      },
    };

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

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

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

describe("effortRule.getById", () => {
  it("returns the rule set when found", async () => {
    const set = sampleRuleSet();
    const db = {
      effortRuleSet: {
        findUnique: vi.fn().mockResolvedValue(set),
      },
    };

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

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

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

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

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

describe("effortRule.create", () => {
  it("creates a rule set with rules", async () => {
    const created = sampleRuleSet();
    const db = {
      effortRuleSet: {
        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: "VFX Standard",
      isDefault: false,
      rules: [
        {
          scopeType: "shot",
          discipline: "Compositing",
          unitMode: "per_frame",
          hoursPerUnit: 0.08,
          sortOrder: 0,
        },
      ],
    });

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

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

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

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

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

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

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

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

  it("replaces rules when rules array is provided", async () => {
    const existing = sampleRuleSet();
    const updated = sampleRuleSet({ name: "Updated" });
    const db = {
      effortRuleSet: {
        findUnique: vi.fn().mockResolvedValue(existing),
        updateMany: vi.fn().mockResolvedValue({ count: 0 }),
        update: vi.fn().mockResolvedValue(updated),
      },
      effortRule: {
        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: "ers_1",
      rules: [
        { scopeType: "shot", discipline: "Lighting", unitMode: "per_frame", hoursPerUnit: 0.1, sortOrder: 0 },
        { scopeType: "asset", discipline: "Modeling", unitMode: "per_item", hoursPerUnit: 8, sortOrder: 1 },
      ],
    });

    expect(db.effortRule.deleteMany).toHaveBeenCalledWith({ where: { ruleSetId: "ers_1" } });
    expect(db.effortRule.createMany).toHaveBeenCalledWith({
      data: expect.arrayContaining([
        expect.objectContaining({ ruleSetId: "ers_1", discipline: "Lighting" }),
        expect.objectContaining({ ruleSetId: "ers_1", discipline: "Modeling" }),
      ]),
    });
  });

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

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

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

describe("effortRule.delete", () => {
  it("deletes the rule set and returns its id", async () => {
    const existing = sampleRuleSet();
    const db = {
      effortRuleSet: {
        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: "ers_1" });

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

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

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

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

describe("effortRule.preview", () => {
  it("returns expansion result with aggregation", async () => {
    const estimate = {
      id: "est_1",
      baseCurrency: "EUR",
      versions: [
        {
          id: "v_1",
          versionNumber: 1,
          status: "WORKING",
          scopeItems: [
            {
              id: "si_1",
              name: "Shot_001",
              scopeType: "shot",
              frameCount: 200,
              itemCount: null,
              unitMode: "per_frame",
              sortOrder: 0,
            },
          ],
        },
      ],
    };
    const ruleSet = sampleRuleSet();

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

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

    expect(result.lines).toHaveLength(1);
    expect(result.scopeItemCount).toBe(1);
    expect(result.ruleCount).toBe(1);
    expect(result.aggregated).toBeDefined();
  });

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

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

  it("throws NOT_FOUND when rule set does not exist", async () => {
    const estimate = { id: "est_1", versions: [{ id: "v_1", scopeItems: [] }] };
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      effortRuleSet: { findUnique: vi.fn().mockResolvedValue(null) },
    };

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

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

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

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

describe("effortRule.applyRules", () => {
  function makeEstimate(versionStatus: string, demandLines: unknown[] = []) {
    return {
      id: "est_1",
      baseCurrency: "EUR",
      versions: [
        {
          id: "v_1",
          versionNumber: 1,
          status: versionStatus,
          scopeItems: [
            {
              id: "si_1",
              name: "Shot_001",
              scopeType: "shot",
              frameCount: 200,
              itemCount: null,
              unitMode: "per_frame",
              sortOrder: 0,
            },
          ],
          demandLines,
        },
      ],
    };
  }

  it("replaces existing demand lines in replace mode", async () => {
    const estimate = makeEstimate("WORKING", [{ id: "dl_old" }]);
    const ruleSet = sampleRuleSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
      estimateDemandLine: {
        deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
        createMany: vi.fn().mockResolvedValue({ count: 1 }),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    const result = await caller.applyRules({
      estimateId: "est_1",
      ruleSetId: "ers_1",
      mode: "replace",
    });

    expect(result.linesGenerated).toBe(1);
    expect(db.estimateDemandLine.deleteMany).toHaveBeenCalledWith({
      where: { estimateVersionId: "v_1" },
    });
    expect(db.estimateDemandLine.createMany).toHaveBeenCalledTimes(1);
    expect(db.auditLog.create).toHaveBeenCalledTimes(1);
  });

  it("does not delete existing lines in append mode", async () => {
    const estimate = makeEstimate("WORKING", [{ id: "dl_old" }]);
    const ruleSet = sampleRuleSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
      estimateDemandLine: {
        deleteMany: vi.fn(),
        createMany: vi.fn().mockResolvedValue({ count: 1 }),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    const result = await caller.applyRules({
      estimateId: "est_1",
      ruleSetId: "ers_1",
      mode: "append",
    });

    expect(result.linesGenerated).toBe(1);
    expect(db.estimateDemandLine.deleteMany).not.toHaveBeenCalled();
  });

  it("rejects applying to a non-WORKING version", async () => {
    const estimate = makeEstimate("SUBMITTED");
    const ruleSet = sampleRuleSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
      estimateDemandLine: { deleteMany: vi.fn(), createMany: vi.fn() },
      auditLog: { create: vi.fn() },
    };

    const caller = createManagerCaller(db);
    await expect(
      caller.applyRules({ estimateId: "est_1", ruleSetId: "ers_1", mode: "replace" }),
    ).rejects.toThrow("Can only apply rules to a WORKING version");
  });

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

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

  it("throws NOT_FOUND when estimate has no versions", async () => {
    const estimate = { id: "est_1", baseCurrency: "EUR", versions: [] };
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      effortRuleSet: { findUnique: vi.fn().mockResolvedValue(sampleRuleSet()) },
      estimateDemandLine: { deleteMany: vi.fn(), createMany: vi.fn() },
      auditLog: { create: vi.fn() },
    };

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

  it("creates demand lines with correct metadata shape", async () => {
    const estimate = makeEstimate("WORKING");
    const ruleSet = sampleRuleSet();
    const db = {
      estimate: { findUnique: vi.fn().mockResolvedValue(estimate) },
      effortRuleSet: { findUnique: vi.fn().mockResolvedValue(ruleSet) },
      estimateDemandLine: {
        deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
        createMany: vi.fn().mockResolvedValue({ count: 1 }),
      },
      auditLog: { create: vi.fn().mockResolvedValue({}) },
    };

    const caller = createManagerCaller(db);
    await caller.applyRules({ estimateId: "est_1", ruleSetId: "ers_1", mode: "replace" });

    const createManyArg = db.estimateDemandLine.createMany.mock.calls[0][0];
    const firstLine = createManyArg.data[0];
    expect(firstLine.estimateVersionId).toBe("v_1");
    expect(firstLine.lineType).toBe("LABOR");
    expect(firstLine.currency).toBe("EUR");
    expect(firstLine.costRateCents).toBe(0);
    expect(firstLine.billRateCents).toBe(0);
    expect(firstLine.metadata).toEqual(
      expect.objectContaining({
        effortRule: expect.objectContaining({
          ruleSetId: "ers_1",
          ruleSetName: "VFX Standard",
        }),
      }),
    );
  });
});
