import { describe, expect, it } from "vitest";
import {
  expandScopeToEffort,
  aggregateByDiscipline,
  type EffortRuleInput,
  type ScopeItemInput,
} from "../estimate/effort-rules.js";

const STANDARD_RULES: EffortRuleInput[] = [
  { scopeType: "SHOT", discipline: "3D Animation", chapter: "Animation", unitMode: "per_frame", hoursPerUnit: 0.5, sortOrder: 0 },
  { scopeType: "SHOT", discipline: "3D Lighting", chapter: "Lighting", unitMode: "per_frame", hoursPerUnit: 0.2, sortOrder: 1 },
  { scopeType: "SHOT", discipline: "Compositing", chapter: "Compositing", unitMode: "per_frame", hoursPerUnit: 0.15, sortOrder: 2 },
  { scopeType: "ASSET", discipline: "3D Modeling", chapter: "Modeling", unitMode: "per_item", hoursPerUnit: 40, sortOrder: 0 },
  { scopeType: "ASSET", discipline: "3D Rigging", chapter: "Rigging", unitMode: "per_item", hoursPerUnit: 24, sortOrder: 1 },
  { scopeType: "ENVIRONMENT", discipline: "3D Environment", chapter: "Environment", unitMode: "flat", hoursPerUnit: 80, sortOrder: 0 },
];

describe("expandScopeToEffort", () => {
  it("expands a single shot scope item into multiple discipline lines", () => {
    const items: ScopeItemInput[] = [
      { name: "Shot 001", scopeType: "SHOT", frameCount: 120 },
    ];
    const result = expandScopeToEffort(items, STANDARD_RULES);

    expect(result.lines).toHaveLength(3);
    expect(result.warnings).toHaveLength(0);
    expect(result.unmatchedScopeItems).toHaveLength(0);

    const anim = result.lines.find((l) => l.discipline === "3D Animation")!;
    expect(anim.hours).toBe(60); // 120 * 0.5
    expect(anim.unitCount).toBe(120);
    expect(anim.chapter).toBe("Animation");

    const lighting = result.lines.find((l) => l.discipline === "3D Lighting")!;
    expect(lighting.hours).toBe(24); // 120 * 0.2

    const comp = result.lines.find((l) => l.discipline === "Compositing")!;
    expect(comp.hours).toBe(18); // 120 * 0.15
  });

  it("expands asset scope items using per_item mode", () => {
    const items: ScopeItemInput[] = [
      { name: "Hero Character", scopeType: "ASSET", itemCount: 3 },
    ];
    const result = expandScopeToEffort(items, STANDARD_RULES);

    expect(result.lines).toHaveLength(2);
    const modeling = result.lines.find((l) => l.discipline === "3D Modeling")!;
    expect(modeling.hours).toBe(120); // 3 * 40
    const rigging = result.lines.find((l) => l.discipline === "3D Rigging")!;
    expect(rigging.hours).toBe(72); // 3 * 24
  });

  it("uses flat hours for environment scope items", () => {
    const items: ScopeItemInput[] = [
      { name: "Forest Scene", scopeType: "ENVIRONMENT" },
    ];
    const result = expandScopeToEffort(items, STANDARD_RULES);

    expect(result.lines).toHaveLength(1);
    expect(result.lines[0]!.hours).toBe(80); // flat
    expect(result.lines[0]!.unitCount).toBe(1);
  });

  it("defaults to 1 when frameCount is null in per_frame mode", () => {
    const items: ScopeItemInput[] = [
      { name: "Shot X", scopeType: "SHOT", frameCount: null },
    ];
    const result = expandScopeToEffort(items, STANDARD_RULES);

    const anim = result.lines.find((l) => l.discipline === "3D Animation")!;
    expect(anim.unitCount).toBe(1);
    expect(anim.hours).toBe(0.5); // 1 * 0.5
  });

  it("handles multiple scope items", () => {
    const items: ScopeItemInput[] = [
      { name: "Shot 001", scopeType: "SHOT", frameCount: 100 },
      { name: "Shot 002", scopeType: "SHOT", frameCount: 200 },
      { name: "Prop A", scopeType: "ASSET", itemCount: 1 },
    ];
    const result = expandScopeToEffort(items, STANDARD_RULES);

    // 2 shots * 3 rules + 1 asset * 2 rules = 8 lines
    expect(result.lines).toHaveLength(8);
  });

  it("reports unmatched scope items", () => {
    const items: ScopeItemInput[] = [
      { name: "Audio Track", scopeType: "AUDIO" },
    ];
    const result = expandScopeToEffort(items, STANDARD_RULES);

    expect(result.lines).toHaveLength(0);
    expect(result.unmatchedScopeItems).toContain("Audio Track");
    expect(result.warnings.length).toBeGreaterThan(0);
  });

  it("matches scope type case-insensitively", () => {
    const items: ScopeItemInput[] = [
      { name: "Shot 001", scopeType: "shot", frameCount: 50 },
    ];
    const result = expandScopeToEffort(items, STANDARD_RULES);
    expect(result.lines).toHaveLength(3);
  });

  it("warns when no rules provided", () => {
    const result = expandScopeToEffort(
      [{ name: "Shot 001", scopeType: "SHOT", frameCount: 100 }],
      [],
    );
    expect(result.lines).toHaveLength(0);
    expect(result.warnings).toContain("No effort rules provided.");
  });

  it("skips lines with zero hours", () => {
    const rules: EffortRuleInput[] = [
      { scopeType: "SHOT", discipline: "QC", unitMode: "per_frame", hoursPerUnit: 0, sortOrder: 0 },
    ];
    const result = expandScopeToEffort(
      [{ name: "Shot 001", scopeType: "SHOT", frameCount: 100 }],
      rules,
    );
    expect(result.lines).toHaveLength(0);
    expect(result.warnings.some((w) => w.includes("Skipped"))).toBe(true);
  });

  it("skips empty-name scope items", () => {
    const result = expandScopeToEffort(
      [{ name: "", scopeType: "SHOT", frameCount: 100 }],
      STANDARD_RULES,
    );
    expect(result.lines).toHaveLength(0);
  });

  it("rounds hours to 2 decimal places", () => {
    const rules: EffortRuleInput[] = [
      { scopeType: "SHOT", discipline: "FX", unitMode: "per_frame", hoursPerUnit: 0.333, sortOrder: 0 },
    ];
    const result = expandScopeToEffort(
      [{ name: "Shot 001", scopeType: "SHOT", frameCount: 7 }],
      rules,
    );
    // 7 * 0.333 = 2.331
    expect(result.lines[0]!.hours).toBe(2.33);
  });

  it("preserves sortOrder in output order", () => {
    const rules: EffortRuleInput[] = [
      { scopeType: "SHOT", discipline: "Z-Last", unitMode: "flat", hoursPerUnit: 1, sortOrder: 10 },
      { scopeType: "SHOT", discipline: "A-First", unitMode: "flat", hoursPerUnit: 1, sortOrder: 0 },
    ];
    const result = expandScopeToEffort(
      [{ name: "Shot 001", scopeType: "SHOT" }],
      rules,
    );
    expect(result.lines[0]!.discipline).toBe("A-First");
    expect(result.lines[1]!.discipline).toBe("Z-Last");
  });
});

describe("aggregateByDiscipline", () => {
  it("sums hours across scope items per discipline", () => {
    const items: ScopeItemInput[] = [
      { name: "Shot 001", scopeType: "SHOT", frameCount: 100 },
      { name: "Shot 002", scopeType: "SHOT", frameCount: 200 },
    ];
    const { lines } = expandScopeToEffort(items, STANDARD_RULES);
    const aggregated = aggregateByDiscipline(lines);

    const anim = aggregated.find((a) => a.discipline === "3D Animation")!;
    expect(anim.totalHours).toBe(150); // (100+200) * 0.5
    expect(anim.lineCount).toBe(2);
  });

  it("sorts by totalHours descending", () => {
    const items: ScopeItemInput[] = [
      { name: "Shot 001", scopeType: "SHOT", frameCount: 100 },
    ];
    const { lines } = expandScopeToEffort(items, STANDARD_RULES);
    const aggregated = aggregateByDiscipline(lines);

    expect(aggregated[0]!.totalHours).toBeGreaterThanOrEqual(aggregated[1]!.totalHours);
  });

  it("keeps discipline+chapter pairs separate", () => {
    const rules: EffortRuleInput[] = [
      { scopeType: "SHOT", discipline: "Animation", chapter: "2D", unitMode: "flat", hoursPerUnit: 10, sortOrder: 0 },
      { scopeType: "SHOT", discipline: "Animation", chapter: "3D", unitMode: "flat", hoursPerUnit: 20, sortOrder: 1 },
    ];
    const { lines } = expandScopeToEffort(
      [{ name: "Shot 001", scopeType: "SHOT" }],
      rules,
    );
    const aggregated = aggregateByDiscipline(lines);

    expect(aggregated).toHaveLength(2);
    const anim2d = aggregated.find((a) => a.chapter === "2D")!;
    expect(anim2d.totalHours).toBe(10);
    const anim3d = aggregated.find((a) => a.chapter === "3D")!;
    expect(anim3d.totalHours).toBe(20);
  });

  it("handles empty input", () => {
    expect(aggregateByDiscipline([])).toHaveLength(0);
  });
});
