/**
 * 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,
  fetchAdminMe,
  fetchAdminUsers,
  patchAdminDuty,
  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" });
  });
});

describe("fetchAdminMe", () => {
  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({ is_admin: true }),
    } as Response);
  });

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

  it("returns is_admin true on 200", async () => {
    const result = await fetchAdminMe("init-data", "en");
    expect(result).toEqual({ is_admin: true });
  });

  it("returns is_admin false on 403", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 403,
    } as Response);
    const result = await fetchAdminMe("init-data", "en");
    expect(result).toEqual({ is_admin: false });
  });

  it("returns is_admin false on non-200 and non-403", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 500,
    } as Response);
    const result = await fetchAdminMe("init-data", "en");
    expect(result).toEqual({ is_admin: false });
  });

  it("returns is_admin false when response is not boolean", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: true,
      status: 200,
      json: () => Promise.resolve({ is_admin: "yes" }),
    } as Response);
    const result = await fetchAdminMe("init-data", "en");
    expect(result).toEqual({ is_admin: false });
  });
});

describe("fetchAdminUsers", () => {
  const originalFetch = globalThis.fetch;
  const validUsers = [
    { id: 1, full_name: "Alice", username: "alice", role_id: 1 },
    { id: 2, full_name: "Bob", username: null, role_id: 2 },
  ];

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

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

  it("returns users array on 200", async () => {
    const result = await fetchAdminUsers("init-data", "en");
    expect(result).toEqual(validUsers);
  });

  it("throws AccessDeniedError on 403", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 403,
      json: () => Promise.resolve({ detail: "Admin only" }),
    } as Response);
    await expect(fetchAdminUsers("init-data", "en")).rejects.toThrow(AccessDeniedError);
    await expect(fetchAdminUsers("init-data", "en")).rejects.toMatchObject({
      message: "ACCESS_DENIED",
      serverDetail: "Admin only",
    });
  });

  it("filters invalid items from response", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: true,
      status: 200,
      json: () =>
        Promise.resolve([
          validUsers[0],
          { id: 2, full_name: "Bob", username: 123, role_id: 2 },
          { id: 3 },
        ]),
    } as Response);
    const result = await fetchAdminUsers("init-data", "en");
    expect(result).toEqual([validUsers[0]]);
  });
});

describe("patchAdminDuty", () => {
  const originalFetch = globalThis.fetch;
  const updatedDuty = {
    id: 5,
    user_id: 2,
    start_at: "2025-03-01T09:00:00Z",
    end_at: "2025-03-01T18:00:00Z",
  };

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

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

  it("sends PATCH with user_id and returns updated duty", async () => {
    const result = await patchAdminDuty(5, 2, "init-data", "en");
    expect(result).toEqual(updatedDuty);
    const fetchCall = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(fetchCall[1]?.method).toBe("PATCH");
    expect(fetchCall[1]?.body).toBe(JSON.stringify({ user_id: 2 }));
  });

  it("throws AccessDeniedError on 403", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 403,
      json: () => Promise.resolve({ detail: "Admin only" }),
    } as Response);
    await expect(
      patchAdminDuty(5, 2, "init-data", "en")
    ).rejects.toThrow(AccessDeniedError);
  });

  it("throws with server detail on 400", async () => {
    (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: false,
      status: 400,
      json: () => Promise.resolve({ detail: "User not found" }),
    } as Response);
    await expect(
      patchAdminDuty(5, 999, "init-data", "en")
    ).rejects.toThrow("User not found");
  });
});
