import { ErrorCode } from "@batch-cooking/shared";
import { expect } from "chai";
import request from "supertest";
import { createApp } from "../../src/app.js";
import { env } from "../../src/config/env.js";
import { prisma } from "../../src/db/prisma.js";
import { resetDatabase } from "../../test-support/reset-db.js";

const SECRET_HEADER = "X-Internal-Worker-Secret";

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

/** A minimal author + recipe + step fixture — these routes have no notion of a session/viewer, so nothing here needs to go through `/auth/signup` the way `recipe.test.ts`'s fixtures do. */
async function createRecipeWithStep(
  description = "Faire mijoter la sauce.",
): Promise<{ stepId: number; recipeId: number }> {
  const author = await prisma.userProfile.create({
    data: {
      firstName: "Test",
      lastName: "Author",
      email: `${crypto.randomUUID()}@example.test`,
      passwordHash: "not-a-real-hash",
    },
  });
  const recipe = await prisma.recipe.create({
    data: {
      name: "Recette",
      authorId: author.id,
      portions: 4,
      steps: { create: [{ description, order: 0 }] },
    },
    include: { steps: true },
  });
  const step = recipe.steps[0];
  if (!step) throw new Error("expected the fixture recipe to have one step");
  return { stepId: step.id, recipeId: recipe.id };
}

describe("Internal tech-step worker routes", () => {
  const app = createApp();

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

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

  describe("requireInternalWorker", () => {
    it("rejects a request with no secret header with 401 NOT_AUTHENTICATED", async () => {
      const res = await request(app).get("/internal/tech-steps/audit-batch");

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

    it("rejects a request with the wrong secret with 401 NOT_AUTHENTICATED", async () => {
      const res = await request(app)
        .get("/internal/tech-steps/audit-batch")
        .set(SECRET_HEADER, "definitely-not-the-right-secret");

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

    it("rejects even the correct secret with 401 NOT_AUTHENTICATED on a plain user-facing route (no bypass of requireAuth)", async () => {
      const res = await request(app)
        .get("/recipes")
        .query({ tab: "publique" })
        .set(SECRET_HEADER, env.INTERNAL_WORKER_SECRET ?? "irrelevant-unset-in-this-env");

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

  // Every test below needs a real configured secret to exercise the success
  // path — skipped (not failed) in an environment that hasn't set one, same
  // "optional, but the surface fails closed without it" posture
  // `INTERNAL_WORKER_SECRET` itself has (see config/env.ts). Both this
  // repo's `.env.test.example` and `.github/workflows/ci.yml` set one, so
  // this only actually skips in an environment that deliberately diverges
  // from both.
  describe("with a configured secret", () => {
    before(function skipWithoutConfiguredSecret() {
      if (env.INTERNAL_WORKER_SECRET === undefined) {
        // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed without @types/mocha (not a dependency here) — same untyped-`this` shape a plain JS mocha callback would have.
        (this as any).skip();
      }
    });

    function withSecret(req: request.Test): request.Test {
      return req.set(SECRET_HEADER, env.INTERNAL_WORKER_SECRET as string);
    }

    describe("GET /internal/tech-steps/audit-batch", () => {
      // A true low-confidence positive case can't be asserted here without
      // a live trained classifier to verify the exact sentence against
      // first — same limitation `tech-step-eval-dataset.ts` documents for
      // the same reason (no local Postgres was reachable in the session
      // that introduced this file). This test instead covers the
      // deterministic negative: a step the classifier confidently resolves
      // (proven by `tech-step-matcher.test.ts`'s own identical-sentence
      // case) must produce zero audit entries — nothing here should ever
      // flag a confident match as worth a second opinion.
      it("finds nothing to audit in a step the classifier confidently resolves", async () => {
        await createRecipeWithStep("Faire mijoter à feu doux");

        const res = await withSecret(
          request(app).get("/internal/tech-steps/audit-batch").query({ locale: "fr" }),
        );

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

      it("finds nothing to audit in a step naming no technique at all", async () => {
        await createRecipeWithStep("Ranger les couverts dans le tiroir");

        const res = await withSecret(
          request(app).get("/internal/tech-steps/audit-batch").query({ locale: "fr" }),
        );

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

      it("rejects a non-positive limit with 400 VALIDATION_ERROR", async () => {
        const res = await withSecret(
          request(app).get("/internal/tech-steps/audit-batch").query({ limit: 0 }),
        );

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

    describe("GET /internal/tech-steps/pending-corrections", () => {
      it("returns unconsumed corrections, oldest first, excluding already-consumed ones", async () => {
        const { stepId, recipeId } = await createRecipeWithStep();
        const simmerId = await techStepId("simmer");
        const author = await prisma.recipe
          .findUniqueOrThrow({ where: { id: recipeId } })
          .then((recipe) => recipe.authorId);

        const older = await prisma.stepTechStepCorrection.create({
          data: { stepId, correctorId: author, start: 6, end: 13, correctedTechStepId: simmerId },
        });
        const consumed = await prisma.stepTechStepCorrection.create({
          data: {
            stepId,
            correctorId: author,
            start: 0,
            end: 5,
            correctedTechStepId: simmerId,
            consumedAt: new Date(),
          },
        });
        const newer = await prisma.stepTechStepCorrection.create({
          data: { stepId, correctorId: author, start: 6, end: 13, correctedTechStepId: simmerId },
        });

        const res = await withSecret(request(app).get("/internal/tech-steps/pending-corrections"));

        expect(res.status).to.equal(200);
        const ids = (res.body as Array<{ id: number }>).map((entry) => entry.id);
        expect(ids).to.deep.equal([older.id, newer.id]);
        expect(ids).to.not.include(consumed.id);
      });

      it("respects ?limit=", async () => {
        const { stepId, recipeId } = await createRecipeWithStep();
        const simmerId = await techStepId("simmer");
        const author = await prisma.recipe
          .findUniqueOrThrow({ where: { id: recipeId } })
          .then((recipe) => recipe.authorId);
        await prisma.stepTechStepCorrection.createMany({
          data: [
            { stepId, correctorId: author, start: 0, end: 5, correctedTechStepId: simmerId },
            { stepId, correctorId: author, start: 6, end: 13, correctedTechStepId: simmerId },
          ],
        });

        const res = await withSecret(
          request(app).get("/internal/tech-steps/pending-corrections").query({ limit: 1 }),
        );

        expect(res.status).to.equal(200);
        expect(res.body).to.have.length(1);
      });
    });

    describe("POST /internal/tech-steps/training-suggestions", () => {
      it("creates a suggestion and marks its source correction consumed", async () => {
        const { stepId, recipeId } = await createRecipeWithStep();
        const simmerId = await techStepId("simmer");
        const author = await prisma.recipe
          .findUniqueOrThrow({ where: { id: recipeId } })
          .then((recipe) => recipe.authorId);
        const correction = await prisma.stepTechStepCorrection.create({
          data: { stepId, correctorId: author, start: 6, end: 13, correctedTechStepId: simmerId },
        });

        const res = await withSecret(
          request(app)
            .post("/internal/tech-steps/training-suggestions")
            .send({
              suggestions: [
                {
                  techStepKey: "simmer",
                  locale: "fr",
                  suggestedSynonyms: ["frémissonner"],
                  suggestedUtterances: ["laisser frémissonner à feu très doux"],
                  sourceType: "correction",
                  sourceCorrectionId: correction.id,
                },
              ],
            }),
        );

        expect(res.status).to.equal(201);
        expect(res.body).to.deep.equal({ created: 1 });

        const suggestions = await prisma.techStepTrainingSuggestion.findMany({
          where: { techStepId: simmerId },
        });
        expect(suggestions).to.have.length(1);
        expect(suggestions[0]?.sourceCorrectionId).to.equal(correction.id);
        expect(suggestions[0]?.status).to.equal("pending");

        const updatedCorrection = await prisma.stepTechStepCorrection.findUniqueOrThrow({
          where: { id: correction.id },
        });
        expect(updatedCorrection.consumedAt).to.not.equal(null);
      });

      it("accepts an llm_audit suggestion with no sourceCorrectionId", async () => {
        const res = await withSecret(
          request(app)
            .post("/internal/tech-steps/training-suggestions")
            .send({
              suggestions: [
                {
                  techStepKey: "boil",
                  locale: "fr",
                  suggestedSynonyms: ["bouillonner"],
                  suggestedUtterances: [],
                  sourceType: "llm_audit",
                },
              ],
            }),
        );

        expect(res.status).to.equal(201);
        expect(res.body).to.deep.equal({ created: 1 });
      });

      it("rejects sourceType 'correction' with no sourceCorrectionId with 400 VALIDATION_ERROR", async () => {
        const res = await withSecret(
          request(app)
            .post("/internal/tech-steps/training-suggestions")
            .send({
              suggestions: [
                {
                  techStepKey: "boil",
                  locale: "fr",
                  suggestedSynonyms: ["bouillonner"],
                  suggestedUtterances: [],
                  sourceType: "correction",
                },
              ],
            }),
        );

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

      it("rejects an unknown techStepKey with 404 TECH_STEP_NOT_FOUND", async () => {
        const res = await withSecret(
          request(app)
            .post("/internal/tech-steps/training-suggestions")
            .send({
              suggestions: [
                {
                  techStepKey: "not-a-real-tech-step",
                  locale: "fr",
                  suggestedSynonyms: [],
                  suggestedUtterances: [],
                  sourceType: "llm_audit",
                },
              ],
            }),
        );

        expect(res.status).to.equal(404);
        expect(res.body.code).to.equal(ErrorCode.TECH_STEP_NOT_FOUND);
      });
    });
  });
});
