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

/** Stubs `globalThis.fetch` to return `html` as the response body — same pattern as `json-ld-recipe.test.ts`'s `stubFetchHtml`. */
function stubFetchHtml(html: string, status = 200) {
  globalThis.fetch = (async () => new Response(html, { status })) as typeof fetch;
}

/**
 * Wraps a schema.org `ItemList` payload (already an object, not yet
 * stringified) in a minimal HTML page carrying it as one
 * `<script type="application/ld+json">` block — the shape marmiton.org's
 * search-results page embeds `list()` reads.
 */
function htmlWithItemListJsonLd(itemListElement: unknown[]): string {
  const payload = {
    "@context": "https://schema.org",
    "@graph": [
      { "@type": "WebSite", name: "Marmiton" },
      {
        "@type": "ItemList",
        "@id": "https://www.marmiton.org/recettes/recherche.aspx?aqt=poulet#itemlist",
        numberOfItems: itemListElement.length,
        itemListElement,
      },
    ],
  };
  return `<!doctype html><html><head><script type="application/ld+json">${JSON.stringify(
    payload,
  )}</script></head><body></body></html>`;
}

const RECIPE_URL = "https://www.marmiton.org/recettes/recette_tarte-aux-pommes_11457.aspx";

const baseListItem = {
  "@type": "ListItem",
  position: 1,
  url: RECIPE_URL,
  name: "Tarte aux pommes",
  image: "https://assets.afcdn.com/recipe/tarte.jpg",
};

const baseRecipeJsonLd = {
  "@context": "https://schema.org",
  "@type": "Recipe",
  name: "Tarte aux pommes",
  description: "Une tarte aux pommes classique.",
  image: "https://assets.afcdn.com/recipe/tarte.jpg",
  recipeYield: "6 personnes",
  recipeIngredient: ["3 pommes", "1 pâte brisée"],
  recipeInstructions: [
    { "@type": "HowToStep", text: "Épluchez les pommes." },
    { "@type": "HowToStep", text: "Enfournez 30 minutes." },
  ],
};

function htmlWithRecipeJsonLd(): string {
  return `<!doctype html><html><head><script type="application/ld+json">${JSON.stringify(
    baseRecipeJsonLd,
  )}</script></head><body></body></html>`;
}

describe("marmitonAdapter", () => {
  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(marmitonAdapter.key).to.equal("marmiton");
    expect(marmitonAdapter.name).to.equal("Marmiton");
    expect(marmitonAdapter.official).to.equal(false);
    expect(marmitonAdapter.iconUrl).to.be.a("string");
    expect(marmitonAdapter.locale).to.equal("fr");
  });

  describe("list", () => {
    it("maps the search page's ItemList into RecipeSourceListItems and offers a next page", async () => {
      stubFetchHtml(htmlWithItemListJsonLd([baseListItem]));

      const result = await marmitonAdapter.list({ query: "tarte aux pommes" });

      expect(result.items).to.deep.equal([
        {
          externalId: RECIPE_URL,
          title: "Tarte aux pommes",
          picture: "https://assets.afcdn.com/recipe/tarte.jpg",
          url: RECIPE_URL,
        },
      ]);
      expect(result.nextCursor).to.equal("2");
    });

    it("requests the given cursor's page and stops offering a next page once a page comes back empty", async () => {
      let requestedUrl: string | undefined;
      globalThis.fetch = (async (url: string) => {
        requestedUrl = url;
        return new Response(htmlWithItemListJsonLd([]), { status: 200 });
      }) as typeof fetch;

      const result = await marmitonAdapter.list({ query: "tarte", cursor: "3" });

      expect(requestedUrl).to.include("page=3");
      expect(result.items).to.deep.equal([]);
      expect(result.nextCursor).to.be.null;
    });

    it("treats a 404 (page past the last one) as an empty final page, not a failure", async () => {
      stubFetchHtml("", 404);

      const result = await marmitonAdapter.list({ query: "tarte", cursor: "999" });

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

    it("skips a ListItem missing a url or a name", async () => {
      stubFetchHtml(
        htmlWithItemListJsonLd([
          { "@type": "ListItem", position: 1, name: "No url" },
          { "@type": "ListItem", position: 2, url: RECIPE_URL },
        ]),
      );

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

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

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

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

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

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

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

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

      try {
        await marmitonAdapter.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(htmlWithRecipeJsonLd());

      const result = await marmitonAdapter.fetchDetail(RECIPE_URL);

      expect(result.url).to.equal(RECIPE_URL);
      expect(result.html).to.include("Tarte aux pommes");
    });

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

      try {
        await marmitonAdapter.fetchDetail(RECIPE_URL);
        expect.fail("expected fetchDetail to throw");
      } catch (err) {
        expect(err).to.be.instanceOf(RecipeSourceFetchError);
        expect((err as RecipeSourceFetchError).sourceKey).to.equal("marmiton");
      }
    });
  });

  describe("parse", () => {
    it("delegates to the generic JSON-LD parser end to end", () => {
      const parsed = marmitonAdapter.parse({ html: htmlWithRecipeJsonLd(), url: RECIPE_URL });

      expect(parsed.name).to.equal("Tarte aux pommes");
      expect(parsed.portions).to.equal(6);
      expect(parsed.sourceUrl).to.equal(RECIPE_URL);
      expect(parsed.ingredients).to.deep.equal([
        { rawText: "3 pommes", quantity: null, unit: null, name: "3 pommes" },
        { rawText: "1 pâte brisée", quantity: null, unit: null, name: "1 pâte brisée" },
      ]);
      expect(parsed.steps).to.deep.equal([
        { description: "Épluchez les pommes.", picture: null },
        { description: "Enfournez 30 minutes.", picture: null },
      ]);
    });

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

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