import { describe, expect, it, vi } from "vitest";
import { ApiClient } from "./client.js";
import { ApiHttpError, ApiTimeoutError } from "./errors.js";

function mockFetch(responses: Array<{ status: number; body?: unknown; headers?: Record<string, string>; delay?: number }>): typeof fetch {
  let callIndex = 0;
  return vi.fn(async (_url: string | URL | Request, _init?: RequestInit) => {
    const resp = responses[callIndex++];
    if (!resp) throw new Error("No more mock responses");
    if (resp.delay) await new Promise((r) => setTimeout(r, resp.delay));
    return new Response(
      resp.body !== undefined ? JSON.stringify(resp.body) : null,
      {
        status: resp.status,
        headers: new Headers(resp.headers ?? {}),
      },
    );
  }) as unknown as typeof fetch;
}

describe("ApiClient", () => {
  it("deserializes a successful JSON response", async () => {
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "ru",
      fetchImpl: mockFetch([{ status: 200, body: { flights: [] } }]),
    });
    const data = await client.get<{ flights: unknown[] }>("/flights");
    expect(data).toEqual({ flights: [] });
  });

  it("appends query params and locale header", async () => {
    const fetchSpy = mockFetch([{ status: 200, body: {} }]);
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "en",
      fetchImpl: fetchSpy,
    });
    await client.get("/flights", { date: "2025-01-15", dep: "SVO" });
    const calledUrl = (fetchSpy as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as string;
    expect(calledUrl).toContain("/flights?");
    expect(calledUrl).toContain("date=2025-01-15");
    expect(calledUrl).toContain("dep=SVO");
  });

  it("throws ApiHttpError on 404 without retrying", async () => {
    const fetchSpy = mockFetch([{ status: 404, body: { error: "not found" } }]);
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "ru",
      fetchImpl: fetchSpy,
      retry: { maxRetries: 3 },
    });
    await expect(client.get("/flights/999")).rejects.toThrow(ApiHttpError);
    expect((fetchSpy as ReturnType<typeof vi.fn>).mock.calls).toHaveLength(1);
  });

  it("retries on 500 up to maxRetries then throws ApiHttpError", async () => {
    const fetchSpy = mockFetch([
      { status: 500 },
      { status: 500 },
      { status: 500 },
      { status: 500 },
    ]);
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "ru",
      fetchImpl: fetchSpy,
      retry: { maxRetries: 3, timeoutFactor: 0 },
    });
    await expect(client.get("/flights")).rejects.toThrow(ApiHttpError);
    // 1 initial + 3 retries = 4 total
    expect((fetchSpy as ReturnType<typeof vi.fn>).mock.calls).toHaveLength(4);
  });

  it("retries on 429 and succeeds on retry", async () => {
    const fetchSpy = mockFetch([
      { status: 429, headers: { "Retry-After": "0" } },
      { status: 200, body: { ok: true } },
    ]);
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "ru",
      fetchImpl: fetchSpy,
      retry: { maxRetries: 3, timeoutFactor: 0 },
    });
    const data = await client.get<{ ok: boolean }>("/flights");
    expect(data.ok).toBe(true);
    expect((fetchSpy as ReturnType<typeof vi.fn>).mock.calls).toHaveLength(2);
  });

  it("does not retry on 400", async () => {
    const fetchSpy = mockFetch([{ status: 400, body: { error: "bad request" } }]);
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "ru",
      fetchImpl: fetchSpy,
      retry: { maxRetries: 3 },
    });
    await expect(client.get("/flights")).rejects.toThrow(ApiHttpError);
    expect((fetchSpy as ReturnType<typeof vi.fn>).mock.calls).toHaveLength(1);
  });

  it("throws ApiTimeoutError on timeout", async () => {
    const neverResolve = vi.fn((_url: string | URL | Request, init?: RequestInit) =>
      new Promise<Response>((_resolve, reject) => {
        init?.signal?.addEventListener("abort", () => {
          reject(new DOMException("The operation was aborted.", "AbortError"));
        });
      }),
    );
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "ru",
      fetchImpl: neverResolve as unknown as typeof fetch,
      defaultTimeoutMs: 10,
      retry: { maxRetries: 0 },
    });
    await expect(client.get("/flights")).rejects.toThrow(ApiTimeoutError);
  });

  it("post() sends JSON body", async () => {
    const fetchSpy = mockFetch([{ status: 201, body: { id: 1 } }]);
    const client = new ApiClient({
      baseUrl: "https://api.example",
      locale: "ru",
      fetchImpl: fetchSpy,
    });
    const data = await client.post<{ id: number }>("/bookings", { flight: "SU100" });
    expect(data.id).toBe(1);
    const callInit = (fetchSpy as ReturnType<typeof vi.fn>).mock.calls[0]?.[1] as RequestInit;
    expect(callInit.method).toBe("POST");
    expect(callInit.body).toBe(JSON.stringify({ flight: "SU100" }));
  });
});
