import type { DateTime } from "@batch-cooking/date-tools";
import { ErrorCode, type SignupInput } from "@batch-cooking/shared";
import { faker } from "@faker-js/faker";
import { expect } from "chai";
import request from "supertest";
import { createApp } from "../src/app.js";
import { prisma } from "../src/db/prisma.js";
import { TEST_REFERENCE_DATE } from "../test-support/reference-date.js";
import { resetDatabase } from "../test-support/reset-db.js";

/** See `auth.test.ts` — same rationale for generating rather than hardcoding. */
function buildSignupPayload(): SignupInput {
  const firstName = faker.person.firstName();
  const lastName = faker.person.lastName();
  return {
    firstName,
    lastName,
    email: faker.internet.email({ firstName, lastName }).toLowerCase(),
    password: faker.internet.password({ length: 16 }),
  };
}

/** The fixed test "today", as the `YYYY-MM-DD` string `GET /shopping-list`'s `?date=` expects. */
function today(): string {
  return isoDate(TEST_REFERENCE_DATE);
}

/** `toISODate()` only returns `null` for an invalid `DateTime` — never the case for the always-valid values built in this file. */
function isoDate(date: DateTime): string {
  const iso = date.toISODate();
  if (iso === null) throw new Error("Unexpectedly invalid DateTime in a test helper");
  return iso;
}

/** Resolves a reference ingredient's id by its `reference-seed-data.ts` uid (also its DB `key`) — same helper as `recipe.test.ts`. */
async function ingredientId(key: string): Promise<number> {
  const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key } });
  return ingredient.id;
}

/** Resolves a reference unit's id by its `reference-seed-data.ts` uid — same helper as `recipe.test.ts`. */
async function unitId(key: string): Promise<number> {
  const unit = await prisma.unit.findFirstOrThrow({ where: { key } });
  return unit.id;
}

describe("Shopping list", () => {
  const app = createApp();

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

  after(async () => {
    await prisma.$disconnect();
  });

  describe("GET /shopping-list", () => {
    it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
      const res = await request(app).get("/shopping-list").query({ date: today() });

      expect(res.status).to.equal(401);
      expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
    });

    it("rejects a missing date with 400 VALIDATION_ERROR", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());

      const res = await agent.get("/shopping-list");

      expect(res.status).to.equal(400);
      expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR);
    });

    it("rejects a malformed date with 400 VALIDATION_ERROR", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());

      const res = await agent.get("/shopping-list").query({ date: "not-a-date" });

      expect(res.status).to.equal(400);
      expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR);
    });

    it("rejects a date shaped right but calendarially impossible with 400 VALIDATION_ERROR", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());

      const res = await agent.get("/shopping-list").query({ date: "2026-02-30" });

      expect(res.status).to.equal(400);
      expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR);
    });

    it("returns an empty list when the profile has no household", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());

      const res = await agent.get("/shopping-list").query({ date: today() });

      expect(res.status).to.equal(200);
      expect(res.body.items).to.deep.equal([]);
    });

    it("returns an empty list when the household has no planning covering that date", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());
      await agent.post("/house").send({ name: "Chez moi" });

      const res = await agent.get("/shopping-list").query({ date: today() });

      expect(res.status).to.equal(200);
      expect(res.body.items).to.deep.equal([]);
    });

    it("sums one recipe's ingredient across two planning slots, scaled by each slot's own portions", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());
      const houseRes = await agent.post("/house").send({ name: "Chez moi" });
      const houseId: number = houseRes.body.id;
      const authorId: number = houseRes.body.adminId;

      const tomatoId = await ingredientId("tomato");
      const gramId = await unitId("gram");

      // Written for 2 portions, 100g tomato — planned twice this week at
      // 4 portions each, so the shopping list should show 100 × (4/2) × 2
      // = 400g, not the raw 200g the recipe itself lists.
      const recipe = await prisma.recipe.create({
        data: {
          name: "Salade de tomates",
          authorId,
          portions: 2,
          ingredients: { create: [{ ingredientId: tomatoId, quantity: 100, unitId: gramId }] },
        },
      });
      const planning = await prisma.planning.create({
        data: {
          houseId,
          startDate: TEST_REFERENCE_DATE.minus({ days: 2 }).toJSDate(),
          finishDate: TEST_REFERENCE_DATE.plus({ days: 2 }).toJSDate(),
        },
      });
      await prisma.planningItem.createMany({
        data: [
          {
            planningId: planning.id,
            weekDay: "lundi",
            meal: "dejeuner",
            recipeId: recipe.id,
            portions: 4,
          },
          {
            planningId: planning.id,
            weekDay: "mercredi",
            meal: "diner",
            recipeId: recipe.id,
            portions: 4,
          },
        ],
      });

      const res = await agent.get("/shopping-list").query({ date: today() });

      expect(res.status).to.equal(200);
      expect(res.body.items).to.have.length(1);
      expect(res.body.items[0].ingredient.key).to.equal("tomato");
      expect(res.body.items[0].unit.key).to.equal("gram");
      expect(res.body.items[0].quantity).to.equal(400);
    });

    it("sums the same ingredient across two different recipes sharing a unit", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());
      const houseRes = await agent.post("/house").send({ name: "Chez moi" });
      const houseId: number = houseRes.body.id;
      const authorId: number = houseRes.body.adminId;

      const onionId = await ingredientId("onion");
      const gramId = await unitId("gram");

      const recipeA = await prisma.recipe.create({
        data: {
          name: "Soupe à l'oignon",
          authorId,
          portions: 4,
          ingredients: { create: [{ ingredientId: onionId, quantity: 200, unitId: gramId }] },
        },
      });
      const recipeB = await prisma.recipe.create({
        data: {
          name: "Tarte à l'oignon",
          authorId,
          portions: 4,
          ingredients: { create: [{ ingredientId: onionId, quantity: 150, unitId: gramId }] },
        },
      });
      const planning = await prisma.planning.create({
        data: {
          houseId,
          startDate: TEST_REFERENCE_DATE.minus({ days: 2 }).toJSDate(),
          finishDate: TEST_REFERENCE_DATE.plus({ days: 2 }).toJSDate(),
        },
      });
      await prisma.planningItem.createMany({
        data: [
          {
            planningId: planning.id,
            weekDay: "lundi",
            meal: "dejeuner",
            recipeId: recipeA.id,
            portions: 4,
          },
          {
            planningId: planning.id,
            weekDay: "mardi",
            meal: "diner",
            recipeId: recipeB.id,
            portions: 4,
          },
        ],
      });

      const res = await agent.get("/shopping-list").query({ date: today() });

      expect(res.status).to.equal(200);
      expect(res.body.items).to.have.length(1);
      expect(res.body.items[0].ingredient.key).to.equal("onion");
      expect(res.body.items[0].quantity).to.equal(350);
    });

    it("keeps the same ingredient in two different units as two separate lines", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());
      const houseRes = await agent.post("/house").send({ name: "Chez moi" });
      const houseId: number = houseRes.body.id;
      const authorId: number = houseRes.body.adminId;

      const tomatoId = await ingredientId("tomato");
      const gramId = await unitId("gram");
      const kilogramId = await unitId("kilogram");

      const recipeA = await prisma.recipe.create({
        data: {
          name: "Recette A",
          authorId,
          portions: 2,
          ingredients: { create: [{ ingredientId: tomatoId, quantity: 100, unitId: gramId }] },
        },
      });
      const recipeB = await prisma.recipe.create({
        data: {
          name: "Recette B",
          authorId,
          portions: 2,
          ingredients: { create: [{ ingredientId: tomatoId, quantity: 1, unitId: kilogramId }] },
        },
      });
      const planning = await prisma.planning.create({
        data: {
          houseId,
          startDate: TEST_REFERENCE_DATE.minus({ days: 2 }).toJSDate(),
          finishDate: TEST_REFERENCE_DATE.plus({ days: 2 }).toJSDate(),
        },
      });
      await prisma.planningItem.createMany({
        data: [
          {
            planningId: planning.id,
            weekDay: "lundi",
            meal: "dejeuner",
            recipeId: recipeA.id,
            portions: 2,
          },
          {
            planningId: planning.id,
            weekDay: "mardi",
            meal: "diner",
            recipeId: recipeB.id,
            portions: 2,
          },
        ],
      });

      const res = await agent.get("/shopping-list").query({ date: today() });

      expect(res.status).to.equal(200);
      expect(res.body.items).to.have.length(2);
      const units = res.body.items.map((item: { unit: { key: string } }) => item.unit.key).sort();
      expect(units).to.deep.equal(["gram", "kilogram"]);
    });

    it("returns a different week's shopping list when asked for a date outside the current one", async () => {
      const agent = request.agent(app);
      await agent.post("/auth/signup").send(buildSignupPayload());
      const houseRes = await agent.post("/house").send({ name: "Chez moi" });
      const houseId: number = houseRes.body.id;
      const authorId: number = houseRes.body.adminId;

      const tomatoId = await ingredientId("tomato");
      const gramId = await unitId("gram");
      const recipe = await prisma.recipe.create({
        data: {
          name: "Curry de lentilles",
          authorId,
          portions: 2,
          ingredients: { create: [{ ingredientId: tomatoId, quantity: 100, unitId: gramId }] },
        },
      });
      const nextWeek = TEST_REFERENCE_DATE.plus({ weeks: 1 });
      const planning = await prisma.planning.create({
        data: {
          houseId,
          startDate: nextWeek.startOf("week").toJSDate(),
          finishDate: nextWeek.endOf("week").startOf("day").toJSDate(),
        },
      });
      await prisma.planningItem.create({
        data: {
          planningId: planning.id,
          weekDay: "mardi",
          meal: "dejeuner",
          recipeId: recipe.id,
          portions: 2,
        },
      });

      const nextWeekRes = await agent.get("/shopping-list").query({ date: isoDate(nextWeek) });
      expect(nextWeekRes.body.items).to.have.length(1);

      const thisWeekRes = await agent.get("/shopping-list").query({ date: today() });
      expect(thisWeekRes.body.items).to.deep.equal([]);
    });
  });
});
