import { describe, expect, test } from "bun:test";
import {
  cancelBatchScrape,
  classifyScrapeResult,
  firecrawlConfigured,
  getBatchScrape,
  mapBatchResponse,
  scrapeOne,
  startBatchScrape,
} from "../src/firecrawl";

describe("classifyScrapeResult", () => {
  test("200 + html → ok", () => {
    expect(
      classifyScrapeResult({ statusCode: 200, rawHtml: "<html>x</html>" }),
    ).toEqual({
      status: "ok",
      httpStatus: 200,
    });
  });
  test("403 → blocked", () => {
    expect(
      classifyScrapeResult({ statusCode: 403, rawHtml: null }).status,
    ).toBe("blocked");
  });
  test("429 → blocked, 500 → fetch_error", () => {
    expect(
      classifyScrapeResult({ statusCode: 429, rawHtml: null }).status,
    ).toBe("blocked");
    expect(
      classifyScrapeResult({ statusCode: 500, rawHtml: null }).status,
    ).toBe("fetch_error");
  });
  test("200 + html vide/challenge → empty/blocked", () => {
    expect(classifyScrapeResult({ statusCode: 200, rawHtml: "" }).status).toBe(
      "empty",
    );
    expect(
      classifyScrapeResult({
        statusCode: 200,
        rawHtml: "<title>Just a moment...</title>",
      }).status,
    ).toBe("blocked");
  });
  test("401 → blocked", () => {
    expect(
      classifyScrapeResult({ statusCode: 401, rawHtml: null }).status,
    ).toBe("blocked");
  });
  test("challenge markers are case-insensitive and only checked in the first 2KB", () => {
    expect(
      classifyScrapeResult({
        statusCode: 200,
        rawHtml: "<html>ATTENTION REQUIRED</html>",
      }).status,
    ).toBe("blocked");
    expect(
      classifyScrapeResult({
        statusCode: 200,
        rawHtml: "<html>incapsula incident</html>",
      }).status,
    ).toBe("blocked");
    // marker beyond the first 2KB must NOT trigger a false positive.
    const farHtml = `<html>${"a".repeat(2100)}Just a moment</html>`;
    expect(
      classifyScrapeResult({ statusCode: 200, rawHtml: farHtml }).status,
    ).toBe("ok");
  });
  test("error mentionnant un timeout → timeout, quel que soit le statusCode", () => {
    expect(
      classifyScrapeResult({
        statusCode: 200,
        rawHtml: null,
        error: "Request timeout after 30000ms",
      }).status,
    ).toBe("timeout");
  });
});

describe("mapBatchResponse", () => {
  test("job scraping → progression", () => {
    expect(
      mapBatchResponse({
        status: "scraping",
        completed: 3,
        total: 10,
        data: [],
      }),
    ).toEqual({
      status: "scraping",
      completed: 3,
      total: 10,
    });
  });
  test("job completed → pages classifiées", () => {
    const r = mapBatchResponse({
      status: "completed",
      completed: 2,
      total: 2,
      data: [
        {
          metadata: { sourceURL: "https://a.com", statusCode: 200 },
          rawHtml: "<html>a</html>",
        },
        {
          metadata: { sourceURL: "https://b.com", statusCode: 403 },
          rawHtml: null,
        },
      ],
    });
    expect(r.status).toBe("completed");
    if (r.status === "completed") {
      expect(r.pages[0]).toMatchObject({ url: "https://a.com", status: "ok" });
      expect(r.pages[1]).toMatchObject({
        url: "https://b.com",
        status: "blocked",
        httpStatus: 403,
      });
    }
  });
  test("job failed → error", () => {
    expect(mapBatchResponse({ status: "failed", error: "boom" })).toEqual({
      status: "failed",
      error: "boom",
    });
  });
});

describe("firecrawlConfigured", () => {
  test("false when FIRECRAWL_API_URL/FIRECRAWL_API_KEY are unset", () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = undefined;
    process.env.FIRECRAWL_API_KEY = undefined;
    try {
      expect(firecrawlConfigured()).toBe(false);
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("true when both are set", () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      expect(firecrawlConfigured()).toBe(true);
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });
});

describe("startBatchScrape", () => {
  const withEnv = async (fn: () => Promise<void>) => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      await fn();
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  };

  test("POST /v2/batch/scrape with the right body/headers → jobId", async () => {
    await withEnv(async () => {
      let capturedUrl: string | undefined;
      let capturedInit: RequestInit | undefined;
      const fakeFetch = (async (url: string, init?: RequestInit) => {
        capturedUrl = url;
        capturedInit = init;
        return new Response(JSON.stringify({ success: true, id: "job-123" }), {
          status: 200,
        });
      }) as typeof fetch;

      const { jobId } = await startBatchScrape(
        ["https://a.com", "https://b.com"],
        fakeFetch,
      );

      expect(jobId).toBe("job-123");
      expect(capturedUrl).toBe("https://firecrawl.example.com/v2/batch/scrape");
      expect(capturedInit?.method).toBe("POST");
      const headers = capturedInit?.headers as Record<string, string>;
      expect(headers.Authorization).toBe("Bearer test-key");
      const body = JSON.parse(capturedInit?.body as string);
      expect(body).toEqual({
        urls: ["https://a.com", "https://b.com"],
        formats: ["rawHtml"],
        onlyMainContent: false,
        maxAge: 0,
        timeout: 30000,
      });
      expect(capturedInit?.signal).toBeInstanceOf(AbortSignal);
    });
  });

  test("throws when Firecrawl is not configured", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    delete process.env.FIRECRAWL_API_URL;
    delete process.env.FIRECRAWL_API_KEY;
    try {
      await expect(startBatchScrape(["https://a.com"])).rejects.toThrow();
    } finally {
      if (prevUrl !== undefined) process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey !== undefined) process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });
});

describe("getBatchScrape", () => {
  test("GET /v2/batch/scrape/{id} → BatchState mappé", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      let capturedUrl: string | undefined;
      let capturedInit: RequestInit | undefined;
      const fakeFetch = (async (url: string, init?: RequestInit) => {
        capturedUrl = url;
        capturedInit = init;
        return new Response(
          JSON.stringify({
            status: "scraping",
            completed: 1,
            total: 4,
            data: [],
          }),
          { status: 200 },
        );
      }) as typeof fetch;

      const state = await getBatchScrape("job-123", fakeFetch);

      expect(capturedUrl).toBe(
        "https://firecrawl.example.com/v2/batch/scrape/job-123",
      );
      expect(capturedInit?.signal).toBeInstanceOf(AbortSignal);
      expect(state).toEqual({ status: "scraping", completed: 1, total: 4 });
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("HTTP non-ok → unreachable (API injoignable, pas un échec du job)", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      const fakeFetch = (async () =>
        new Response("nope", { status: 500 })) as unknown as typeof fetch;
      const state = await getBatchScrape("job-123", fakeFetch);
      expect(state.status).toBe("unreachable");
      expect((state as { error: string }).error).toContain("500");
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("fetch rejeté (réseau) → unreachable", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      const fakeFetch = (async () => {
        throw new Error("network down");
      }) as unknown as typeof fetch;
      const state = await getBatchScrape("job-123", fakeFetch);
      expect(state).toEqual({ status: "unreachable", error: "network down" });
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("fetch abandonné par AbortSignal.timeout (hang TCP) → unreachable, comme toute erreur réseau", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      const fakeFetch = (async () => {
        throw new DOMException("The operation was aborted.", "TimeoutError");
      }) as unknown as typeof fetch;
      const state = await getBatchScrape("job-123", fakeFetch);
      expect(state.status).toBe("unreachable");
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("JSON invalide (200 mais body non-parsable) → unreachable", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      const fakeFetch = (async () =>
        ({
          ok: true,
          status: 200,
          json: async () => {
            throw new SyntaxError("Unexpected token < in JSON at position 0");
          },
        }) as unknown as Response) as unknown as typeof fetch;
      const state = await getBatchScrape("job-123", fakeFetch);
      expect(state.status).toBe("unreachable");
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("réponse valide avec status failed/cancelled → failed (job RÉELLEMENT en échec)", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      const fakeFetch = (async () =>
        new Response(
          JSON.stringify({ status: "failed", error: "job blew up" }),
          { status: 200 },
        )) as unknown as typeof fetch;
      const state = await getBatchScrape("job-123", fakeFetch);
      expect(state).toEqual({ status: "failed", error: "job blew up" });
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("Firecrawl non configuré → unreachable, jamais un throw", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    delete process.env.FIRECRAWL_API_URL;
    delete process.env.FIRECRAWL_API_KEY;
    try {
      const state = await getBatchScrape("job-123");
      expect(state.status).toBe("unreachable");
    } finally {
      if (prevUrl !== undefined) process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey !== undefined) process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });
});

describe("cancelBatchScrape", () => {
  const withEnv = async (fn: () => Promise<void>) => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      await fn();
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  };

  test("DELETE /v2/batch/scrape/{jobId} avec les bons headers", async () => {
    await withEnv(async () => {
      let capturedUrl: string | undefined;
      let capturedInit: RequestInit | undefined;
      const fakeFetch = (async (url: string, init?: RequestInit) => {
        capturedUrl = url;
        capturedInit = init;
        return new Response(null, { status: 200 });
      }) as typeof fetch;

      await cancelBatchScrape("job-123", fakeFetch);

      expect(capturedUrl).toBe(
        "https://firecrawl.example.com/v2/batch/scrape/job-123",
      );
      expect(capturedInit?.method).toBe("DELETE");
      const headers = capturedInit?.headers as Record<string, string>;
      expect(headers.Authorization).toBe("Bearer test-key");
      expect(capturedInit?.signal).toBeInstanceOf(AbortSignal);
    });
  });

  test("best-effort : HTTP non-ok ne throw jamais", async () => {
    await withEnv(async () => {
      const fakeFetch = (async () =>
        new Response("nope", { status: 500 })) as unknown as typeof fetch;
      await expect(cancelBatchScrape("job-123", fakeFetch)).resolves.toBeUndefined();
    });
  });

  test("best-effort : erreur réseau ne throw jamais", async () => {
    await withEnv(async () => {
      const fakeFetch = (async () => {
        throw new Error("network down");
      }) as unknown as typeof fetch;
      await expect(cancelBatchScrape("job-123", fakeFetch)).resolves.toBeUndefined();
    });
  });

  test("best-effort : fetch abandonné par AbortSignal.timeout (hang TCP) ne throw jamais", async () => {
    await withEnv(async () => {
      const fakeFetch = (async () => {
        throw new DOMException("The operation was aborted.", "TimeoutError");
      }) as unknown as typeof fetch;
      await expect(cancelBatchScrape("job-123", fakeFetch)).resolves.toBeUndefined();
    });
  });

  test("Firecrawl non configuré → ne throw jamais", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    delete process.env.FIRECRAWL_API_URL;
    delete process.env.FIRECRAWL_API_KEY;
    try {
      await expect(cancelBatchScrape("job-123")).resolves.toBeUndefined();
    } finally {
      if (prevUrl !== undefined) process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey !== undefined) process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });
});

describe("scrapeOne", () => {
  const withEnv = async (fn: () => Promise<void>) => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    process.env.FIRECRAWL_API_URL = "https://firecrawl.example.com";
    process.env.FIRECRAWL_API_KEY = "test-key";
    try {
      await fn();
    } finally {
      if (prevUrl === undefined) delete process.env.FIRECRAWL_API_URL;
      else process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey === undefined) delete process.env.FIRECRAWL_API_KEY;
      else process.env.FIRECRAWL_API_KEY = prevKey;
    }
  };

  test("POST /v2/scrape → FirecrawlPage ok", async () => {
    await withEnv(async () => {
      let capturedInit: RequestInit | undefined;
      const fakeFetch = (async (
        url: RequestInfo | URL,
        init?: RequestInit,
      ) => {
        expect(url).toBe("https://firecrawl.example.com/v2/scrape");
        capturedInit = init;
        return new Response(
          JSON.stringify({
            success: true,
            data: { rawHtml: "<html>ok</html>", metadata: { statusCode: 200 } },
          }),
          { status: 200 },
        );
      }) as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(page).toEqual({
        url: "https://a.com",
        html: "<html>ok</html>",
        status: "ok",
        httpStatus: 200,
      });
      expect(capturedInit?.signal).toBeInstanceOf(AbortSignal);
    });
  });

  test("erreur réseau → 1 retry, succès au 2e essai", async () => {
    await withEnv(async () => {
      let calls = 0;
      const fakeFetch = (async () => {
        calls++;
        if (calls === 1) throw new Error("network down");
        return new Response(
          JSON.stringify({
            success: true,
            data: { rawHtml: "<html>ok</html>", metadata: { statusCode: 200 } },
          }),
          { status: 200 },
        );
      }) as unknown as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(calls).toBe(2);
      expect(page.status).toBe("ok");
    });
  });

  test("erreur réseau persistante (2 échecs) → fetch_error, pas de 3e essai", async () => {
    await withEnv(async () => {
      let calls = 0;
      const fakeFetch = (async () => {
        calls++;
        throw new Error("network down");
      }) as unknown as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(calls).toBe(2);
      expect(page).toEqual({
        url: "https://a.com",
        html: null,
        status: "fetch_error",
      });
    });
  });

  test("fetch abandonné par AbortSignal.timeout (hang TCP) sur les 2 tentatives → timeout, pas fetch_error", async () => {
    await withEnv(async () => {
      let calls = 0;
      const fakeFetch = (async () => {
        calls++;
        throw new DOMException("The operation was aborted.", "TimeoutError");
      }) as unknown as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(calls).toBe(2);
      expect(page).toEqual({
        url: "https://a.com",
        html: null,
        status: "timeout",
      });
    });
  });

  test("statut HTTP non-ok (403) → pas de retry, fetch_error PAS blocked (403 est celui de l'appel API, pas de la page cible)", async () => {
    await withEnv(async () => {
      let calls = 0;
      const fakeFetch = (async () => {
        calls++;
        return new Response("forbidden", { status: 403 });
      }) as unknown as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(calls).toBe(1);
      expect(page.status).toBe("fetch_error");
      expect(page.httpStatus).toBe(403);
    });
  });

  test("statut HTTP non-ok (401, API Firecrawl elle-même) → fetch_error, pas blocked", async () => {
    await withEnv(async () => {
      const fakeFetch = (async () =>
        new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(page.status).toBe("fetch_error");
      expect(page.httpStatus).toBe(401);
    });
  });

  test("body.data.metadata.statusCode = 403 (page cible bloquée, API 200) → blocked", async () => {
    await withEnv(async () => {
      const fakeFetch = (async () =>
        new Response(
          JSON.stringify({
            success: true,
            data: { rawHtml: null, metadata: { statusCode: 403 } },
          }),
          { status: 200 },
        )) as unknown as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(page.status).toBe("blocked");
      expect(page.httpStatus).toBe(403);
    });
  });

  test("Firecrawl non configuré → fetch_error, jamais un throw", async () => {
    const prevUrl = process.env.FIRECRAWL_API_URL;
    const prevKey = process.env.FIRECRAWL_API_KEY;
    delete process.env.FIRECRAWL_API_URL;
    delete process.env.FIRECRAWL_API_KEY;
    try {
      const page = await scrapeOne("https://a.com");
      expect(page).toEqual({
        url: "https://a.com",
        html: null,
        status: "fetch_error",
      });
    } finally {
      if (prevUrl !== undefined) process.env.FIRECRAWL_API_URL = prevUrl;
      if (prevKey !== undefined) process.env.FIRECRAWL_API_KEY = prevKey;
    }
  });

  test("réponse 200 avec body non-JSON (ex. proxy tronqué) → fetch_error, jamais un throw", async () => {
    await withEnv(async () => {
      const fakeFetch = (async () => {
        return {
          ok: true,
          status: 200,
          json: async () => {
            throw new SyntaxError("Unexpected token < in JSON at position 0");
          },
        } as unknown as Response;
      }) as unknown as typeof fetch;

      const page = await scrapeOne("https://a.com", fakeFetch);
      expect(page).toEqual({
        url: "https://a.com",
        html: null,
        status: "fetch_error",
      });
    });
  });
});
