import { afterEach, describe, expect, it, vi } from "vitest";
import { MegaWebFallback } from "../src/main/mega-web-fallback";

const originalFetch = globalThis.fetch;

describe("mega-web-fallback", () => {
  afterEach(() => {
    globalThis.fetch = originalFetch;
    vi.restoreAllMocks();
  });

  describe("MegaWebFallback class", () => {
    it("returns null when credentials are empty", async () => {
      const fallback = new MegaWebFallback(() => ({ login: "", password: "" }));
      const result = await fallback.unrestrict("https://mega.debrid/test");
      expect(result).toBeNull();
    });

    it("logs in, fetches HTML, parses code, and polls AJAX for direct url", async () => {
      let fetchCallCount = 0;
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const urlStr = String(url);
        fetchCallCount += 1;

        if (urlStr.includes("form=login")) {
          const headers = new Headers();
          headers.append("set-cookie", "session=goodcookie; path=/");
          return new Response("", { headers, status: 200 });
        }

        if (urlStr.includes("page=debrideur")) {
          return new Response('<form id="debridForm"></form>', { status: 200 });
        }

        if (urlStr.includes("form=debrid")) {
          return new Response(`
            <div class="acp-box">
              <h3>Link: https://mega.debrid/link1</h3>
              <a href="javascript:processDebrid(1,'secretcode123',0)">Download</a>
            </div>
          `, { status: 200 });
        }

        if (urlStr.includes("ajax=debrid")) {
          return new Response(JSON.stringify({ link: "https://mega.direct/123" }), { status: 200 });
        }

        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" }));

      const result = await fallback.unrestrict("https://mega.debrid/link1");
      expect(result).not.toBeNull();
      expect(result?.directUrl).toBe("https://mega.direct/123");
      expect(result?.fileName).toBe("link1");
      expect(fetchCallCount).toBe(4);
    });

    it("fails fast on 'Kein Server für diesen Hoster' (account hoster quota) instead of re-login + re-poll", async () => {
      let ajaxCalls = 0;
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const urlStr = String(url);
        if (urlStr.includes("form=login")) {
          const headers = new Headers();
          headers.append("set-cookie", "session=goodcookie; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (urlStr.includes("page=debrideur")) {
          return new Response('<form id="debridForm"></form>', { status: 200 });
        }
        if (urlStr.includes("form=debrid")) {
          return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l1</h3><a href="javascript:processDebrid(1,'code1',0)">d</a></div>`, { status: 200 });
        }
        if (urlStr.includes("ajax=debrid")) {
          ajaxCalls += 1;
          return new Response(JSON.stringify({ link: "", text: "Erreur : Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal." }), { status: 200 });
        }
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" }));
      await expect(fallback.unrestrict("https://mega.debrid/l1")).rejects.toThrow(/kein server für diesen hoster/i);
      expect(ajaxCalls).toBe(1);
    });

    it("surfaces 'Kein Server für diesen Hoster' from the debrid PAGE (daily limit, no debrid code) instead of empty", async () => {
      let ajaxCalls = 0;
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const urlStr = String(url);
        if (urlStr.includes("form=login")) {
          const headers = new Headers();
          headers.append("set-cookie", "session=goodcookie; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (urlStr.includes("page=debrideur")) {
          return new Response('<form id="debridForm"></form>', { status: 200 });
        }
        if (urlStr.includes("form=debrid")) {
          return new Response('<div class="error">Erreur : Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal.</div>', { status: 200 });
        }
        if (urlStr.includes("ajax=debrid")) {
          ajaxCalls += 1;
          return new Response(JSON.stringify({ link: "https://should.not/happen" }), { status: 200 });
        }
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" }));
      await expect(fallback.unrestrict("https://mega.debrid/l1")).rejects.toThrow(/kein server für diesen hoster/i);
      expect(ajaxCalls).toBe(0);
    });

    it("logs in with the per-account credentials passed to unrestrict, not the default", async () => {
      const loginsUsed: string[] = [];
      globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: { body?: unknown }) => {
        const urlStr = String(url);
        if (urlStr.includes("form=login")) {
          const params = new URLSearchParams(String(opts?.body ?? ""));
          loginsUsed.push(params.get("login") || "");
          const headers = new Headers();
          headers.append("set-cookie", "session=goodcookie; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (urlStr.includes("page=debrideur")) {
          return new Response('<form id="debridForm"></form>', { status: 200 });
        }
        if (urlStr.includes("form=debrid")) {
          return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l1</h3><a href="javascript:processDebrid(1,'code1',0)">d</a></div>`, { status: 200 });
        }
        if (urlStr.includes("ajax=debrid")) {
          return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 });
        }
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "defaultacc", password: "defpw" }));
      const result = await fallback.unrestrict("https://mega.debrid/l1", undefined, { login: "account2", password: "pw2" });
      expect(result?.directUrl).toBe("https://mega.direct/ok");
      expect(loginsUsed).toContain("account2");
      expect(loginsUsed).not.toContain("defaultacc");
    });

    it("throws if login fails to set cookie", async () => {
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const urlStr = String(url);
        if (urlStr.includes("form=login")) {
          const headers = new Headers();
          return new Response("", { headers, status: 200 });
        }
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "bad", password: "bad" }));

      await expect(fallback.unrestrict("http://mega.debrid/file"))
        .rejects.toThrow("Mega-Web Login liefert kein Session-Cookie");
    });

    it("throws if login verify check fails (no form found)", async () => {
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const urlStr = String(url);
        if (urlStr.includes("form=login")) {
          const headers = new Headers();
          headers.append("set-cookie", "session=goodcookie; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (urlStr.includes("page=debrideur")) {
          return new Response('<html><body>Nothing here</body></html>', { status: 200 });
        }
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "a", password: "b" }));

      await expect(fallback.unrestrict("http://mega.debrid/file"))
        .rejects.toThrow("Mega-Web Login ungültig oder Session blockiert");
    });

    it("returns null if generation fails to find a code", async () => {
      let callCount = 0;
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const urlStr = String(url);
        callCount++;
        if (urlStr.includes("form=login")) {
          const headers = new Headers();
          headers.append("set-cookie", "session=goodcookie; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (urlStr.includes("page=debrideur")) {
          return new Response('<form id="debridForm"></form>', { status: 200 });
        }
        if (urlStr.includes("form=debrid")) {
          return new Response(`<div>No links here</div>`, { status: 200 });
        }
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "a", password: "b" }));
      const result = await fallback.unrestrict("http://mega.debrid/file");

      expect(result).toBeNull();
    });

    it("serialisiert gleichzeitige Umwandlungen auf DEMSELBEN Account (kein Doppel-Login)", async () => {
      let loginCount = 0;
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const u = String(url);
        if (u.includes("form=login")) {
          loginCount += 1;
          await new Promise((r) => setTimeout(r, 15));
          const headers = new Headers();
          headers.append("set-cookie", "session=c; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (u.includes("page=debrideur")) return new Response('<form id="debridForm"></form>', { status: 200 });
        if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { status: 200 });
        if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 });
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "same", password: "pw" }));
      const [r1, r2] = await Promise.all([
        fallback.unrestrict("https://mega.debrid/a", undefined, { login: "same", password: "pw" }),
        fallback.unrestrict("https://mega.debrid/b", undefined, { login: "same", password: "pw" })
      ]);
      expect(r1?.directUrl).toBe("https://mega.direct/ok");
      expect(r2?.directUrl).toBe("https://mega.direct/ok");
      // Serialisiert auf demselben Account → der zweite nutzt die gecachte Session, kein zweiter Login.
      expect(loginCount).toBe(1);
    });

    it("wandelt auf VERSCHIEDENEN Accounts parallel um (Logins laufen gleichzeitig)", async () => {
      let activeLogins = 0;
      let maxActiveLogins = 0;
      const bothInFlight = new Promise<void>((resolve) => {
        const check = setInterval(() => { if (activeLogins >= 2) { clearInterval(check); resolve(); } }, 5);
      });
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const u = String(url);
        if (u.includes("form=login")) {
          activeLogins += 1;
          maxActiveLogins = Math.max(maxActiveLogins, activeLogins);
          await bothInFlight;
          activeLogins -= 1;
          const headers = new Headers();
          headers.append("set-cookie", "session=c; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (u.includes("page=debrideur")) return new Response('<form id="debridForm"></form>', { status: 200 });
        if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { status: 200 });
        if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 });
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "d", password: "p" }));
      const [r1, r2] = await Promise.all([
        fallback.unrestrict("https://mega.debrid/a", undefined, { login: "acc1", password: "p" }),
        fallback.unrestrict("https://mega.debrid/b", undefined, { login: "acc2", password: "p" })
      ]);
      expect(r1?.directUrl).toBe("https://mega.direct/ok");
      expect(r2?.directUrl).toBe("https://mega.direct/ok");
      // Verschiedene Accounts → beide Logins gleichzeitig in-flight (sonst haengt es am bothInFlight-Barrier).
      expect(maxActiveLogins).toBe(2);
    }, 10000);

    it("aborts pending Mega-Web polling when signal is cancelled", async () => {
      globalThis.fetch = vi.fn((url: string | URL | Request, init?: RequestInit): Promise<Response> => {
        const urlStr = String(url);

        if (urlStr.includes("form=login")) {
          const headers = new Headers();
          headers.append("set-cookie", "session=goodcookie; path=/");
          return Promise.resolve(new Response("", { headers, status: 200 }));
        }

        if (urlStr.includes("page=debrideur")) {
          return Promise.resolve(new Response('<form id="debridForm"></form>', { status: 200 }));
        }

        if (urlStr.includes("form=debrid")) {
          return Promise.resolve(new Response(`
            <div class="acp-box">
              <h3>Link: https://mega.debrid/link2</h3>
              <a href="javascript:processDebrid(1,'secretcode456',0)">Download</a>
            </div>
          `, { status: 200 }));
        }

        if (urlStr.includes("ajax=debrid")) {
          return new Promise<Response>((_resolve, reject) => {
            const signal = init?.signal;
            const onAbort = (): void => reject(new Error("aborted:ajax"));
            if (signal?.aborted) {
              onAbort();
              return;
            }
            signal?.addEventListener("abort", onAbort, { once: true });
          });
        }

        return Promise.resolve(new Response("Not found", { status: 404 }));
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" }));
      const controller = new AbortController();
      const timer = setTimeout(() => {
        controller.abort("test");
      }, 200);

      try {
        await expect(fallback.unrestrict("https://mega.debrid/link2", controller.signal)).rejects.toThrow(/aborted/i);
      } finally {
        clearTimeout(timer);
      }
    });

    it("klassifiziert einen Abbruch WAEHREND in der Queue als Queue-Timeout (nicht harter Abbruch), damit der belegte Account nicht bestraft wird", async () => {
      let releaseLogin: () => void = () => {};
      const loginGate = new Promise<void>((resolve) => { releaseLogin = resolve; });
      globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
        const u = String(url);
        if (u.includes("form=login")) {
          await loginGate;
          const headers = new Headers();
          headers.append("set-cookie", "session=c; path=/");
          return new Response("", { headers, status: 200 });
        }
        if (u.includes("page=debrideur")) return new Response('<form id="debridForm"></form>', { status: 200 });
        if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { status: 200 });
        if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 });
        return new Response("Not found", { status: 404 });
      }) as unknown as typeof fetch;

      const fallback = new MegaWebFallback(() => ({ login: "same", password: "pw" }));
      const firstCtrl = new AbortController();
      const first = fallback.unrestrict("https://mega.debrid/a", firstCtrl.signal, { login: "same", password: "pw" });

      await new Promise((r) => setTimeout(r, 25));

      const secondCtrl = new AbortController();
      const second = fallback.unrestrict("https://mega.debrid/b", secondCtrl.signal, { login: "same", password: "pw" });

      await new Promise((r) => setTimeout(r, 25));
      secondCtrl.abort("caller-timeout");

      await expect(second).rejects.toThrow(/queue.?timeout/i);

      releaseLogin();
      await first.catch(() => null);
      await new Promise((r) => setTimeout(r, 20));
    }, 10000);
  });
});
