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 { hashAdminPassword } from "../src/modules/admin/admin-auth.service.js";
import { resetDatabase } from "../test-support/reset-db.js";

async function seedAdmin(): Promise<{ email: string; password: string }> {
  const email = faker.internet.email().toLowerCase();
  const password = faker.internet.password({ length: 16 });
  await prisma.adminUser.create({
    data: { email, name: faker.person.fullName(), passwordHash: await hashAdminPassword(password) },
  });
  return { email, password };
}

/** A recipe with one placeholder ingredient line whose `displayName` is `name`. Returns the placeholder `Ingredient` id. */
async function seedPlaceholderRecipe(name: string): Promise<number> {
  const author = await prisma.userProfile.create({
    data: {
      firstName: "T",
      lastName: "A",
      email: `${faker.string.uuid()}@example.test`,
      passwordHash: "x",
    },
  });
  const unit = await prisma.unit.findFirstOrThrow({ where: { key: "piece" } });
  const placeholder = await prisma.ingredient.create({
    data: {
      key: `placeholder:${faker.string.uuid()}`,
      isPlaceholder: true,
      displayName: name,
      createdById: author.id,
      createdAt: new Date(),
    },
  });
  await prisma.recipe.create({
    data: {
      name: faker.lorem.words(3),
      authorId: author.id,
      portions: 2,
      ingredients: { create: [{ ingredientId: placeholder.id, quantity: 1, unitId: unit.id }] },
    },
  });
  return placeholder.id;
}

/**
 * `/admin/catalog/*` — the off-catalog ingredient review. Every route is
 * behind `requireAdmin`; the list groups placeholder rows by normalized
 * name, `mark-reviewed` stamps `reviewedAt`, `prune-orphans` deletes rows
 * no recipe references any more.
 */
describe("Admin catalog — off-catalog ingredients", () => {
  const app = createApp();

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

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

  async function adminAgent() {
    const { email, password } = await seedAdmin();
    const agent = request.agent(app);
    await agent.post("/admin/auth/login").send({ email, password });
    return agent;
  }

  it("rejects every route without an admin session", async () => {
    const get = await request(app).get("/admin/catalog/placeholders");
    expect(get.status).to.equal(401);
    const patch = await request(app)
      .patch("/admin/catalog/placeholders/mark-reviewed")
      .send({ ingredientIds: [1] });
    expect(patch.status).to.equal(401);
    const post = await request(app).post("/admin/catalog/placeholders/prune-orphans");
    expect(post.status).to.equal(401);
  });

  it("groups two spellings of the same missing ingredient into one row", async () => {
    await seedPlaceholderRecipe("Piment d'Espelette");
    await seedPlaceholderRecipe("piment d espelette");
    await seedPlaceholderRecipe("Sumac");

    const agent = await adminAgent();
    const res = await agent.get("/admin/catalog/placeholders");
    expect(res.status).to.equal(200);
    expect(res.body).to.have.length(2);

    const espelette = res.body.find(
      (g: { normalizedName: string }) => g.normalizedName === "piment d espelette",
    );
    expect(espelette.recipeCount).to.equal(2);
    expect(espelette.ingredientIds).to.have.length(2);
    expect(espelette.displayNames).to.have.members(["Piment d'Espelette", "piment d espelette"]);
    // Impact-ordered: the 2-recipe gap before the 1-recipe one.
    expect(res.body[0].normalizedName).to.equal("piment d espelette");
  });

  it("mark-reviewed stamps reviewedAt and moves the group out of the default list", async () => {
    const id = await seedPlaceholderRecipe("Galanga");
    const agent = await adminAgent();

    const patched = await agent
      .patch("/admin/catalog/placeholders/mark-reviewed")
      .send({ ingredientIds: [id] });
    expect(patched.status).to.equal(200);
    expect(patched.body.reviewed).to.equal(1);
    expect(
      (await prisma.ingredient.findUniqueOrThrow({ where: { id } })).reviewedAt,
    ).to.be.an.instanceOf(Date);

    const pending = await agent.get("/admin/catalog/placeholders");
    expect(pending.body).to.have.length(0);
    const reviewed = await agent.get("/admin/catalog/placeholders").query({ reviewed: "true" });
    expect(reviewed.body).to.have.length(1);
    expect(reviewed.body[0].allReviewed).to.equal(true);
  });

  it("prune-orphans deletes only placeholder rows with no recipe left", async () => {
    await seedPlaceholderRecipe("Encore utilisé");
    await prisma.ingredient.create({
      data: {
        key: `placeholder:${faker.string.uuid()}`,
        isPlaceholder: true,
        displayName: "Orphelin",
        createdAt: new Date(),
      },
    });

    const agent = await adminAgent();
    const res = await agent.post("/admin/catalog/placeholders/prune-orphans");
    expect(res.status).to.equal(200);
    expect(res.body.deleted).to.equal(1);
    expect(await prisma.ingredient.count({ where: { isPlaceholder: true } })).to.equal(1);
  });
});
