import { describe, expect, it } from "vitest";
import {
  updateCookingAssumptionProfile,
  rollbackCookingAssumptionProfile,
} from "../src/cooking-profiles.js";

describe("updateCookingAssumptionProfile", () => {
  it("initializes profile from first observation", () => {
    const result = updateCookingAssumptionProfile(
      {
        householdId: "h1",
        canonicalIngredientId: "i1",
        plannedPortions: 4,
        actualPortionsEaten: 3,
        leftoverEstimatePortions: 1,
        sessionId: "s1",
        date: "2026-08-07",
      },
      { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 },
    );
    expect(result.averageEatenPortions).toBe(3);
    expect(result.averageLeftoverPortions).toBe(1);
    expect(result.observationCount).toBe(1);
  });

  it("clamps eaten and leftovers to valid ranges", () => {
    const result = updateCookingAssumptionProfile(
      {
        householdId: "h1",
        canonicalIngredientId: "i1",
        plannedPortions: 4,
        actualPortionsEaten: -1,
        leftoverEstimatePortions: 5,
        sessionId: "s1",
        date: "2026-08-07",
      },
      { averageEatenPortions: 2, averageLeftoverPortions: 1, observationCount: 3 },
    );
    // Eaten clamps to 0, leftovers clamps to planned - eaten = 4.
    // alpha = 1/4, prevEaten=2 => 2 + 0.25*(0-2) = 1.5
    // prevLeftovers=1 => 1 + 0.25*(4-1) = 1.75
    expect(result.averageEatenPortions).toBe(1.5);
    expect(result.averageLeftoverPortions).toBe(1.75);
  });

  it("keeps a rolling history of last 10 sessions", () => {
    type Profile = { averageEatenPortions: number | null; averageLeftoverPortions: number | null; observationCount: number; lastSessionAnswers?: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }> | null };
    let existing: Profile = { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 };
    for (let i = 0; i < 12; i++) {
      existing = updateCookingAssumptionProfile(
        {
          householdId: "h1",
          canonicalIngredientId: "i1",
          plannedPortions: 4,
          actualPortionsEaten: 2,
          leftoverEstimatePortions: 1,
          sessionId: `s${i}`,
          date: "2026-08-07",
        },
        existing,
      );
    }
    expect(existing.lastSessionAnswers).toHaveLength(10);
    expect(existing.lastSessionAnswers![0]!.sessionId).toBe("s11");
  });
});

describe("rollbackCookingAssumptionProfile", () => {
  it("returns null averages and zero count when history becomes empty", () => {
    const existing = updateCookingAssumptionProfile(
      {
        householdId: "h1",
        canonicalIngredientId: "i1",
        plannedPortions: 4,
        actualPortionsEaten: 3,
        leftoverEstimatePortions: 1,
        sessionId: "s1",
        date: "2026-08-07",
      },
      { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 },
    );
    const rolled = rollbackCookingAssumptionProfile("s1", existing);
    expect(rolled.averageEatenPortions).toBeNull();
    expect(rolled.averageLeftoverPortions).toBeNull();
    expect(rolled.observationCount).toBe(0);
    expect(rolled.lastSessionAnswers).toHaveLength(0);
  });

  it("recalculates averages deterministically from remaining history", () => {
    let existing: {
      averageEatenPortions: number | null;
      averageLeftoverPortions: number | null;
      observationCount: number;
      lastSessionAnswers?: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }> | null;
    } = { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 };

    // Three chronological observations.
    const observations = [
      { sessionId: "s1", eaten: 4, leftovers: 0 },
      { sessionId: "s2", eaten: 2, leftovers: 1 },
      { sessionId: "s3", eaten: 3, leftovers: 1 },
    ];
    for (const obs of observations) {
      existing = updateCookingAssumptionProfile(
        {
          householdId: "h1",
          canonicalIngredientId: "i1",
          plannedPortions: 4,
          actualPortionsEaten: obs.eaten,
          leftoverEstimatePortions: obs.leftovers,
          sessionId: obs.sessionId,
          date: "2026-08-07",
        },
        existing,
      );
    }

    // Roll back the middle observation and recompute from scratch.
    const rolled = rollbackCookingAssumptionProfile("s2", existing);
    expect(rolled.lastSessionAnswers).toHaveLength(2);
    expect(rolled.lastSessionAnswers!.map((a) => a.sessionId)).toContain("s1");
    expect(rolled.lastSessionAnswers!.map((a) => a.sessionId)).toContain("s3");

    // Recompute EMA manually from s1 then s3.
    // s1: avgEaten=4, avgLeftovers=0
    // s3: alpha=1/2 => eaten=4+0.5*(3-4)=3.5, leftovers=0+0.5*(1-0)=0.5
    expect(rolled.averageEatenPortions).toBe(3.5);
    expect(rolled.averageLeftoverPortions).toBe(0.5);
    expect(rolled.observationCount).toBe(2);
  });

  it("is idempotent: rolling back the same session twice leaves history unchanged", () => {
    let existing: {
      averageEatenPortions: number | null;
      averageLeftoverPortions: number | null;
      observationCount: number;
      lastSessionAnswers?: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }> | null;
    } = { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 };
    existing = updateCookingAssumptionProfile(
      {
        householdId: "h1",
        canonicalIngredientId: "i1",
        plannedPortions: 4,
        actualPortionsEaten: 3,
        leftoverEstimatePortions: 1,
        sessionId: "s1",
        date: "2026-08-07",
      },
      existing,
    );
    existing = updateCookingAssumptionProfile(
      {
        householdId: "h1",
        canonicalIngredientId: "i1",
        plannedPortions: 4,
        actualPortionsEaten: 2,
        leftoverEstimatePortions: 0,
        sessionId: "s2",
        date: "2026-08-07",
      },
      existing,
    );
    const first = rollbackCookingAssumptionProfile("s1", existing);
    const second = rollbackCookingAssumptionProfile("s1", first);
    expect(second).toEqual(first);
  });
});
