import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import userManager from "./UserManager";
import api from "./Api";

// Api.js talks to the server through the global fetch and looks up credentials via userManager.
// (vi.mock is hoisted above the imports by vitest, so the import above receives the mock.)
vi.mock("./UserManager", () => ({ default: { get: vi.fn() } }));

let fetchMock;

beforeEach(() => {
  vi.clearAllMocks();
  vi.spyOn(console, "log").mockImplementation(() => {});
  fetchMock = vi.fn();
  vi.stubGlobal("fetch", fetchMock);
  userManager.get.mockResolvedValue(undefined); // anonymous by default
});

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllGlobals();
});

describe("Api.poll", () => {
  it("parses newline-delimited JSON and skips lines without an id", async () => {
    const body = [
      JSON.stringify({ id: "1", message: "a" }),
      JSON.stringify({ event: "keepalive" }), // no id -> skipped
      JSON.stringify({ id: "2", message: "b" }),
    ].join("\n");
    fetchMock.mockResolvedValue(new Response(body));

    const messages = await api.poll("https://ntfy.sh", "mytopic");

    expect(messages.map((m) => m.id)).toEqual(["1", "2"]);
  });

  it("uses the plain poll URL without a since cursor", async () => {
    fetchMock.mockResolvedValue(new Response(""));
    await api.poll("https://ntfy.sh", "mytopic");
    expect(fetchMock).toHaveBeenCalledWith("https://ntfy.sh/mytopic/json?poll=1", expect.anything());
  });

  it("uses the since URL when a cursor is given", async () => {
    fetchMock.mockResolvedValue(new Response(""));
    await api.poll("https://ntfy.sh", "mytopic", 12345);
    expect(fetchMock).toHaveBeenCalledWith("https://ntfy.sh/mytopic/json?poll=1&since=12345", expect.anything());
  });
});

describe("Api.publish", () => {
  it("PUTs the message body to the base URL", async () => {
    fetchMock.mockResolvedValue({ status: 200 });
    await api.publish("https://ntfy.sh", "mytopic", "Hello", { priority: 5, tags: ["warning"] });
    expect(fetchMock).toHaveBeenCalledWith(
      "https://ntfy.sh",
      expect.objectContaining({
        method: "PUT",
        body: JSON.stringify({ topic: "mytopic", message: "Hello", priority: 5, tags: ["warning"] }),
      }),
    );
  });

  it("attaches basic auth when the user has a password", async () => {
    userManager.get.mockResolvedValue({ username: "phil", password: "secret" });
    fetchMock.mockResolvedValue({ status: 200 });
    await api.publish("https://ntfy.sh", "mytopic", "Hi");
    const [, options] = fetchMock.mock.calls[0];
    expect(options.headers.Authorization).toBe(`Basic ${btoa("phil:secret")}`);
  });

  it("attaches bearer auth when the user has a token", async () => {
    userManager.get.mockResolvedValue({ token: "tk_abc" });
    fetchMock.mockResolvedValue({ status: 200 });
    await api.publish("https://ntfy.sh", "mytopic", "Hi");
    const [, options] = fetchMock.mock.calls[0];
    expect(options.headers.Authorization).toBe("Bearer tk_abc");
  });
});

describe("Api.topicAuth", () => {
  it("returns true for a 2xx response", async () => {
    fetchMock.mockResolvedValue({ status: 200 });
    expect(await api.topicAuth("https://ntfy.sh", "mytopic")).toBe(true);
  });

  it("returns false for 401/403", async () => {
    fetchMock.mockResolvedValue({ status: 403 });
    expect(await api.topicAuth("https://ntfy.sh", "mytopic")).toBe(false);
  });

  it("throws for any other status", async () => {
    fetchMock.mockResolvedValue({ status: 500 });
    await expect(api.topicAuth("https://ntfy.sh", "mytopic")).rejects.toThrow("Unexpected server response 500");
  });
});

describe("Api web push", () => {
  const pushSubscription = {
    endpoint: "https://push.example/abc",
    keys: { auth: "AUTH", p256dh: "P256" },
  };

  it("updateWebPush POSTs endpoint, keys and topics to the web push URL", async () => {
    fetchMock.mockResolvedValue({ status: 200 });
    await api.updateWebPush(pushSubscription, ["topicA", "topicB"]);
    expect(fetchMock).toHaveBeenCalledWith(
      "https://ntfy.sh/v1/webpush",
      expect.objectContaining({
        method: "POST",
        body: JSON.stringify({ endpoint: "https://push.example/abc", auth: "AUTH", p256dh: "P256", topics: ["topicA", "topicB"] }),
      }),
    );
  });

  it("deleteWebPush DELETEs just the endpoint", async () => {
    fetchMock.mockResolvedValue({ status: 200 });
    await api.deleteWebPush(pushSubscription);
    expect(fetchMock).toHaveBeenCalledWith(
      "https://ntfy.sh/v1/webpush",
      expect.objectContaining({
        method: "DELETE",
        body: JSON.stringify({ endpoint: "https://push.example/abc" }),
      }),
    );
  });
});
