import { afterEach, describe, expect, it, vi } from "vitest";
import {
  bookRequestSlots,
  cancelBookedSlots,
  fetchBookedSlotsByRequest,
  fetchRequestSlots,
} from "./slotsApi";

function jsonResponse(body: unknown): Response {
  return new Response(JSON.stringify(body), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
}

function textResponse(body: string): Response {
  return new Response(body, { status: 200 });
}

const baseParams = { requestId: "r1", timeStart: "2026-01-01T00:00:00", timeStop: "2026-01-02T00:00:00" };

afterEach(() => vi.restoreAllMocks());

describe("fetchRequestSlots", () => {
  it("прокидывает coverageStrategy в query", async () => {
    const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([]));
    await fetchRequestSlots({ ...baseParams, coverageStrategy: "CONTINUOUS" });
    const url = String(fetchMock.mock.calls[0][0]);
    expect(url).toContain("request-cover");
    expect(url).toContain("coverageStrategy=CONTINUOUS");
  });

  it("не добавляет coverageStrategy, если не задана", async () => {
    const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([]));
    await fetchRequestSlots(baseParams);
    expect(String(fetchMock.mock.calls[0][0])).not.toContain("coverageStrategy");
  });

  it("прокидывает all=true в query (показать и занятые бронью)", async () => {
    const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([]));
    await fetchRequestSlots({ ...baseParams, all: true });
    expect(String(fetchMock.mock.calls[0][0])).toContain("all=true");
  });

  it("не добавляет all, если не задан", async () => {
    const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([]));
    await fetchRequestSlots(baseParams);
    expect(String(fetchMock.mock.calls[0][0])).not.toContain("all=");
  });

  it("пробрасывает AbortSignal в fetch", async () => {
    const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([]));
    const ctrl = new AbortController();
    await fetchRequestSlots(baseParams, ctrl.signal);
    expect(fetchMock.mock.calls[0][1]?.signal).toBe(ctrl.signal);
  });

  it("отменённый запрос пробрасывает ошибку отмены", async () => {
    vi.spyOn(globalThis, "fetch").mockRejectedValue(new DOMException("aborted", "AbortError"));
    await expect(fetchRequestSlots(baseParams)).rejects.toThrow();
  });

  it("на не-ok бросает с текстом ошибки сервера", async () => {
    vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("boom", { status: 500 }));
    await expect(fetchRequestSlots(baseParams)).rejects.toThrow("boom");
  });
});

describe("bookRequestSlots", () => {
  it("POST с телом requestId+slots и возвращает забронированные", async () => {
    const fetchMock = vi
      .spyOn(globalThis, "fetch")
      .mockResolvedValue(jsonResponse([{ bookedSlotId: 1, satelliteId: 1, slotNum: 2, cycle: 0 }]));
    const res = await bookRequestSlots("r1", [{ satelliteId: 1, slotNum: 2, cycle: 0 }]);
    const [url, init] = fetchMock.mock.calls[0];
    expect(String(url)).toContain("booking/request");
    expect(init?.method).toBe("POST");
    expect(JSON.parse(String(init?.body))).toEqual({
      requestId: "r1",
      slots: [{ satelliteId: 1, slotNum: 2, cycle: 0 }],
    });
    expect(res).toHaveLength(1);
  });
});

describe("cancelBookedSlots", () => {
  it("DELETE booking/batch с requestId и списком ids в query, возвращает count", async () => {
    const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(textResponse("3"));
    const res = await cancelBookedSlots("r1", [11, 12, 13]);
    const [url, init] = fetchMock.mock.calls[0];
    expect(String(url)).toContain("booking/batch");
    expect(String(url)).toContain("requestId=r1");
    expect(String(url)).toContain("ids=11");
    expect(String(url)).toContain("ids=12");
    expect(String(url)).toContain("ids=13");
    expect(init?.method).toBe("DELETE");
    expect(res).toBe(3);
  });

  it("пустой список ids — не зовёт fetch и возвращает 0", async () => {
    const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(textResponse("0"));
    const res = await cancelBookedSlots("r1", []);
    expect(fetchMock).not.toHaveBeenCalled();
    expect(res).toBe(0);
  });

  it("на не-ok бросает с текстом ошибки сервера", async () => {
    vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("boom", { status: 500 }));
    await expect(cancelBookedSlots("r1", [1])).rejects.toThrow("boom");
  });
});

describe("fetchBookedSlotsByRequest", () => {
  it("возвращает брони с bookedSlotId и контуром", async () => {
    vi.spyOn(globalThis, "fetch").mockResolvedValue(
      jsonResponse([
        {
          bookedSlotId: 42,
          cycle: 3,
          satelliteId: 12,
          slotNumber: 340,
          tn: "2026-01-01T10:00:00",
          tk: "2026-01-01T10:01:00",
          roll: 0,
          contour: "POLYGON ((0 0, 1 0, 1 1, 0 0))",
          revolution: 1,
          revolutionSign: "ASC",
          state: "BOOKED",
          latitude: 0,
          longitude: 0,
        },
      ]),
    );
    const res = await fetchBookedSlotsByRequest("r1");
    expect(res).toHaveLength(1);
    expect(res[0].bookedSlotId).toBe(42);
    expect(res[0].cycle).toBe(3);
    expect(res[0].contour).toContain("POLYGON");
  });
});
