import type { SignupInput } from "@batch-cooking/shared";
import { ErrorCode } 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 { resetDatabase } from "../test-support/reset-db.js";

/** See `auth.test.ts` — generated, never a real-looking person. */
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 }),
  };
}

/** Resolves a reference unit's id by its seed uid (also its DB `key`). */
async function unitId(key: string): Promise<number> {
  return (await prisma.unit.findFirstOrThrow({ where: { key } })).id;
}

/** Resolves a reference ingredient's id by its seed uid. */
async function ingredientId(key: string): Promise<number> {
  return (await prisma.ingredient.findFirstOrThrow({ where: { key } })).id;
}

/**
 * The off-catalog ("placeholder") ingredient escape hatch on `POST /recipes`
 * / `PATCH /recipes/:id` — a line with `placeholderName` instead of
 * `ingredientId` creates a dedicated `Ingredient` row
 * (`isPlaceholder: true`) so the recipe still saves, and that row is
 * surfaced (with its `displayName`) inside the recipe but never in the
 * browsable catalog.
 */
describe("Recipes — off-catalog placeholder ingredients", () => {
  const app = createApp();

  async function signup(): Promise<{ agent: ReturnType<typeof request.agent>; profileId: number }> {
    const agent = request.agent(app);
    const res = await agent.post("/auth/signup").send(buildSignupPayload());
    return { agent, profileId: res.body.id };
  }

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

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

  it("creates a placeholder Ingredient row for a `placeholderName` line and returns it inside the recipe", async () => {
    const { agent, profileId } = await signup();
    const piece = await unitId("piece");
    const tomato = await ingredientId("tomato");

    const res = await agent.post("/recipes").send({
      name: "Poulet basquaise",
      portions: 4,
      dietIds: [],
      ingredients: [
        { ingredientId: tomato, quantity: 3, unitId: piece },
        { placeholderName: "Piment d'Espelette", quantity: 1, unitId: piece },
      ],
      steps: [{ description: "Tout mélanger" }],
    });

    expect(res.status).to.equal(201);
    expect(res.body.ingredients).to.have.length(2);

    const placeholderLine = res.body.ingredients.find(
      (line: { ingredient: { isPlaceholder: boolean } }) => line.ingredient.isPlaceholder,
    );
    expect(placeholderLine, "a placeholder line is present").to.not.equal(undefined);
    expect(placeholderLine.ingredient.displayName).to.equal("Piment d'Espelette");
    expect(placeholderLine.ingredient.key).to.match(/^placeholder:/);
    expect(placeholderLine.ingredient.allergens).to.deep.equal([]);
    expect(placeholderLine.quantity).to.equal(1);

    const row = await prisma.ingredient.findUniqueOrThrow({
      where: { id: placeholderLine.ingredient.id },
    });
    expect(row.isPlaceholder).to.equal(true);
    expect(row.displayName).to.equal("Piment d'Espelette");
    expect(row.createdById).to.equal(profileId);
    expect(row.createdAt).to.be.an.instanceOf(Date);
    expect(row.reviewedAt).to.equal(null);
  });

  it("never lists placeholder rows in GET /reference/ingredients", async () => {
    const { agent } = await signup();
    const piece = await unitId("piece");

    await agent.post("/recipes").send({
      name: "Test",
      portions: 2,
      dietIds: [],
      ingredients: [{ placeholderName: "Feuille de combava", quantity: 1, unitId: piece }],
      steps: [{ description: "x" }],
    });

    const reference = await agent.get("/reference/ingredients");
    expect(reference.status).to.equal(200);
    expect(
      reference.body.some(
        (i: { isPlaceholder?: boolean; displayName?: string }) =>
          i.isPlaceholder === true || i.displayName === "Feuille de combava",
      ),
    ).to.equal(false);
  });

  it("reuses the existing placeholder row on edit (no duplicate) and drops it when the line is removed", async () => {
    const { agent } = await signup();
    const piece = await unitId("piece");
    const tomato = await ingredientId("tomato");

    const created = await agent.post("/recipes").send({
      name: "Édition",
      portions: 2,
      dietIds: [],
      ingredients: [{ placeholderName: "Sumac", quantity: 1, unitId: piece }],
      steps: [{ description: "x" }],
    });
    const placeholderId = created.body.ingredients[0].ingredient.id;

    // Re-submit the same recipe, keeping the placeholder line by its real id.
    const edited = await agent.patch(`/recipes/${created.body.id}`).send({
      name: "Édition",
      portions: 2,
      dietIds: [],
      ingredients: [
        { ingredientId: placeholderId, quantity: 2, unitId: piece },
        { ingredientId: tomato, quantity: 1, unitId: piece },
      ],
      steps: [{ description: "x" }],
    });
    expect(edited.status).to.equal(200);
    expect(await prisma.ingredient.count({ where: { isPlaceholder: true } })).to.equal(1);

    // Now edit again, dropping the placeholder line entirely.
    await agent.patch(`/recipes/${created.body.id}`).send({
      name: "Édition",
      portions: 2,
      dietIds: [],
      ingredients: [{ ingredientId: tomato, quantity: 1, unitId: piece }],
      steps: [{ description: "x" }],
    });
    // The row is now an orphan (kept on purpose — the admin catalog view
    // prunes it), but no *new* placeholder was created.
    expect(await prisma.ingredient.count({ where: { isPlaceholder: true } })).to.equal(1);
  });

  it("rejects a line carrying both ingredientId and placeholderName with 400", async () => {
    const { agent } = await signup();
    const piece = await unitId("piece");
    const tomato = await ingredientId("tomato");

    const res = await agent.post("/recipes").send({
      name: "Invalide",
      portions: 2,
      dietIds: [],
      ingredients: [
        { ingredientId: tomato, placeholderName: "Tomate", quantity: 1, unitId: piece },
      ],
      steps: [{ description: "x" }],
    });

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

  it("allows two placeholder lines with the same text (each becomes its own row)", async () => {
    const { agent } = await signup();
    const piece = await unitId("piece");

    const res = await agent.post("/recipes").send({
      name: "Doublons libres",
      portions: 2,
      dietIds: [],
      ingredients: [
        { placeholderName: "Herbes de garrigue", quantity: 1, unitId: piece },
        { placeholderName: "Herbes de garrigue", quantity: 2, unitId: piece },
      ],
      steps: [{ description: "x" }],
    });

    expect(res.status).to.equal(201);
    expect(res.body.ingredients).to.have.length(2);
    expect(await prisma.ingredient.count({ where: { isPlaceholder: true } })).to.equal(2);
  });
});
