import { describe, it, expect, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useListData } from "../useListData";
import type { FetchError } from "../../types/api";

describe("useListData", () => {
  it("loads items and updates loading", async () => {
    const fetcher = vi.fn().mockResolvedValue({ items: ["one", "two"] });
    const selector = vi.fn((response: { items: string[] }) => response.items);

    const { result } = renderHook(() =>
      useListData({
        fetcher,
        selector,
        errorMessage: "Failed to load",
      })
    );

    expect(result.current.loading).toBe(true);
    expect(result.current.items).toEqual([]);

    await act(async () => {
      await Promise.resolve();
    });

    expect(fetcher).toHaveBeenCalledTimes(1);
    expect(selector).toHaveBeenCalledWith({ items: ["one", "two"] });
    expect(result.current.items).toEqual(["one", "two"]);
    expect(result.current.loading).toBe(false);
    expect(result.current.error).toBeNull();
  });

  it("sets typed error when the fetcher rejects", async () => {
    const fetcher = vi.fn().mockRejectedValue(new Error("network"));
    const selector = vi.fn();

    const { result } = renderHook(() =>
      useListData({
        fetcher,
        selector,
        errorMessage: "Failed to load",
      })
    );

    await act(async () => {
      await Promise.resolve();
    });

    const error = result.current.error as FetchError | null;
    expect(error).not.toBeNull();
    expect(error?.type).toBe("network_error");
    expect(error?.message).toBe("network");
    expect(result.current.loading).toBe(false);
    expect(result.current.items).toEqual([]);
  });

  it("refresh triggers a second fetch", async () => {
    const fetcher = vi.fn()
      .mockResolvedValueOnce({ items: ["first"] })
      .mockResolvedValueOnce({ items: ["second"] });
    const selector = vi.fn((response: { items: string[] }) => response.items);

    const { result } = renderHook(() =>
      useListData({
        fetcher,
        selector,
        errorMessage: "Failed to load",
      })
    );

    await act(async () => {
      await Promise.resolve();
    });

    expect(result.current.items).toEqual(["first"]);
    await act(async () => {
      result.current.refresh();
      await Promise.resolve();
    });

    expect(fetcher).toHaveBeenCalledTimes(2);
    expect(result.current.items).toEqual(["second"]);
  });

  it("does not update state when signal is aborted before resolution", async () => {
    const abortedSignals: AbortSignal[] = [];
    const fetcher = vi.fn().mockImplementation(async (signal: AbortSignal) => {
      abortedSignals.push(signal);
      // Simulate a delay
      await new Promise((resolve) => setTimeout(resolve, 50));
      return { items: ["late"] };
    });
    const selector = vi.fn((response: { items: string[] }) => response.items);

    const { result, unmount } = renderHook(() =>
      useListData({
        fetcher,
        selector,
        errorMessage: "Failed to load",
      })
    );

    // Unmount immediately to trigger abort
    await act(async () => {
      await Promise.resolve();
    });
    unmount();

    // Verify at least one signal was collected and it was aborted
    expect(abortedSignals.length).toBeGreaterThan(0);
    const lastSignal = abortedSignals[abortedSignals.length - 1];
    expect(lastSignal != null && lastSignal.aborted).toBe(true);

    // Items should not be updated after abort
    expect(result.current.items).toEqual([]);
  });

  it("silently handles abort errors", async () => {
    const fetcher = vi.fn().mockRejectedValue(new DOMException("Aborted", "AbortError"));
    const selector = vi.fn();

    const { result } = renderHook(() =>
      useListData({
        fetcher,
        selector,
        errorMessage: "Failed to load",
      })
    );

    await act(async () => {
      await Promise.resolve();
    });

    // Abort errors should not set error state
    expect(result.current.error).toBeNull();
  });

  it("exposes typed API error information", async () => {
    // Mock an API error response
    const fetcher = vi.fn().mockImplementation(() => {
      return Promise.reject((() => {
        const error = new Error("API error 500: Server error");
        (error as any).name = "ApiError";
        (error as any).status = 500;
        (error as any).body = "Server error";
        return error;
      })());
    });
    const selector = vi.fn();

    const { result } = renderHook(() =>
      useListData({
        fetcher,
        selector,
        errorMessage: "Failed to load",
      })
    );

    await act(async () => {
      await Promise.resolve();
    });

    const error = result.current.error as FetchError | null;
    expect(error).not.toBeNull();
    expect(error?.type).toBe("api_error");
    if (error?.type === "api_error") {
      expect(error.status).toBe(500);
      expect(error.body).toBe("Server error");
    }
  });
});
