import { describe, expect, it } from "vitest";
import {
  compareEstimateVersions,
  type VersionCompareInput,
} from "../estimate/index.js";

function makeLine(overrides: Partial<VersionCompareInput["demandLines"][number]> & { id: string; name: string }) {
  return {
    hours: 0,
    costRateCents: 0,
    billRateCents: 0,
    costTotalCents: 0,
    priceTotalCents: 0,
    lineType: "ROLE",
    ...overrides,
  };
}

function makeVersion(overrides: Partial<VersionCompareInput> = {}): VersionCompareInput {
  return {
    versionNumber: 1,
    demandLines: [],
    assumptions: [],
    scopeItems: [],
    ...overrides,
  };
}

describe("compareEstimateVersions", () => {
  it("returns zeroed diff for identical versions", () => {
    const lines = [
      makeLine({ id: "l1", name: "Designer", hours: 100, costTotalCents: 500_00, priceTotalCents: 800_00 }),
    ];
    const assumptions = [{ key: "rate", label: "Day rate", value: 500 }];
    const scopeItems = [{ name: "Shot A", sequenceNo: 1, scopeType: "SHOT" }];

    const v = makeVersion({ demandLines: lines, assumptions, scopeItems });
    const diff = compareEstimateVersions(v, v);

    expect(diff.summary.totalHoursDelta).toBe(0);
    expect(diff.summary.totalCostDelta).toBe(0);
    expect(diff.summary.totalPriceDelta).toBe(0);
    expect(diff.summary.linesAdded).toBe(0);
    expect(diff.summary.linesRemoved).toBe(0);
    expect(diff.summary.linesChanged).toBe(0);
    expect(diff.summary.assumptionsChanged).toBe(0);
    expect(diff.summary.scopeItemsAdded).toBe(0);
    expect(diff.summary.scopeItemsRemoved).toBe(0);

    expect(diff.demandLineDiffs).toHaveLength(1);
    expect(diff.demandLineDiffs[0]!.status).toBe("unchanged");

    expect(diff.assumptionDiffs).toHaveLength(1);
    expect(diff.assumptionDiffs[0]!.status).toBe("unchanged");
  });

  it("detects added demand lines", () => {
    const a = makeVersion({ demandLines: [] });
    const b = makeVersion({
      demandLines: [
        makeLine({ id: "l1", name: "Animator", hours: 40, costTotalCents: 200_00, priceTotalCents: 320_00 }),
      ],
    });

    const diff = compareEstimateVersions(a, b);

    expect(diff.summary.linesAdded).toBe(1);
    expect(diff.summary.linesRemoved).toBe(0);
    expect(diff.summary.totalHoursDelta).toBe(40);
    expect(diff.summary.totalCostDelta).toBe(200_00);
    expect(diff.summary.totalPriceDelta).toBe(320_00);

    expect(diff.demandLineDiffs).toHaveLength(1);
    expect(diff.demandLineDiffs[0]).toEqual(
      expect.objectContaining({
        name: "Animator",
        status: "added",
        hoursDelta: 40,
        costDelta: 200_00,
        priceDelta: 320_00,
      }),
    );
  });

  it("detects removed demand lines", () => {
    const a = makeVersion({
      demandLines: [
        makeLine({ id: "l1", name: "Lighter", hours: 20, costTotalCents: 100_00, priceTotalCents: 160_00 }),
      ],
    });
    const b = makeVersion({ demandLines: [] });

    const diff = compareEstimateVersions(a, b);

    expect(diff.summary.linesAdded).toBe(0);
    expect(diff.summary.linesRemoved).toBe(1);
    expect(diff.summary.totalHoursDelta).toBe(-20);

    expect(diff.demandLineDiffs[0]).toEqual(
      expect.objectContaining({
        name: "Lighter",
        status: "removed",
        hoursDelta: -20,
      }),
    );
  });

  it("detects changed hours and rates", () => {
    const a = makeVersion({
      demandLines: [
        makeLine({ id: "l1", name: "TD", hours: 50, costTotalCents: 300_00, priceTotalCents: 500_00 }),
      ],
    });
    const b = makeVersion({
      demandLines: [
        makeLine({ id: "l1", name: "TD", hours: 80, costTotalCents: 480_00, priceTotalCents: 800_00 }),
      ],
    });

    const diff = compareEstimateVersions(a, b);

    expect(diff.summary.linesChanged).toBe(1);
    expect(diff.summary.linesAdded).toBe(0);
    expect(diff.summary.linesRemoved).toBe(0);
    expect(diff.summary.totalHoursDelta).toBe(30);
    expect(diff.summary.totalCostDelta).toBe(180_00);
    expect(diff.summary.totalPriceDelta).toBe(300_00);

    const lineDiff = diff.demandLineDiffs[0]!;
    expect(lineDiff.status).toBe("changed");
    expect(lineDiff.hoursDelta).toBe(30);
    expect(lineDiff.costDelta).toBe(180_00);
    expect(lineDiff.priceDelta).toBe(300_00);
    expect(lineDiff.a).toEqual({ hours: 50, costTotalCents: 300_00, priceTotalCents: 500_00 });
    expect(lineDiff.b).toEqual({ hours: 80, costTotalCents: 480_00, priceTotalCents: 800_00 });
  });

  it("fuzzy-matches demand lines by name+lineType when ids differ", () => {
    const a = makeVersion({
      demandLines: [
        makeLine({ id: "old-id", name: "Compositor", lineType: "ROLE", hours: 10, costTotalCents: 50_00, priceTotalCents: 80_00 }),
      ],
    });
    const b = makeVersion({
      demandLines: [
        makeLine({ id: "new-id", name: "Compositor", lineType: "ROLE", hours: 20, costTotalCents: 100_00, priceTotalCents: 160_00 }),
      ],
    });

    const diff = compareEstimateVersions(a, b);

    expect(diff.summary.linesAdded).toBe(0);
    expect(diff.summary.linesRemoved).toBe(0);
    expect(diff.summary.linesChanged).toBe(1);
    expect(diff.demandLineDiffs[0]!.hoursDelta).toBe(10);
  });

  it("detects added and removed assumptions", () => {
    const a = makeVersion({
      assumptions: [
        { key: "overhead", label: "Overhead %", value: 15 },
        { key: "old_key", label: "Old", value: "x" },
      ],
    });
    const b = makeVersion({
      assumptions: [
        { key: "overhead", label: "Overhead %", value: 20 },
        { key: "new_key", label: "New", value: "y" },
      ],
    });

    const diff = compareEstimateVersions(a, b);

    expect(diff.summary.assumptionsChanged).toBe(3);

    const overheadDiff = diff.assumptionDiffs.find((d) => d.key === "overhead")!;
    expect(overheadDiff.status).toBe("changed");
    expect(overheadDiff.aValue).toBe(15);
    expect(overheadDiff.bValue).toBe(20);

    expect(diff.assumptionDiffs.find((d) => d.key === "old_key")!.status).toBe("removed");
    expect(diff.assumptionDiffs.find((d) => d.key === "new_key")!.status).toBe("added");
  });

  it("detects scope item additions and removals", () => {
    const a = makeVersion({
      scopeItems: [
        { name: "Shot A", sequenceNo: 1, scopeType: "SHOT" },
        { name: "Asset X", sequenceNo: 2, scopeType: "ASSET" },
      ],
    });
    const b = makeVersion({
      scopeItems: [
        { name: "Shot A", sequenceNo: 1, scopeType: "SHOT" },
        { name: "Shot B", sequenceNo: 2, scopeType: "SHOT" },
      ],
    });

    const diff = compareEstimateVersions(a, b);

    expect(diff.summary.scopeItemsAdded).toBe(1);
    expect(diff.summary.scopeItemsRemoved).toBe(1);
  });

  it("handles empty versions", () => {
    const diff = compareEstimateVersions(makeVersion(), makeVersion());

    expect(diff.summary.totalHoursDelta).toBe(0);
    expect(diff.summary.linesAdded).toBe(0);
    expect(diff.demandLineDiffs).toHaveLength(0);
    expect(diff.assumptionDiffs).toHaveLength(0);
    expect(diff.scopeItemDiffs).toHaveLength(0);
    expect(diff.resourceSnapshotDiffs).toHaveLength(0);
    expect(diff.chapterSubtotals).toHaveLength(0);
    expect(diff.summary.marginPercentA).toBe(0);
    expect(diff.summary.marginPercentB).toBe(0);
  });

  it("compares complex assumption values by deep equality", () => {
    const a = makeVersion({
      assumptions: [{ key: "config", label: "Config", value: { fps: 24, resolution: "4K" } }],
    });
    const b = makeVersion({
      assumptions: [{ key: "config", label: "Config", value: { fps: 24, resolution: "4K" } }],
    });

    const diff = compareEstimateVersions(a, b);
    expect(diff.assumptionDiffs[0]!.status).toBe("unchanged");

    const b2 = makeVersion({
      assumptions: [{ key: "config", label: "Config", value: { fps: 30, resolution: "4K" } }],
    });
    const diff2 = compareEstimateVersions(a, b2);
    expect(diff2.assumptionDiffs[0]!.status).toBe("changed");
  });

  describe("scope item diffs (detailed)", () => {
    it("detects changed scope item fields", () => {
      const a = makeVersion({
        scopeItems: [{ name: "Shot 010", sequenceNo: 10, scopeType: "SHOT", frameCount: 120 }],
      });
      const b = makeVersion({
        scopeItems: [{ name: "Shot 010", sequenceNo: 10, scopeType: "SHOT", frameCount: 200 }],
      });
      const diff = compareEstimateVersions(a, b);
      expect(diff.scopeItemDiffs).toHaveLength(1);
      expect(diff.scopeItemDiffs[0]!.status).toBe("changed");
      expect(diff.scopeItemDiffs[0]!.changedFields).toContain("frameCount");
      expect(diff.summary.scopeItemsChanged).toBe(1);
    });

    it("detects description changes in scope items", () => {
      const diff = compareEstimateVersions(
        makeVersion({ scopeItems: [{ name: "Env", sequenceNo: 1, scopeType: "ENV", description: "Old" }] }),
        makeVersion({ scopeItems: [{ name: "Env", sequenceNo: 1, scopeType: "ENV", description: "New" }] }),
      );
      expect(diff.scopeItemDiffs[0]!.status).toBe("changed");
      expect(diff.scopeItemDiffs[0]!.changedFields).toContain("description");
    });

    it("marks unchanged scope items", () => {
      const scope = { name: "Asset Hero", sequenceNo: 1, scopeType: "ASSET", frameCount: 50 };
      const diff = compareEstimateVersions(
        makeVersion({ scopeItems: [scope] }),
        makeVersion({ scopeItems: [scope] }),
      );
      expect(diff.scopeItemDiffs[0]!.status).toBe("unchanged");
    });
  });

  describe("resource snapshot diffs", () => {
    it("detects added resources", () => {
      const diff = compareEstimateVersions(
        makeVersion(),
        makeVersion({
          resourceSnapshots: [{ displayName: "John", currency: "EUR", lcrCents: 5000, ucrCents: 8000 }],
        }),
      );
      expect(diff.resourceSnapshotDiffs).toHaveLength(1);
      expect(diff.resourceSnapshotDiffs[0]!.status).toBe("added");
      expect(diff.summary.resourceSnapshotsChanged).toBe(1);
    });

    it("detects rate changes", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          resourceSnapshots: [{ resourceId: "r1", displayName: "Jane", currency: "EUR", lcrCents: 5000, ucrCents: 8000 }],
        }),
        makeVersion({
          resourceSnapshots: [{ resourceId: "r1", displayName: "Jane", currency: "EUR", lcrCents: 5500, ucrCents: 8500 }],
        }),
      );
      expect(diff.resourceSnapshotDiffs[0]!.status).toBe("changed");
      expect(diff.resourceSnapshotDiffs[0]!.lcrDelta).toBe(500);
      expect(diff.resourceSnapshotDiffs[0]!.ucrDelta).toBe(500);
    });

    it("detects removed resources", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          resourceSnapshots: [{ displayName: "Max", currency: "EUR", lcrCents: 4000, ucrCents: 7000 }],
        }),
        makeVersion(),
      );
      expect(diff.resourceSnapshotDiffs[0]!.status).toBe("removed");
    });

    it("matches by resourceId for unchanged rates", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          resourceSnapshots: [{ resourceId: "r1", displayName: "Old Name", currency: "EUR", lcrCents: 5000, ucrCents: 8000 }],
        }),
        makeVersion({
          resourceSnapshots: [{ resourceId: "r1", displayName: "New Name", currency: "EUR", lcrCents: 5000, ucrCents: 8000 }],
        }),
      );
      expect(diff.resourceSnapshotDiffs[0]!.status).toBe("unchanged");
    });

    it("detects location changes", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          resourceSnapshots: [{ resourceId: "r1", displayName: "Alex", currency: "EUR", lcrCents: 5000, ucrCents: 8000, location: "Munich" }],
        }),
        makeVersion({
          resourceSnapshots: [{ resourceId: "r1", displayName: "Alex", currency: "EUR", lcrCents: 5000, ucrCents: 8000, location: "Berlin" }],
        }),
      );
      expect(diff.resourceSnapshotDiffs[0]!.status).toBe("changed");
    });
  });

  describe("chapter subtotals", () => {
    it("groups demand lines by chapter and computes deltas", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          demandLines: [
            makeLine({ id: "d1", name: "Anim Lead", hours: 80, costTotalCents: 480_00, priceTotalCents: 720_00, chapter: "Animation" }),
            makeLine({ id: "d2", name: "Modeler", hours: 60, costTotalCents: 300_00, priceTotalCents: 480_00, chapter: "Modeling" }),
          ],
        }),
        makeVersion({
          demandLines: [
            makeLine({ id: "d1", name: "Anim Lead", hours: 100, costTotalCents: 600_00, priceTotalCents: 900_00, chapter: "Animation" }),
            makeLine({ id: "d2", name: "Modeler", hours: 60, costTotalCents: 300_00, priceTotalCents: 480_00, chapter: "Modeling" }),
          ],
        }),
      );
      expect(diff.chapterSubtotals).toHaveLength(2);
      const anim = diff.chapterSubtotals.find((c) => c.chapter === "Animation")!;
      expect(anim.hoursDelta).toBe(20);
      expect(anim.costDelta).toBe(120_00);
    });

    it("uses (no chapter) fallback", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          demandLines: [makeLine({ id: "d1", name: "Misc", hours: 10, costTotalCents: 30_00, priceTotalCents: 50_00 })],
        }),
        makeVersion(),
      );
      expect(diff.chapterSubtotals[0]!.chapter).toBe("(no chapter)");
    });

    it("sorts by absolute cost delta descending", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          demandLines: [
            makeLine({ id: "d1", name: "A", hours: 10, costTotalCents: 100_00, priceTotalCents: 200_00, chapter: "Small" }),
            makeLine({ id: "d2", name: "B", hours: 100, costTotalCents: 5000_00, priceTotalCents: 8000_00, chapter: "Big" }),
          ],
        }),
        makeVersion({
          demandLines: [
            makeLine({ id: "d1", name: "A", hours: 12, costTotalCents: 120_00, priceTotalCents: 240_00, chapter: "Small" }),
            makeLine({ id: "d2", name: "B", hours: 200, costTotalCents: 10000_00, priceTotalCents: 16000_00, chapter: "Big" }),
          ],
        }),
      );
      expect(diff.chapterSubtotals[0]!.chapter).toBe("Big");
    });
  });

  describe("margin calculation", () => {
    it("computes margin percent and delta", () => {
      const diff = compareEstimateVersions(
        makeVersion({
          demandLines: [makeLine({ id: "d1", name: "A", hours: 100, costTotalCents: 500_00, priceTotalCents: 800_00 })],
        }),
        makeVersion({
          demandLines: [makeLine({ id: "d1", name: "A", hours: 100, costTotalCents: 500_00, priceTotalCents: 1000_00 })],
        }),
      );
      // A: (80000-50000)/80000 = 37.5%
      expect(diff.summary.marginPercentA).toBeCloseTo(37.5, 1);
      // B: (100000-50000)/100000 = 50%
      expect(diff.summary.marginPercentB).toBeCloseTo(50, 1);
      expect(diff.summary.marginPercentDelta).toBeCloseTo(12.5, 1);
    });

    it("handles zero price gracefully", () => {
      const diff = compareEstimateVersions(makeVersion(), makeVersion());
      expect(diff.summary.marginPercentA).toBe(0);
      expect(diff.summary.marginPercentB).toBe(0);
    });
  });
});
