/**
 * Unit tests for api: fetchDuties (403 handling, AbortError), fetchCalendarEvents.
 * Ported from webapp/js/api.test.js. buildFetchOptions is tested indirectly via fetch mock.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fetchDuties, fetchCalendarEvents, AccessDeniedError } from "./api";

describe("fetchDuties", () => {
  const originalFetch = globalThis.fetch;

  const validDuty = {
    id: 1,
    user_id: 10,
    start_at: "2025-02-01T09:00:00Z",
    end_at: "2025-02-01T18:00:00Z",
    full_name: "Test User",
    event_type: "duty" as const,
    phone: null as string | null,
    username: "@test",
  };

  beforeEach(() => {
    vi.stubGlobal("fetch", vi.fn());
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: true,
      status: 200,
      json: () => Promise.resolve([validDuty]),
    });
  });

  afterEach(() => {
    vi.stubGlobal("fetch", originalFetch);
  });

  it("returns JSON on 200", async () => {
    const result = await fetchDuties(
      "2025-02-01",
      "2025-02-28",
      "test-init-data",
      "ru"
    );
    expect(result).toEqual([validDuty]);
  });

  it("sets X-Telegram-Init-Data and Accept-Language headers", async () => {
    let capturedOpts: RequestInit | null = null;
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((_url: string, opts?: RequestInit) => {
      capturedOpts = opts ?? null;
      return Promise.resolve({
        ok: true,
        status: 200,
        json: () => Promise.resolve([]),
      } as Response);
    });
    await fetchDuties("2025-02-01", "2025-02-28", "init-data-string", "en");
    const headers = capturedOpts?.headers as Record<string, string>;
    expect(headers["X-Telegram-Init-Data"]).toBe("init-data-string");
    expect(headers["Accept-Language"]).toBe("en");
  });

  it("omits X-Telegram-Init-Data when initData empty", async () => {
    let capturedOpts: RequestInit | null = null;
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation((_url: string, opts?: RequestInit) => {
      capturedOpts = opts ?? null;
      return Promise.resolve({
        ok: true,
        status: 200,
        json: () => Promise.resolve([]),
      } as Response);
    });
    await fetchDuties("2025-02-01", "2025-02-28", "", "ru");
    const headers = capturedOpts?.headers as Record<string, string>;
    expect(headers["X-Telegram-Init-Data"]).toBeUndefined();
  });

  it("throws AccessDeniedError on 403 with server detail from body", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 403,
      json: () => Promise.resolve({ detail: "Custom access denied" }),
    } as Response);
    await expect(
      fetchDuties("2025-02-01", "2025-02-28", "test", "ru")
    ).rejects.toMatchObject({
      message: "ACCESS_DENIED",
      serverDetail: "Custom access denied",
    });
    await expect(
      fetchDuties("2025-02-01", "2025-02-28", "test", "ru")
    ).rejects.toThrow(AccessDeniedError);
  });

  it("rethrows AbortError when request is aborted", async () => {
    const aborter = new AbortController();
    const abortError = new DOMException("aborted", "AbortError");
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation(() => Promise.reject(abortError));
    await expect(
      fetchDuties("2025-02-01", "2025-02-28", "test", "ru", aborter.signal)
    ).rejects.toMatchObject({ name: "AbortError" });
  });
});

describe("fetchCalendarEvents", () => {
  const originalFetch = globalThis.fetch;

  beforeEach(() => {
    vi.stubGlobal("fetch", vi.fn());
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: true,
      status: 200,
      json: () =>
        Promise.resolve([{ date: "2025-02-25", summary: "Holiday" }]),
    } as Response);
  });

  afterEach(() => {
    vi.stubGlobal("fetch", originalFetch);
  });

  it("returns JSON array on 200", async () => {
    const result = await fetchCalendarEvents(
      "2025-02-01",
      "2025-02-28",
      "init-data",
      "ru"
    );
    expect(result).toEqual([{ date: "2025-02-25", summary: "Holiday" }]);
  });

  it("returns empty array on non-OK response", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 500,
    } as Response);
    const result = await fetchCalendarEvents(
      "2025-02-01",
      "2025-02-28",
      "init-data",
      "ru"
    );
    expect(result).toEqual([]);
  });

  it("throws AccessDeniedError on 403", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 403,
      json: () => Promise.resolve({ detail: "Access denied" }),
    } as Response);
    await expect(
      fetchCalendarEvents("2025-02-01", "2025-02-28", "init-data", "ru")
    ).rejects.toThrow(AccessDeniedError);
    await expect(
      fetchCalendarEvents("2025-02-01", "2025-02-28", "init-data", "ru")
    ).rejects.toMatchObject({
      message: "ACCESS_DENIED",
      serverDetail: "Access denied",
    });
  });

  it("rethrows AbortError when request is aborted", async () => {
    const aborter = new AbortController();
    const abortError = new DOMException("aborted", "AbortError");
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockImplementation(() => Promise.reject(abortError));
    await expect(
      fetchCalendarEvents(
        "2025-02-01",
        "2025-02-28",
        "init-data",
        "ru",
        aborter.signal
      )
    ).rejects.toMatchObject({ name: "AbortError" });
  });
});
