/**
 * Unit tests for useOnlineBoard hook — TZ §4.1.10, §4.1.12
 *
 * Covers:
 * - AbortController wired: new search aborts the previous in-flight request
 * - cancel() aborts the controller and resets loading
 *
 * @vitest-environment jsdom
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import type { ReactNode } from "react";
import React from "react";
import { useOnlineBoard } from "./useOnlineBoard.js";
import * as api from "../api.js";
import type { IBoardResponse } from "../types.js";
import { ApiClientProvider } from "@/shared/api/provider.js";
import type { ApiClient } from "@/shared/api/client.js";

// Minimal mock API client
function makeClient(): ApiClient {
  return {
    locale: "ru",
    get: vi.fn(),
    post: vi.fn(),
  } as unknown as ApiClient;
}

function makeWrapper(client: ApiClient) {
  return function Wrapper({ children }: { children: ReactNode }) {
    return React.createElement(ApiClientProvider, { client, children });
  };
}

const EMPTY_RESPONSE: IBoardResponse = {
  data: { routes: [], partners: [] },
};

describe("useOnlineBoard — §4.1.12 cancellation", () => {
  const baseParams = {
    dateFrom: "2026-01-15T00:00:00",
    dateTo: "2026-01-16T00:00:00",
    departure: "SVO",
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("passes AbortSignal to searchFlights", async () => {
    const client = makeClient();
    const spy = vi.spyOn(api, "searchFlights").mockResolvedValue(EMPTY_RESPONSE);

    renderHook(() => useOnlineBoard(baseParams), {
      wrapper: makeWrapper(client),
    });

    // Wait for the async effect
    await act(async () => {
      await Promise.resolve();
    });

    expect(spy).toHaveBeenCalledOnce();
    const [, , signal] = spy.mock.calls[0] as [unknown, unknown, AbortSignal | undefined];
    expect(signal).toBeInstanceOf(AbortSignal);
  });

  it("cancel() aborts the request and sets loading to false", async () => {
    const client = makeClient();
    // Never resolve — simulates a slow in-flight request
    vi.spyOn(api, "searchFlights").mockReturnValue(new Promise(() => {}));

    const { result } = renderHook(() => useOnlineBoard(baseParams), {
      wrapper: makeWrapper(client),
    });

    // Should be loading initially
    expect(result.current.loading).toBe(true);

    act(() => {
      result.current.cancel();
    });

    expect(result.current.loading).toBe(false);
  });

  it("new param change aborts the previous in-flight request (§4.1.12)", async () => {
    const client = makeClient();
    const capturedSignals: AbortSignal[] = [];

    vi.spyOn(api, "searchFlights").mockImplementation((_c, _p, signal) => {
      if (signal) capturedSignals.push(signal);
      return new Promise(() => {}); // Never resolves
    });

    const { rerender } = renderHook(
      (params) => useOnlineBoard(params),
      {
        initialProps: baseParams,
        wrapper: makeWrapper(client),
      },
    );

    // Wait for first effect
    await act(async () => { await Promise.resolve(); });
    expect(capturedSignals).toHaveLength(1);

    // Change params — triggers new effect which aborts the old controller
    rerender({ ...baseParams, departure: "LED" });
    await act(async () => { await Promise.resolve(); });

    // First signal should now be aborted
    expect(capturedSignals[0]?.aborted).toBe(true);
    // Second request was started with a fresh (non-aborted) controller
    expect(capturedSignals).toHaveLength(2);
    expect(capturedSignals[1]?.aborted).toBe(false);
  });
});
