import { describe, expect, test } from "bun:test";
import {
  discoverSitemapUrls,
  type FetchText,
  parseRobotsSitemaps,
  parseSitemapXml,
  SITEMAP_URL_CAP,
} from "./sitemap";

/** Construit un FetchText DI depuis une table URL → contenu (null si absent). */
function fetcher(map: Record<string, string | null>): FetchText {
  return async (url) => (url in map ? map[url] : null);
}

describe("parseRobotsSitemaps", () => {
  test("extrait plusieurs lignes Sitemap (insensible casse, espaces)", () => {
    const robots = [
      "User-agent: *",
      "Disallow: /admin",
      "Sitemap: https://ex.com/sitemap.xml",
      "sitemap:https://ex.com/news.xml",
      "  SITEMAP :  https://ex.com/img.xml  ",
    ].join("\n");
    expect(parseRobotsSitemaps(robots)).toEqual([
      "https://ex.com/sitemap.xml",
      "https://ex.com/news.xml",
      "https://ex.com/img.xml",
    ]);
  });

  test("aucune ligne Sitemap → []", () => {
    expect(parseRobotsSitemaps("User-agent: *\nDisallow:")).toEqual([]);
  });
});

describe("parseSitemapXml", () => {
  test("urlset → urls, pas de children", () => {
    const xml = `<?xml version="1.0"?>
      <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
        <url><loc>https://ex.com/a</loc></url>
        <url><loc> https://ex.com/b </loc><lastmod>2024</lastmod></url>
      </urlset>`;
    expect(parseSitemapXml(xml)).toEqual({
      urls: ["https://ex.com/a", "https://ex.com/b"],
      children: [],
    });
  });

  test("sitemapindex → children, pas d'urls", () => {
    const xml = `<sitemapindex>
      <sitemap><loc>https://ex.com/s1.xml</loc></sitemap>
      <sitemap><loc>https://ex.com/s2.xml</loc></sitemap>
    </sitemapindex>`;
    expect(parseSitemapXml(xml)).toEqual({
      urls: [],
      children: ["https://ex.com/s1.xml", "https://ex.com/s2.xml"],
    });
  });

  test("CDATA, namespaces préfixés et entités tolérés", () => {
    const xml = `<ns:urlset>
      <ns:url><ns:loc><![CDATA[https://ex.com/x?a=1&amp;b=2]]></ns:loc></ns:url>
    </ns:urlset>`;
    expect(parseSitemapXml(xml).urls).toEqual(["https://ex.com/x?a=1&b=2"]);
  });

  test("XML pourri → tout vide, sans throw", () => {
    expect(parseSitemapXml("<urlset><url><loc>oops")).toEqual({
      urls: [],
      children: [],
    });
    expect(parseSitemapXml("")).toEqual({ urls: [], children: [] });
    expect(parseSitemapXml("not xml at all")).toEqual({
      urls: [],
      children: [],
    });
  });
});

describe("discoverSitemapUrls", () => {
  test("robots.txt multi-Sitemap → union dédupliquée", async () => {
    const f = fetcher({
      "https://ex.com/robots.txt":
        "Sitemap: https://ex.com/a.xml\nSitemap: https://ex.com/b.xml",
      "https://ex.com/a.xml":
        "<urlset><url><loc>https://ex.com/1</loc></url><url><loc>https://ex.com/2</loc></url></urlset>",
      "https://ex.com/b.xml":
        "<urlset><url><loc>https://ex.com/2</loc></url><url><loc>https://ex.com/3</loc></url></urlset>",
    });
    const res = await discoverSitemapUrls("https://ex.com", f);
    expect(res.truncated).toBe(false);
    expect(res.urls).toEqual([
      "https://ex.com/1",
      "https://ex.com/2",
      "https://ex.com/3",
    ]);
  });

  test("fallback /sitemap.xml quand robots absent ou sans Sitemap", async () => {
    const f = fetcher({
      "https://ex.com/robots.txt": "User-agent: *\nDisallow:",
      "https://ex.com/sitemap.xml":
        "<urlset><url><loc>https://ex.com/home</loc></url></urlset>",
    });
    const res = await discoverSitemapUrls("https://ex.com", f);
    expect(res.urls).toEqual(["https://ex.com/home"]);
  });

  test("sitemapindex récursif (profondeur imbriquée)", async () => {
    const f = fetcher({
      "https://ex.com/robots.txt": "Sitemap: https://ex.com/index.xml",
      "https://ex.com/index.xml":
        "<sitemapindex><sitemap><loc>https://ex.com/sub.xml</loc></sitemap></sitemapindex>",
      "https://ex.com/sub.xml":
        "<urlset><url><loc>https://ex.com/deep</loc></url></urlset>",
    });
    const res = await discoverSitemapUrls("https://ex.com", f);
    expect(res.urls).toEqual(["https://ex.com/deep"]);
  });

  test("profondeur > 3 non suivie (chaîne d'index)", async () => {
    // index (1) → l2 (2) → l3 (3) → l4 (4, non fetch) ; les pages de l3 sont prises.
    const f = fetcher({
      "https://ex.com/robots.txt": "Sitemap: https://ex.com/l1.xml",
      "https://ex.com/l1.xml":
        "<sitemapindex><sitemap><loc>https://ex.com/l2.xml</loc></sitemap></sitemapindex>",
      "https://ex.com/l2.xml":
        "<sitemapindex><sitemap><loc>https://ex.com/l3.xml</loc></sitemap></sitemapindex>",
      "https://ex.com/l3.xml":
        "<sitemapindex><sitemap><loc>https://ex.com/l4.xml</loc></sitemap></sitemapindex>",
      "https://ex.com/l4.xml":
        "<urlset><url><loc>https://ex.com/too-deep</loc></url></urlset>",
    });
    const res = await discoverSitemapUrls("https://ex.com", f);
    expect(res.urls).toEqual([]);
  });

  test("anti-boucle sur sitemap auto-référent", async () => {
    const f = fetcher({
      "https://ex.com/robots.txt": "Sitemap: https://ex.com/loop.xml",
      "https://ex.com/loop.xml":
        "<sitemapindex><sitemap><loc>https://ex.com/loop.xml</loc></sitemap><sitemap><loc>https://ex.com/real.xml</loc></sitemap></sitemapindex>",
      "https://ex.com/real.xml":
        "<urlset><url><loc>https://ex.com/ok</loc></url></urlset>",
    });
    const res = await discoverSitemapUrls("https://ex.com", f);
    expect(res.urls).toEqual(["https://ex.com/ok"]);
  });

  test("cross-domain filtré, www toléré des deux côtés", async () => {
    const f = fetcher({
      "https://www.ex.com/robots.txt": "Sitemap: https://www.ex.com/s.xml",
      "https://www.ex.com/s.xml": `<urlset>
        <url><loc>https://ex.com/keep</loc></url>
        <url><loc>https://www.ex.com/also-keep</loc></url>
        <url><loc>https://evil.com/drop</loc></url>
      </urlset>`,
    });
    // site déclaré avec www → host de base « ex.com » ; ex.com et www.ex.com acceptés, evil.com filtré.
    const res = await discoverSitemapUrls("https://www.ex.com", f);
    expect(res.urls).toEqual([
      "https://ex.com/keep",
      "https://www.ex.com/also-keep",
    ]);
  });

  test("cap 5000 + truncated", async () => {
    const many = Array.from(
      { length: SITEMAP_URL_CAP + 10 },
      (_, i) => `<url><loc>https://ex.com/p${i}</loc></url>`,
    ).join("");
    const f = fetcher({
      "https://ex.com/robots.txt": "Sitemap: https://ex.com/big.xml",
      "https://ex.com/big.xml": `<urlset>${many}</urlset>`,
    });
    const res = await discoverSitemapUrls("https://ex.com", f);
    expect(res.truncated).toBe(true);
    expect(res.urls).toHaveLength(SITEMAP_URL_CAP);
  });

  test("robots + sitemap.xml tous absents → vide sans throw", async () => {
    const res = await discoverSitemapUrls("https://ex.com", fetcher({}));
    expect(res).toEqual({ urls: [], truncated: false });
  });

  test("siteUrl invalide → vide sans throw", async () => {
    const res = await discoverSitemapUrls("pas-une-url", fetcher({}));
    expect(res).toEqual({ urls: [], truncated: false });
  });
});
