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 }),
  };
}

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

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

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

describe("Cooking session", () => {
  const app = createApp();

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

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

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

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

    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("/cooking-session").query({ date: "not-a-date" });

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

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

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

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

    it("returns an empty plan when no planning covers that week", 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("/cooking-session").query({ date: today() });

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

    it("pools an identical prep step from two planned recipes into one merged-prep task", 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 pieceId = await unitId("piece");
      const chopId = await techStepId("chop");
      const simmerId = await techStepId("simmer");
      const mixId = await techStepId("mix");

      /**
       * A recipe: one pure-prep "chop onion" step, then (optionally) an
       * active "mix" step, then one simmer step. The `withActiveStep` recipe
       * is still doing hands-on work in the phase after its simmer starts, so
       * the other recipe's simmer floats into that phase as `background`.
       */
      async function makeRecipe(name: string, onionQty: number, withActiveStep = false) {
        const chopStep = {
          order: 0,
          description: "Émincer les oignons",
          techSteps: {
            create: [
              {
                techStepId: chopId,
                order: 0,
                ingredients: {
                  create: [
                    {
                      ingredientId: onionId,
                      quantity: onionQty,
                      unitId: pieceId,
                      start: 0,
                      end: 1,
                    },
                  ],
                },
              },
            ],
          },
        };
        const activeStep = {
          order: 1,
          description: "Mélanger l'appareil",
          techSteps: { create: [{ techStepId: mixId, order: 0 }] },
        };
        const simmerStep = {
          order: withActiveStep ? 2 : 1,
          description: "Faire mijoter",
          techSteps: { create: [{ techStepId: simmerId, order: 0 }] },
        };
        return prisma.recipe.create({
          data: {
            name,
            authorId,
            portions: 4,
            steps: {
              create: withActiveStep ? [chopStep, activeStep, simmerStep] : [chopStep, simmerStep],
            },
          },
        });
      }

      const soupe = await makeRecipe("Soupe", 2);
      const tarte = await makeRecipe("Tarte", 3, true);

      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: soupe.id,
            portions: 4,
          },
          {
            planningId: planning.id,
            weekDay: "mardi",
            meal: "diner",
            recipeId: tarte.id,
            portions: 4,
          },
        ],
      });

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

      expect(res.status).to.equal(200);
      expect(res.body.recipes.map((r: { name: string }) => r.name)).to.have.members([
        "Soupe",
        "Tarte",
      ]);

      const mise = res.body.phases[0];
      expect(mise.kind).to.equal("mise-en-place");
      const merged = mise.tasks.filter((t: { kind: string }) => t.kind === "merged-prep");
      expect(merged).to.have.length(1);
      expect(merged[0].technique.key).to.equal("chop");
      expect(merged[0].ingredients[0].ingredient.key).to.equal("onion");
      expect(merged[0].ingredients[0].quantity).to.equal(5);
      expect(merged[0].sourceRecipes).to.have.length(2);

      // The simmer steps land in a later phase, and one shows as background.
      const later = res.body.phases.slice(1);
      const backgrounds = later.flatMap((p: { background: unknown[] }) => p.background);
      expect(backgrounds.length).to.be.greaterThan(0);
    });
  });
});
