import type { StepTechStepTemperatureView, StepTechStepView } from "@batch-cooking/shared";
import {
  splitDescriptionByTechSteps,
  splitDescriptionSegments,
} from "../../src/features/recipes/steps/highlight-tech-steps";

// Pure logic, no DOM/mount needed — reuses the component-test runner
// (Cypress's Mocha/Chai, same as CheckboxOption.cy.tsx) purely for its
// `expect`, not for rendering. `.cy.tsx` (not `.cy.ts`) only because that's
// what `cypress.config.ts`'s component `specPattern` looks for.

/** Builds a `StepTechStepView` — `context` omitted entirely (not just undefined) when absent, matching what the API actually sends for an older, not-yet-recomputed match (see `StepTechStepView`'s own doc comment). `source` defaults to `"auto"`, the common case every test not specifically about the manual/auto distinction uses. */
function techStep(
  key: string,
  id: number,
  start: number,
  end: number,
  context?: { start: number; end: number },
  source: StepTechStepView["source"] = "auto",
): StepTechStepView {
  return {
    techStep: { id, key },
    start,
    end,
    source,
    ...(context ? { contextStart: context.start, contextEnd: context.end } : {}),
  };
}

describe("splitDescriptionByTechSteps", () => {
  it("returns the whole description as one plain segment when there are no matches", () => {
    expect(splitDescriptionByTechSteps("Servir immédiatement", [])).to.deep.equal([
      { text: "Servir immédiatement", techStep: null, isKeyword: false, source: null },
    ]);
  });

  it("splits a single keyword-only match (no context) into before/match/after segments", () => {
    // "Faire mijoter à feu doux" — "mijoter" is [6, 13). Same shape as
    // before context spans existed at all — the common case for a short,
    // already-imperative clause where the keyword and its context coincide.
    const result = splitDescriptionByTechSteps("Faire mijoter à feu doux", [
      techStep("simmer", 1, 6, 13),
    ]);
    expect(result).to.deep.equal([
      { text: "Faire ", techStep: null, isKeyword: false, source: null },
      { text: "mijoter", techStep: { id: 1, key: "simmer" }, isKeyword: true, source: "auto" },
      { text: " à feu doux", techStep: null, isKeyword: false, source: null },
    ]);
  });

  it("handles a match at the very start, with nothing before it", () => {
    const result = splitDescriptionByTechSteps("Hacher les oignons", [techStep("chop", 2, 0, 6)]);
    expect(result).to.deep.equal([
      { text: "Hacher", techStep: { id: 2, key: "chop" }, isKeyword: true, source: "auto" },
      { text: " les oignons", techStep: null, isKeyword: false, source: null },
    ]);
  });

  it("handles a match at the very end, with nothing after it", () => {
    const result = splitDescriptionByTechSteps("Faire cuire", [techStep("cook", 3, 6, 11)]);
    expect(result).to.deep.equal([
      { text: "Faire ", techStep: null, isKeyword: false, source: null },
      { text: "cuire", techStep: { id: 3, key: "cook" }, isKeyword: true, source: "auto" },
    ]);
  });

  it("handles several non-adjacent matches, preserving the plain text between them", () => {
    const text = "Préchauffer la poêle, puis faire fondre le beurre";
    const result = splitDescriptionByTechSteps(text, [
      techStep("preheat", 4, 0, 11),
      techStep("melt", 5, 27, 39),
    ]);
    expect(result.map((s) => s.text).join("")).to.equal(text);
    expect(result.filter((s) => s.techStep !== null)).to.have.length(2);
    expect(result[0]).to.deep.equal({
      text: "Préchauffer",
      techStep: { id: 4, key: "preheat" },
      isKeyword: true,
      source: "auto",
    });
  });

  it("re-sorts entries that aren't already in start order", () => {
    const text = "Faire fondre le beurre puis préchauffer le four";
    // Passed in techStepId order, not text order — the function must sort
    // by position, not trust the input order.
    const result = splitDescriptionByTechSteps(text, [
      techStep("preheat", 4, 28, 39),
      techStep("melt", 5, 0, 12),
    ]);
    const matches = result.filter((s) => s.techStep !== null && s.isKeyword);
    expect(matches.map((s) => s.techStep?.key)).to.deep.equal(["melt", "preheat"]);
  });

  it("drops a match whose end is past the end of the description", () => {
    const result = splitDescriptionByTechSteps("Cuire", [techStep("cook", 3, 0, 999)]);
    expect(result).to.deep.equal([
      { text: "Cuire", techStep: null, isKeyword: false, source: null },
    ]);
  });

  it("drops a match with a negative start", () => {
    const result = splitDescriptionByTechSteps("Cuire", [techStep("cook", 3, -1, 5)]);
    expect(result).to.deep.equal([
      { text: "Cuire", techStep: null, isKeyword: false, source: null },
    ]);
  });

  it("drops a match whose start isn't before its end", () => {
    const result = splitDescriptionByTechSteps("Cuire", [techStep("cook", 3, 3, 3)]);
    expect(result).to.deep.equal([
      { text: "Cuire", techStep: null, isKeyword: false, source: null },
    ]);
  });

  it("drops a later match that overlaps one already accepted", () => {
    // Two entries claiming overlapping ranges shouldn't happen in practice
    // (the backend already resolves overlaps), but the splitter defends
    // against it anyway rather than producing a garbled/duplicated slice.
    const result = splitDescriptionByTechSteps("Cuire au four", [
      techStep("bake", 3, 0, 13),
      techStep("cook", 2, 0, 5),
    ]);
    expect(result).to.deep.equal([
      { text: "Cuire au four", techStep: { id: 3, key: "bake" }, isKeyword: true, source: "auto" },
    ]);
  });

  it("returns a single empty-ish segment for an empty description with no matches", () => {
    expect(splitDescriptionByTechSteps("", [])).to.deep.equal([]);
  });

  it("carries a manual correction's source through its segments, distinct from an auto match", () => {
    const text = "Faire mijoter le riz, puis dresser dans les assiettes";
    const result = splitDescriptionByTechSteps(text, [
      techStep("simmer", 1, 6, 13),
      techStep("plate", 2, 28, 35, undefined, "manual"),
    ]);
    const keywordSegments = result.filter((s) => s.isKeyword);
    expect(keywordSegments.map((s) => ({ key: s.techStep?.key, source: s.source }))).to.deep.equal([
      { key: "simmer", source: "auto" },
      { key: "plate", source: "manual" },
    ]);
  });

  it("never lets one match's wider context swallow another match's own keyword span", () => {
    // The motivating real bug (found via live testing, not invented for
    // this test): "simmer" is the only NER candidate `splitIntoClauses`
    // found, so its context spans the *entire* description — before this
    // was fixed, that wide context advanced `cursor` past 39, silently
    // dropping "setAside"'s own keyword span (a manual correction on
    // "materiel", a word with no relation to "simmer" at all) instead of
    // rendering it.
    const text = "Faire mijoter la sauce, puis ranger le materiel.";
    const result = splitDescriptionByTechSteps(text, [
      techStep("simmer", 1, 6, 13, { start: 0, end: 48 }),
      techStep("setAside", 2, 39, 47, undefined, "manual"),
    ]);
    const keywordSegments = result.filter((s) => s.isKeyword);
    expect(
      keywordSegments.map((s) => ({ key: s.techStep?.key, text: s.text, source: s.source })),
    ).to.deep.equal([
      { key: "simmer", text: "mijoter", source: "auto" },
      { key: "setAside", text: "materiel", source: "manual" },
    ]);
    expect(result.map((s) => s.text).join("")).to.equal(text);
  });

  describe("with a context span wider than the keyword", () => {
    it("splits into context-before / keyword / context-after around a keyword in the middle of its clause", () => {
      // The motivating example: "Dans une poêle chaude, faire chauffer une
      // noix de beurre" — `preheat`'s keyword is "poêle chaude", its
      // context is the whole "Dans une poêle chaude" clause around it.
      const text = "Dans une poêle chaude, faire chauffer une noix de beurre";
      const result = splitDescriptionByTechSteps(text, [
        techStep("preheat", 4, 9, 21, { start: 0, end: 21 }),
      ]);
      expect(result).to.deep.equal([
        {
          text: "Dans une ",
          techStep: { id: 4, key: "preheat" },
          isKeyword: false,
          source: "auto",
        },
        {
          text: "poêle chaude",
          techStep: { id: 4, key: "preheat" },
          isKeyword: true,
          source: "auto",
        },
        {
          text: ", faire chauffer une noix de beurre",
          techStep: null,
          isKeyword: false,
          source: null,
        },
      ]);
    });

    it("omits the context-before segment when the keyword starts right at the context's own start", () => {
      const result = splitDescriptionByTechSteps("préchauffer le four", [
        techStep("preheat", 4, 0, 11, { start: 0, end: 19 }),
      ]);
      expect(result).to.deep.equal([
        {
          text: "préchauffer",
          techStep: { id: 4, key: "preheat" },
          isKeyword: true,
          source: "auto",
        },
        { text: " le four", techStep: { id: 4, key: "preheat" }, isKeyword: false, source: "auto" },
      ]);
    });

    it("omits the context-after segment when the keyword ends right at the context's own end", () => {
      const result = splitDescriptionByTechSteps("mettre le four à préchauffer", [
        techStep("preheat", 4, 17, 28, { start: 7, end: 28 }),
      ]);
      expect(result).to.deep.equal([
        { text: "mettre ", techStep: null, isKeyword: false, source: null },
        {
          text: "le four à ",
          techStep: { id: 4, key: "preheat" },
          isKeyword: false,
          source: "auto",
        },
        {
          text: "préchauffer",
          techStep: { id: 4, key: "preheat" },
          isKeyword: true,
          source: "auto",
        },
      ]);
    });

    it("falls back to a keyword-only segment when context is absent (an older, not-yet-recomputed match)", () => {
      const result = splitDescriptionByTechSteps("Faire mijoter à feu doux", [
        techStep("simmer", 1, 6, 13),
      ]);
      expect(result.some((s) => s.techStep !== null && !s.isKeyword)).to.equal(false);
    });

    it("drops an entry whose context doesn't actually contain its own keyword span", () => {
      const result = splitDescriptionByTechSteps("Cuire au four", [
        // contextEnd (5) is before the keyword's own end (13) — malformed.
        techStep("bake", 3, 0, 13, { start: 0, end: 5 }),
      ]);
      expect(result).to.deep.equal([
        { text: "Cuire au four", techStep: null, isKeyword: false, source: null },
      ]);
    });
  });
});

/** Builds a `StepTechStepTemperatureView` — only the fields a test cares about, the rest nulled. */
function temperature(
  start: number,
  end: number,
  fields: Partial<StepTechStepTemperatureView> = {},
): StepTechStepTemperatureView {
  return {
    start,
    end,
    celsius: null,
    gasMark: null,
    qualitative: null,
    raw: "",
    source: "auto",
    ...fields,
  };
}

describe("splitDescriptionSegments", () => {
  it("adds temperature: null to every segment when there are no temperatures", () => {
    const result = splitDescriptionSegments("Hacher les oignons", [techStep("chop", 1, 0, 6)], []);
    expect(result).to.deep.equal([
      {
        text: "Hacher",
        techStep: { id: 1, key: "chop" },
        isKeyword: true,
        source: "auto",
        temperature: null,
      },
      { text: " les oignons", techStep: null, isKeyword: false, source: null, temperature: null },
    ]);
  });

  it("splits a plain run around a temperature mention, keeping the technique keyword untouched", () => {
    const text = "Enfourner à 180°C environ 30 min";
    const temp = temperature(12, 17, { celsius: 180, raw: "180°C" });
    const result = splitDescriptionSegments(text, [techStep("bake", 2, 0, 9)], [temp]);
    expect(result).to.deep.equal([
      {
        text: "Enfourner",
        techStep: { id: 2, key: "bake" },
        isKeyword: true,
        source: "auto",
        temperature: null,
      },
      { text: " à ", techStep: null, isKeyword: false, source: null, temperature: null },
      { text: "180°C", techStep: null, isKeyword: false, source: null, temperature: temp },
      {
        text: " environ 30 min",
        techStep: null,
        isKeyword: false,
        source: null,
        temperature: null,
      },
    ]);
  });

  it("splits around two temperatures in one description", () => {
    const text = "Saisir à feu vif puis 180°C";
    const vif = temperature(9, 16, { qualitative: "high", raw: "feu vif" });
    const celsius = temperature(22, 27, { celsius: 180, raw: "180°C" });
    const result = splitDescriptionSegments(text, [], [vif, celsius]);
    expect(result.filter((segment) => segment.temperature !== null)).to.deep.equal([
      { text: "feu vif", techStep: null, isKeyword: false, source: null, temperature: vif },
      { text: "180°C", techStep: null, isKeyword: false, source: null, temperature: celsius },
    ]);
  });

  it("skips a temperature span that isn't fully inside a single non-keyword run", () => {
    // Span straddles the keyword — dropped rather than producing a garbled slice.
    const result = splitDescriptionSegments(
      "Cuire au four",
      [techStep("bake", 1, 0, 5)],
      [temperature(3, 10)],
    );
    expect(result.some((segment) => segment.temperature !== null)).to.equal(false);
  });
});
