import { describe, expect, it } from "vitest";
import type { CoverageResult } from "@app/recipe-engine";
import type { MemoryItem, TasteSignal } from "@app/shared-types";
import {
  containsForbiddenCopy,
  DEFAULT_WEIGHTS,
  easterSunday,
  isEventActive,
  midsummerEve,
  NON_PERSONALIZED_WEIGHTS,
  parseCraving,
  rankAll,
  renderProvenance,
  scoreCandidate,
  seasonForDate,
  type CookingAssumption,
  type RecommendationCandidate,
  type RecommendationContext,
} from "../src/index.js";

const fullCoverage: CoverageResult = { coverage: 1, matches: [], missing: [], expiringUsed: [] };
const nutrition = {
  kcal: 550,
  proteinG: 45,
  carbsG: 50,
  fatG: 18,
  saturatedFatG: 6,
  fiberG: 6,
  sugarG: 4,
  saltG: 1.5,
};

function candidate(overrides: Partial<RecommendationCandidate> = {}): RecommendationCandidate {
  return {
    recipeId: "r1",
    titleSv: "Kycklinggryta",
    cuisine: "swedish",
    tags: [],
    totalTimeMinutes: 30,
    nutritionPerPortion: nutrition,
    estimatedCostMinorPerPortion: 2200,
    ratingAverage: null,
    ratingCount: 0,
    peakSeasons: [],
    holidayTags: [],
    spiceLevel: 1,
    coverage: fullCoverage,
    ...overrides,
  };
}

const ctx: RecommendationContext = {
  mealType: "dinner",
  persons: 4,
  currentSeason: "summer",
  activeHolidayTags: [],
  isWeekday: true,
  favoriteCuisines: ["swedish"],
  remainingProteinG: 60,
  remainingKcal: 800,
  personalizationEnabled: false,
};

describe("poängsättning (spec §18)", () => {
  it("utgångsvaror lyfter rekommendationen", () => {
    const expiring = candidate({
      recipeId: "expiring",
      coverage: {
        coverage: 1,
        matches: [],
        missing: [],
        expiringUsed: [
          {
            canonicalIngredientId: "chicken",
            displayNameSv: "kycklingen",
            required: 500,
            unit: "GRAM",
            availableInUnit: 600,
            covered: true,
            optional: false,
            mostUrgentDaysLeft: 1,
            usesExpiringItem: true,
          },
        ],
      },
    });
    const fresh = candidate({ recipeId: "fresh" });
    const ranked = rankAll([fresh, expiring], ctx);
    expect(ranked[0]?.recipeId).toBe("expiring");
    expect(ranked[0]?.whySv).toContain("senast i morgon");
  });

  it("förklaringen innehåller täckning och protein (specens exempel)", () => {
    const scored = scoreCandidate(
      candidate({
        coverage: { ...fullCoverage, coverage: 0.92 },
        nutritionPerPortion: { ...nutrition, proteinG: 58 },
      }),
      ctx,
    );
    expect(scored.whySv).toContain("92 %");
    expect(scored.whySv).toContain("58 gram protein");
  });

  it("recept över tidsgränsen filtreras bort", () => {
    const slow = candidate({ recipeId: "slow", totalTimeMinutes: 90 });
    const ranked = rankAll([slow], { ...ctx, maxMinutes: 45 });
    expect(ranked).toHaveLength(0);
  });

  it("nyligen lagat straffas (variation)", () => {
    const recent = scoreCandidate(candidate({ daysSinceLastCooked: 2 }), ctx);
    const old = scoreCandidate(candidate({ daysSinceLastCooked: 30 }), ctx);
    expect(old.score).toBeGreaterThan(recent.score);
  });
});

describe("'jag är sugen på' (spec §19)", () => {
  it("tolkar kök, taggar och kcal-gräns", () => {
    const parsed = parseCraving("något krämigt och asiatiskt under 500 kcal");
    expect(parsed.cuisine).toBe("thai");
    expect(parsed.tags).toContain("creamy");
    expect(parsed.maxKcal).toBe(500);
  });
  it("tolkar svenska uttryck", () => {
    expect(parseCraving("snabb husmanskost").tags).toEqual(
      expect.arrayContaining(["quick", "comfort"]),
    );
    expect(parseCraving("barnvänliga tacos").cuisine).toBe("mexican");
  });
});

describe("säsongs- och eventmotor (spec §28)", () => {
  // Motorn räknar i UTC – testerna använder UTC-datum så de är sanna i alla tidszoner.
  it("årstider", () => {
    expect(seasonForDate(new Date("2026-07-15T00:00:00Z"))).toBe("summer");
    expect(seasonForDate(new Date("2026-01-15T00:00:00Z"))).toBe("winter");
  });
  it("midsommarafton är alltid en fredag 19–25 juni", () => {
    for (const year of [2025, 2026, 2027, 2028]) {
      const eve = midsummerEve(year);
      expect(eve.getUTCDay()).toBe(5);
      expect(eve.getUTCMonth()).toBe(5);
      expect(eve.getUTCDate()).toBeGreaterThanOrEqual(19);
      expect(eve.getUTCDate()).toBeLessThanOrEqual(25);
    }
  });
  it("påskdagen: kända referensår", () => {
    expect(easterSunday(2026).toISOString().slice(0, 10)).toBe("2026-04-05");
    expect(easterSunday(2027).toISOString().slice(0, 10)).toBe("2027-03-28");
  });
  it("event aktiveras inom leadDays", () => {
    const midsummer = {
      dateRule: { kind: "computed", algorithm: "midsummer" } as const,
      leadDays: 10,
    };
    expect(isEventActive(midsummer, new Date("2026-06-15T00:00:00Z"))).toBe(true);
    expect(isEventActive(midsummer, new Date("2026-03-01T00:00:00Z"))).toBe(false);
    const jul = {
      dateRule: { kind: "range", startMonthDay: "12-20", endMonthDay: "12-26" } as const,
      leadDays: 21,
    };
    expect(isEventActive(jul, new Date("2026-12-05T00:00:00Z"))).toBe(true);
    expect(isEventActive(jul, new Date("2026-08-02T00:00:00Z"))).toBe(false);
  });
});

describe("S1 personalisering", () => {
  const memoryBase: MemoryItem = {
    id: "m1",
    userId: "u1",
    kind: "structured_fact",
    key: "favorite_cuisine_swedish",
    summarySv: "Gillar svensk mat",
    value: { favoriteCuisine: "swedish" },
    origin: "user_stated",
    confidence: 1,
    verifiedByUser: true,
    paused: false,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  };

  const aiMemory: MemoryItem = {
    ...memoryBase,
    id: "m2",
    origin: "ai_inferred",
    confidence: 0.5,
    verifiedByUser: false,
    value: { favoriteCuisine: "swedish" },
  };

  const tastePositive: TasteSignal = {
    id: "t1",
    userId: "u1",
    axis: "spice",
    direction: 1,
    strength: 0.8,
    origin: "user_stated",
    createdAt: new Date().toISOString(),
  };

  const tasteNegative: TasteSignal = {
    ...tastePositive,
    id: "t2",
    direction: -1,
  };

  const cookingAssumption: CookingAssumption = {
    canonicalIngredientId: "chicken",
    averageEatenPortions: 4,
    averageLeftoverPortions: 0.5,
    observationCount: 5,
  };

  it("memoryFit boostar verified user_stated högre än ai_inferred", () => {
    const verifiedCtx: RecommendationContext = {
      ...ctx,
      personalizationEnabled: true,
      memoryItems: [memoryBase],
      tasteSignals: [],
      cookingAssumptions: [],
    };
    const aiCtx: RecommendationContext = {
      ...ctx,
      personalizationEnabled: true,
      memoryItems: [aiMemory],
      tasteSignals: [],
      cookingAssumptions: [],
    };
    const verified = scoreCandidate(candidate({ cuisine: "swedish" }), verifiedCtx);
    const ai = scoreCandidate(candidate({ cuisine: "swedish" }), aiCtx);
    expect(verified.parts.memoryFit ?? 0).toBeGreaterThan(ai.parts.memoryFit ?? 0);
  });

  it("tasteFit ger positiv riktning för positiv signal", () => {
    const spicyCandidate = candidate({ spiceLevel: 3 });
    const positiveCtx: RecommendationContext = {
      ...ctx,
      personalizationEnabled: true,
      memoryItems: [],
      tasteSignals: [tastePositive],
      cookingAssumptions: [],
    };
    const negativeCtx: RecommendationContext = {
      ...ctx,
      personalizationEnabled: true,
      memoryItems: [],
      tasteSignals: [tasteNegative],
      cookingAssumptions: [],
    };
    const positive = scoreCandidate(spicyCandidate, positiveCtx);
    const negative = scoreCandidate(spicyCandidate, negativeCtx);
    expect(positive.parts.tasteFit ?? 0).toBeGreaterThan(negative.parts.tasteFit ?? 0);
  });

  it("cookingAssumptionFit boostar ingrediens som hushållet brukar äta upp", () => {
    const ctxWithAssumption: RecommendationContext = {
      ...ctx,
      personalizationEnabled: true,
      memoryItems: [],
      tasteSignals: [],
      cookingAssumptions: [cookingAssumption],
    };
    const boosted = scoreCandidate(
      candidate({ ingredientIds: ["chicken", "onion"] }),
      ctxWithAssumption,
    );
    const noMatch = scoreCandidate(
      candidate({ ingredientIds: ["beef", "onion"] }),
      ctxWithAssumption,
    );
    expect(boosted.parts.cookingAssumptionFit ?? 0).toBeGreaterThan(
      noMatch.parts.cookingAssumptionFit ?? 0,
    );
  });

  it("utan personalizationEnabled är personliga delpoäng noll", () => {
    const noPersonalizationCtx: RecommendationContext = {
      ...ctx,
      personalizationEnabled: false,
      memoryItems: [memoryBase],
      tasteSignals: [tastePositive],
      cookingAssumptions: [cookingAssumption],
    };
    const scored = scoreCandidate(
      candidate({ cuisine: "swedish", spiceLevel: 3 }),
      noPersonalizationCtx,
    );
    expect(scored.parts.memoryFit).toBe(0);
    expect(scored.parts.tasteFit).toBe(0);
    expect(scored.parts.cookingAssumptionFit).toBe(0);
  });

  it("proveniens renderas från mall med grundade fakta", () => {
    const rendered = renderProvenance(
      [{ key: "favoriteCuisine", args: { cuisine: "svensk" } }],
      "sv-SE",
    );
    expect(rendered).toContain("svensk");
    expect(rendered).toContain("berättat");
  });

  it("förbjuden copy upptäcks", () => {
    expect(containsForbiddenCopy("Du har överskridit ditt kalorimål")).toBe(true);
    expect(containsForbiddenCopy("Passar ditt proteinmål")).toBe(false);
  });

  it("scoring är deterministisk", () => {
    const personalCtx: RecommendationContext = {
      ...ctx,
      personalizationEnabled: true,
      memoryItems: [memoryBase],
      tasteSignals: [tastePositive],
      cookingAssumptions: [cookingAssumption],
    };
    const c = candidate({ ingredientIds: ["chicken"], spiceLevel: 3 });
    const a = scoreCandidate(c, personalCtx);
    const b = scoreCandidate(c, personalCtx);
    expect(a.score).toBe(b.score);
    expect(a.whySv).toBe(b.whySv);
  });

  it("icke-personaliserade vikter har noll för S1-komponenter", () => {
    expect(NON_PERSONALIZED_WEIGHTS.memoryFit).toBe(0);
    expect(NON_PERSONALIZED_WEIGHTS.tasteFit).toBe(0);
    expect(NON_PERSONALIZED_WEIGHTS.cookingAssumptionFit).toBe(0);
    // Befintliga vikter ska vara oförändrade.
    expect(NON_PERSONALIZED_WEIGHTS.coverage).toBe(DEFAULT_WEIGHTS.coverage);
  });
});
