import { expect } from "chai";
import {
  RecipeSourceFetchError,
  RecipeSourceParseError,
} from "../../src/lib/recipe-sources/recipe-source-errors.js";
import { mangerBougerAdapter } from "../../src/sources/manger-bouger.js";

/** Stubs `globalThis.fetch` to return `html` as the response body — same pattern as every other adapter test in this family. */
function stubFetchHtml(html: string, status = 200) {
  globalThis.fetch = (async () => new Response(html, { status })) as typeof fetch;
}

const DETAIL_URL =
  "https://www.mangerbouger.fr/manger-mieux/la-fabrique-a-menus/recettes/2854-salade-de-pates-aux-courgettes";

/** Wraps a `props.initialState.recipes` payload (the shape `list()` reads) in a minimal `__NEXT_DATA__` script tag, the same server-rendered hydration data every mangerbouger.fr Next.js page carries. */
function htmlWithListNextData(recipesState: unknown): string {
  const payload = { props: { initialState: { recipes: recipesState } } };
  return `<!doctype html><html><head></head><body><script id="__NEXT_DATA__" type="application/json">${JSON.stringify(
    payload,
  )}</script></body></html>`;
}

/**
 * A real Slate.js rich-text document (paragraph + bulleted-list of
 * list-items, the only block types ever observed live), JSON-stringified —
 * exactly the shape mangerbouger.fr's own JSON-LD embeds as a `HowToStep`'s
 * `text` field.
 */
const SLATE_STEP_DOCUMENT = JSON.stringify([
  { type: "paragraph", children: [{ text: "Cuisson des courgettes", bold: true }] },
  {
    type: "bulleted-list",
    children: [
      { type: "list-item", children: [{ text: "Épluchez les courgettes" }] },
      { type: "list-item", children: [{ text: "Coupez-les en rondelles" }] },
    ],
  },
]);

/** A JSON-LD `Recipe` payload shaped exactly like a real mangerbouger.fr detail page's — no `recipeYield` (verified absent live on every sampled recipe), `recipeInstructions` holding {@link SLATE_STEP_DOCUMENT} instead of prose. */
const RECIPE_JSON_LD_NO_YIELD = {
  "@context": "https://schema.org",
  "@type": "Recipe",
  name: "Salade de pâtes aux courgettes",
  image: "https://api-prod-fam.mangerbouger.fr/storage/recettes/salade.jpg",
  recipeIngredient: ["3 Courgette", "4 cuillères à soupe Huile d'olive"],
  recipeInstructions: [{ "@type": "HowToStep", name: "Étape 1", text: SLATE_STEP_DOCUMENT }],
  url: DETAIL_URL,
};

/** Wraps a JSON-LD `Recipe` payload (already an object, not yet stringified) and, optionally, a `__NEXT_DATA__` detail-page payload carrying `portions`, in one minimal HTML page — the two independent script tags `parse()` reads. */
function htmlWithDetail(recipeJsonLd: unknown, portions?: number): string {
  const nextData = portions === undefined ? "" : htmlWithDetailNextDataScript(portions);
  return `<!doctype html><html><head><script type="application/ld+json">${JSON.stringify(
    recipeJsonLd,
  )}</script></head><body>${nextData}</body></html>`;
}

function htmlWithDetailNextDataScript(portions: number): string {
  const payload = { props: { initialState: { recipe: { recipe: { portions } } } } };
  return `<script id="__NEXT_DATA__" type="application/json">${JSON.stringify(payload)}</script>`;
}

describe("mangerBougerAdapter", () => {
  let originalFetch: typeof fetch;

  beforeEach(() => {
    originalFetch = globalThis.fetch;
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  it("declares itself as an unofficial, French-locale source with an icon", () => {
    expect(mangerBougerAdapter.key).to.equal("mangerBouger");
    expect(mangerBougerAdapter.name).to.equal("Manger Bouger");
    expect(mangerBougerAdapter.official).to.equal(false);
    expect(mangerBougerAdapter.iconUrl).to.be.a("string");
    expect(mangerBougerAdapter.locale).to.equal("fr");
  });

  describe("list", () => {
    it("maps __NEXT_DATA__'s recipes.list into RecipeSourceListItems", async () => {
      stubFetchHtml(
        htmlWithListNextData({
          list: [
            {
              id: "2854",
              slug: "2854-salade-de-pates-aux-courgettes",
              name: "Salade de pâtes aux courgettes",
              image: "https://api-prod-fam.mangerbouger.fr/storage/recettes/salade.jpg",
            },
          ],
          hasMorePages: false,
        }),
      );

      const result = await mangerBougerAdapter.list({ query: "salade" });

      expect(result.items).to.deep.equal([
        {
          externalId: DETAIL_URL,
          title: "Salade de pâtes aux courgettes",
          picture: "https://api-prod-fam.mangerbouger.fr/storage/recettes/salade.jpg",
          url: DETAIL_URL,
        },
      ]);
    });

    it("offers a next page when hasMorePages is true, and none when false", async () => {
      stubFetchHtml(htmlWithListNextData({ list: [], hasMorePages: true }));
      expect((await mangerBougerAdapter.list({ query: "x" })).nextCursor).to.equal("2");

      stubFetchHtml(htmlWithListNextData({ list: [], hasMorePages: false }));
      expect((await mangerBougerAdapter.list({ query: "x" })).nextCursor).to.be.null;
    });

    it("requests the given cursor's page and URL-encodes the query", async () => {
      let requestedUrl: string | undefined;
      globalThis.fetch = (async (url: string) => {
        requestedUrl = url;
        return new Response(htmlWithListNextData({ list: [], hasMorePages: false }), {
          status: 200,
        });
      }) as typeof fetch;

      await mangerBougerAdapter.list({ query: "crème brûlée", cursor: "3" });

      expect(requestedUrl).to.include("page=3");
      expect(requestedUrl).to.include("query=cr%C3%A8me%20br%C3%BBl%C3%A9e");
    });

    it("skips a list entry missing a slug or a name", async () => {
      stubFetchHtml(
        htmlWithListNextData({
          list: [
            { id: "1", name: "No slug", image: null },
            { id: "2", slug: "no-name", image: null },
          ],
          hasMorePages: false,
        }),
      );

      const result = await mangerBougerAdapter.list({ query: "x" });

      expect(result.items).to.deep.equal([]);
    });

    it("returns an empty page rather than throwing when the page has no __NEXT_DATA__ at all", async () => {
      stubFetchHtml("<!doctype html><html><body>Rien ici</body></html>");

      const result = await mangerBougerAdapter.list({ query: "x" });

      expect(result.items).to.deep.equal([]);
      expect(result.nextCursor).to.be.null;
    });

    it("throws RecipeSourceFetchError on a non-2xx response", async () => {
      stubFetchHtml("", 500);

      try {
        await mangerBougerAdapter.list({ query: "x" });
        expect.fail("expected list to throw");
      } catch (err) {
        expect(err).to.be.instanceOf(RecipeSourceFetchError);
        expect((err as RecipeSourceFetchError).sourceKey).to.equal("mangerBouger");
      }
    });

    it("throws RecipeSourceFetchError when the network request itself fails", async () => {
      globalThis.fetch = (async () => {
        throw new Error("network down");
      }) as typeof fetch;

      try {
        await mangerBougerAdapter.list({ query: "x" });
        expect.fail("expected list to throw");
      } catch (err) {
        expect(err).to.be.instanceOf(RecipeSourceFetchError);
        expect((err as RecipeSourceFetchError).cause).to.be.instanceOf(Error);
      }
    });
  });

  describe("fetchDetail", () => {
    it("fetches the given recipe URL and returns its html alongside the url", async () => {
      stubFetchHtml(htmlWithDetail(RECIPE_JSON_LD_NO_YIELD));

      const result = await mangerBougerAdapter.fetchDetail(DETAIL_URL);

      expect(result.url).to.equal(DETAIL_URL);
      expect(result.html).to.include("Salade de p");
    });

    it("throws a RecipeSourceFetchError keyed to mangerBouger, not the underlying generic adapter", async () => {
      stubFetchHtml("", 404);

      try {
        await mangerBougerAdapter.fetchDetail(DETAIL_URL);
        expect.fail("expected fetchDetail to throw");
      } catch (err) {
        expect(err).to.be.instanceOf(RecipeSourceFetchError);
        expect((err as RecipeSourceFetchError).sourceKey).to.equal("mangerBouger");
      }
    });
  });

  describe("parse", () => {
    it("flattens a Slate.js rich-text step into readable plain text", () => {
      const parsed = mangerBougerAdapter.parse({
        html: htmlWithDetail(RECIPE_JSON_LD_NO_YIELD),
        url: DETAIL_URL,
      });

      expect(parsed.steps).to.deep.equal([
        {
          description:
            "Cuisson des courgettes\n- Épluchez les courgettes\n- Coupez-les en rondelles",
          picture: null,
        },
      ]);
    });

    it("backfills recipeYield/portions from __NEXT_DATA__ when the JSON-LD itself doesn't state one", () => {
      const parsed = mangerBougerAdapter.parse({
        html: htmlWithDetail(RECIPE_JSON_LD_NO_YIELD, 4),
        url: DETAIL_URL,
      });

      expect(parsed.portions).to.equal(4);
    });

    it("leaves portions null when __NEXT_DATA__ has no portions to backfill from either", () => {
      const parsed = mangerBougerAdapter.parse({
        html: htmlWithDetail(RECIPE_JSON_LD_NO_YIELD),
        url: DETAIL_URL,
      });

      expect(parsed.portions).to.be.null;
    });

    it("doesn't override recipeYield when the JSON-LD already states one", () => {
      const parsed = mangerBougerAdapter.parse({
        html: htmlWithDetail({ ...RECIPE_JSON_LD_NO_YIELD, recipeYield: 8 }, 4),
        url: DETAIL_URL,
      });

      expect(parsed.portions).to.equal(8);
    });

    it("leaves an already-plain-text step untouched rather than mangling it", () => {
      const parsed = mangerBougerAdapter.parse({
        html: htmlWithDetail({
          ...RECIPE_JSON_LD_NO_YIELD,
          recipeInstructions: [{ "@type": "HowToStep", text: "Faites bouillir de l'eau." }],
        }),
        url: DETAIL_URL,
      });

      expect(parsed.steps).to.deep.equal([
        { description: "Faites bouillir de l'eau.", picture: null },
      ]);
    });

    it("maps name/image/ingredients end to end via the underlying generic adapter", () => {
      const parsed = mangerBougerAdapter.parse({
        html: htmlWithDetail(RECIPE_JSON_LD_NO_YIELD),
        url: DETAIL_URL,
      });

      expect(parsed.name).to.equal("Salade de pâtes aux courgettes");
      expect(parsed.picture).to.equal(
        "https://api-prod-fam.mangerbouger.fr/storage/recettes/salade.jpg",
      );
      expect(parsed.sourceUrl).to.equal(DETAIL_URL);
      expect(parsed.ingredients).to.deep.equal([
        { rawText: "3 Courgette", quantity: null, unit: null, name: "3 Courgette" },
        {
          rawText: "4 cuillères à soupe Huile d'olive",
          quantity: null,
          unit: null,
          name: "4 cuillères à soupe Huile d'olive",
        },
      ]);
    });

    it("throws a RecipeSourceParseError keyed to mangerBouger, not the underlying generic adapter", () => {
      const html = "<!doctype html><html><body>Pas de JSON-LD ici</body></html>";

      try {
        mangerBougerAdapter.parse({ html, url: DETAIL_URL });
        expect.fail("expected parse to throw");
      } catch (err) {
        expect(err).to.be.instanceOf(RecipeSourceParseError);
        expect((err as RecipeSourceParseError).sourceKey).to.equal("mangerBouger");
      }
    });
  });
});
