import "./setup-env.js";
import { describe, it, expect, beforeEach, afterAll } from "vitest";
import { and, eq, gte, lt } from "drizzle-orm";
import { createDatabase, closeDatabase, schema } from "@app/database";
import { processProactiveTips } from "../src/processors/maintenance.js";
import type { WorkerContext } from "../src/context.js";

const FIXED_UTC = new Date(Date.UTC(2026, 7, 10, 8, 0, 0)); // 2026-08-10 08:00 UTC

describe.sequential("SEND_PROACTIVE_TIPS", () => {
  const testDb = createDatabase(process.env.TEST_DATABASE_URL!);
  const ctx: WorkerContext = { db: testDb.db } as never;
  const emailBase = "proactive-tips-test";

  async function cleanup() {
    const emails = [`${emailBase}-owner@example.invalid`, `${emailBase}-member@example.invalid`];
    const users = await testDb.db
      .select({ id: schema.users.id })
      .from(schema.users)
      .where(eq(schema.users.email, emails[0]));
    const userIds = users.map((u) => u.id);
    for (const userId of userIds) {
      await testDb.db.delete(schema.userConsents).where(eq(schema.userConsents.userId, userId));
      await testDb.db
        .delete(schema.householdMembers)
        .where(eq(schema.householdMembers.userId, userId));
      await testDb.db.delete(schema.notifications).where(eq(schema.notifications.userId, userId));
    }
    const households = await testDb.db
      .select({ id: schema.households.id })
      .from(schema.households)
      .where(eq(schema.households.name, "Proactive Tips Test"));
    for (const h of households) {
      await testDb.db
        .delete(schema.inventoryItems)
        .where(eq(schema.inventoryItems.householdId, h.id));
      await testDb.db
        .delete(schema.storageLocations)
        .where(eq(schema.storageLocations.householdId, h.id));
      await testDb.db.delete(schema.households).where(eq(schema.households.id, h.id));
    }
    for (const email of emails) {
      await testDb.db.delete(schema.users).where(eq(schema.users.email, email));
    }
    // Clean up test recipes/ingredients if exists.
    await testDb.db
      .delete(schema.recipeIngredients)
      .where(eq(schema.recipeIngredients.recipeId, "00000000-0000-0000-0000-000000000abc"));
    await testDb.db
      .delete(schema.recipeIngredients)
      .where(eq(schema.recipeIngredients.recipeId, "00000000-0000-0000-0000-000000000def"));
    await testDb.db
      .delete(schema.recipes)
      .where(eq(schema.recipes.id, "00000000-0000-0000-0000-000000000abc"));
    await testDb.db
      .delete(schema.recipes)
      .where(eq(schema.recipes.id, "00000000-0000-0000-0000-000000000def"));
    await testDb.db
      .delete(schema.canonicalIngredients)
      .where(eq(schema.canonicalIngredients.id, "test_cucumber_001"));
    await testDb.db
      .delete(schema.canonicalIngredients)
      .where(eq(schema.canonicalIngredients.id, "test_tomato_001"));
  }

  async function setup() {
    const [owner] = await testDb.db
      .insert(schema.users)
      .values({
        email: `${emailBase}-owner@example.invalid`,
        passwordHash: "not-used",
        displayName: "Owner",
      })
      .returning();
    const [member] = await testDb.db
      .insert(schema.users)
      .values({
        email: `${emailBase}-member@example.invalid`,
        passwordHash: "not-used",
        displayName: "Member",
      })
      .returning();

    const [household] = await testDb.db
      .insert(schema.households)
      .values({
        name: "Proactive Tips Test",
        size: 2,
        locale: "sv-SE",
        inviteCode: "PROACTIVETEST",
      })
      .returning();

    await testDb.db.insert(schema.householdMembers).values([
      { householdId: household!.id, userId: owner!.id, role: "owner" },
      { householdId: household!.id, userId: member!.id, role: "member" },
    ]);

    const [pantry] = await testDb.db
      .insert(schema.storageLocations)
      .values({ householdId: household!.id, type: "pantry", name: "Skafferi" })
      .returning();

    await testDb.db.insert(schema.canonicalIngredients).values({
      id: "test_cucumber_001",
      nameSv: "Gurka",
      nameEn: "Cucumber",
      category: "vegetable",
      defaultUnit: "COUNT",
      allergens: [],
      containsGluten: false,
      containsLactose: false,
      isPork: false,
      isBeef: false,
      isAlcohol: false,
      densityGPerMl: 0.95,
      nutritionPer100: { kcal: 15, proteinG: 0.7, carbsG: 3.6, fatG: 0.1 },
      nutritionProvenance: { source: "test", confidence: "high" },
      shelfLifeGuidance: { pantryDays: 7, fridgeDays: 10 },
    });

    await testDb.db.insert(schema.recipes).values({
      id: "00000000-0000-0000-0000-000000000abc",
      slug: "test-gurkraita",
      titleSv: "Gurkraita",
      descriptionSv: "Fräsch gurkraita.",
      cuisine: "international",
      mealTypes: ["dinner"],
      tags: ["quick", "vegetarian"],
      methods: ["stovetop"],
      difficulty: "easy",
      sourceType: "own_editorial",
      verificationStatus: "verified",
      status: "published",
      portions: 4,
      prepTimeMinutes: 5,
      cookTimeMinutes: 0,
      totalTimeMinutes: 5,
      nutritionPerPortion: { kcal: 120, proteinG: 4, carbsG: 8, fatG: 8 },
      dna: {
        cuisine: "international",
        vegetables: ["cucumber"],
        flavorProfile: ["fresh"],
        spiceLevel: 0,
        method: "stovetop",
        timeMinutes: 5,
        calories: 120,
        proteinGrams: 4,
      },
    });

    await testDb.db.insert(schema.recipeIngredients).values({
      recipeId: "00000000-0000-0000-0000-000000000abc",
      canonicalIngredientId: "test_cucumber_001",
      displayNameSv: "Gurka",
      quantity: 1,
      unit: "COUNT",
      optional: false,
    });

    return {
      ownerId: owner!.id,
      memberId: member!.id,
      householdId: household!.id,
      pantryId: pantry!.id,
    };
  }

  async function addExpiringCucumber(householdId: string, pantryId: string, bestBefore: string) {
    return testDb.db.insert(schema.inventoryItems).values({
      householdId,
      storageLocationId: pantryId,
      canonicalIngredientId: "test_cucumber_001",
      displayName: "Gurka",
      quantity: 2,
      unit: "COUNT",
      bestBeforeDate: bestBefore,
      confidence: 1,
      verifiedByUser: true,
      trustState: "fresh",
      source: "manual_search",
    });
  }

  async function grantConsents(userId: string) {
    await testDb.db.insert(schema.userConsents).values([
      { userId, kind: "personalization", status: "granted" },
      { userId, kind: "notifications", status: "granted" },
    ]);
  }

  beforeEach(async () => {
    await cleanup();
  });

  afterAll(async () => {
    await cleanup();
    await closeDatabase();
  });

  it("skapar en proaktiv puff när båda samtyckena finns och en vara går ut", async () => {
    const { ownerId, householdId, pantryId } = await setup();
    await grantConsents(ownerId);
    await addExpiringCucumber(householdId, pantryId, "2026-08-11"); // 1 dag kvar

    const count = await processProactiveTips(ctx, { nowUtc: FIXED_UTC });
    expect(count).toBe(1);

    const notif = await testDb.db
      .select()
      .from(schema.notifications)
      .where(eq(schema.notifications.userId, ownerId));
    expect(notif).toHaveLength(1);
    expect(notif[0]!.type).toBe("proactive_tip");
    expect(notif[0]!.titleSv).toContain("Gurka");
    expect(notif[0]!.bodySv).toContain("Gurka");
    expect(notif[0]!.templateKey).toBe("notification.proactive_tip.expiring_ingredient");
  });

  it("skapar INGEN puff utan notifications-samtycke", async () => {
    const { ownerId, householdId, pantryId } = await setup();
    await testDb.db.insert(schema.userConsents).values({
      userId: ownerId,
      kind: "personalization",
      status: "granted",
    });
    await addExpiringCucumber(householdId, pantryId, "2026-08-11");

    const count = await processProactiveTips(ctx, { nowUtc: FIXED_UTC });
    expect(count).toBe(0);
  });

  it("skapar INGEN puff utan personalization-samtycke", async () => {
    const { ownerId, householdId, pantryId } = await setup();
    await testDb.db.insert(schema.userConsents).values({
      userId: ownerId,
      kind: "notifications",
      status: "granted",
    });
    await addExpiringCucumber(householdId, pantryId, "2026-08-11");

    const count = await processProactiveTips(ctx, { nowUtc: FIXED_UTC });
    expect(count).toBe(0);
  });

  it("stoppas vid revoke av notifications-samtycke", async () => {
    const { ownerId, householdId, pantryId } = await setup();
    await grantConsents(ownerId);
    await addExpiringCucumber(householdId, pantryId, "2026-08-11");

    // Först skapas puffen.
    let count = await processProactiveTips(ctx, { nowUtc: FIXED_UTC });
    expect(count).toBe(1);

    // Revoke.
    await testDb.db
      .update(schema.userConsents)
      .set({ status: "revoked" })
      .where(
        and(eq(schema.userConsents.userId, ownerId), eq(schema.userConsents.kind, "notifications")),
      );

    // Rensa notisen för att simulera ny dag.
    await testDb.db.delete(schema.notifications).where(eq(schema.notifications.userId, ownerId));

    count = await processProactiveTips(ctx, {
      nowUtc: new Date(FIXED_UTC.getTime() + 24 * 60 * 60 * 1000),
    });
    expect(count).toBe(0);
  });

  it("max 1 puff per hushåll per dag", async () => {
    const { ownerId, memberId, householdId, pantryId } = await setup();
    await grantConsents(ownerId);
    await grantConsents(memberId);
    await addExpiringCucumber(householdId, pantryId, "2026-08-11");

    const count = await processProactiveTips(ctx, { nowUtc: FIXED_UTC });
    expect(count).toBe(1);

    const notifs = await testDb.db
      .select()
      .from(schema.notifications)
      .where(eq(schema.notifications.type, "proactive_tip"));
    expect(notifs).toHaveLength(1);
  });

  it("ingen upprepning av samma vara+recept inom 7 dagar", async () => {
    const { ownerId, householdId, pantryId } = await setup();
    await grantConsents(ownerId);
    await addExpiringCucumber(householdId, pantryId, "2026-08-11");

    let count = await processProactiveTips(ctx, { nowUtc: FIXED_UTC });
    expect(count).toBe(1);

    // Efter 7 dagar är dagsgränsen passerad, men 7-dagars duplicate-gräns stoppar samma vara+recept.
    count = await processProactiveTips(ctx, {
      nowUtc: new Date(Date.UTC(2026, 7, 17, 8, 0, 0)),
    });
    expect(count).toBe(0);
  });

  it("UTC-gränser: ny dag kl 00:00 UTC tillåter ny puff för ny vara", async () => {
    const { ownerId, householdId, pantryId } = await setup();
    await grantConsents(ownerId);
    await addExpiringCucumber(householdId, pantryId, "2026-08-11");

    let count = await processProactiveTips(ctx, { nowUtc: FIXED_UTC });
    expect(count).toBe(1);

    // Lägg till en andra utgångsnära vara för att testa att dagsgränsen är UTC.
    await testDb.db.insert(schema.canonicalIngredients).values({
      id: "test_tomato_001",
      nameSv: "Tomat",
      nameEn: "Tomato",
      category: "vegetable",
      defaultUnit: "COUNT",
      allergens: [],
      containsGluten: false,
      containsLactose: false,
      isPork: false,
      isBeef: false,
      isAlcohol: false,
      densityGPerMl: 0.6,
      nutritionPer100: { kcal: 18, proteinG: 0.9, carbsG: 3.9, fatG: 0.2 },
      nutritionProvenance: { source: "test", confidence: "high" },
      shelfLifeGuidance: { pantryDays: 5, fridgeDays: 8 },
    });
    await testDb.db.insert(schema.recipes).values({
      id: "00000000-0000-0000-0000-000000000def",
      slug: "test-tomatsallad",
      titleSv: "Tomatsallad",
      descriptionSv: "Fräsch tomatsallad.",
      cuisine: "international",
      mealTypes: ["dinner"],
      tags: ["quick", "vegetarian"],
      methods: ["stovetop"],
      difficulty: "easy",
      sourceType: "own_editorial",
      verificationStatus: "verified",
      status: "published",
      portions: 4,
      prepTimeMinutes: 5,
      cookTimeMinutes: 0,
      totalTimeMinutes: 5,
      nutritionPerPortion: { kcal: 120, proteinG: 4, carbsG: 8, fatG: 8 },
      dna: {
        cuisine: "international",
        vegetables: ["tomato"],
        flavorProfile: ["fresh"],
        spiceLevel: 0,
        method: "stovetop",
        timeMinutes: 5,
        calories: 120,
        proteinGrams: 4,
      },
    });
    await testDb.db.insert(schema.recipeIngredients).values({
      recipeId: "00000000-0000-0000-0000-000000000def",
      canonicalIngredientId: "test_tomato_001",
      displayNameSv: "Tomat",
      quantity: 1,
      unit: "COUNT",
      optional: false,
    });
    await testDb.db.insert(schema.inventoryItems).values({
      householdId,
      storageLocationId: pantryId,
      canonicalIngredientId: "test_tomato_001",
      displayName: "Tomat",
      quantity: 2,
      unit: "COUNT",
      bestBeforeDate: "2026-08-11",
      confidence: 1,
      verifiedByUser: true,
      trustState: "fresh",
      source: "manual_search",
    });

    count = await processProactiveTips(ctx, {
      nowUtc: new Date(Date.UTC(2026, 7, 11, 0, 0, 0)),
    });
    expect(count).toBe(1);
  });

  it("texten leder med nyttan och innehåller ingen förbjuden copy", async () => {
    const { ownerId, householdId, pantryId } = await setup();
    await grantConsents(ownerId);
    await addExpiringCucumber(householdId, pantryId, "2026-08-11");

    await processProactiveTips(ctx, { nowUtc: FIXED_UTC });

    const [notif] = await testDb.db
      .select()
      .from(schema.notifications)
      .where(eq(schema.notifications.userId, ownerId));
    expect(notif!.bodySv).toMatch(/slapp|rädda|börjar bli/i);
    expect(notif!.bodySv).not.toMatch(/skam|borde äta mindre|obalanserad|dålig vana/i);
  });
});
