import { act, renderHook } from "@testing-library/react";
import { useComboboxOptions } from "./useComboboxOptions";

describe("useComboboxOptions", () => {
  beforeEach(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe("basic functionality", () => {
    it("should return all options when no input value", () => {
      const options = [
        { label: "Option 1", value: "1" },
        { label: "Option 2", value: "2" },
        { label: "Option 3", value: "3" },
      ];

      const { result } = renderHook(() =>
        useComboboxOptions({
          options,
        }),
      );

      expect(result.current).toStrictEqual({
        updateFilteredOptions: expect.any(Function),
        error: null,
        filteredOptions: options,
        isLoading: false,
        isError: false,
      });
    });

    it("should filter options based on input value", () => {
      const options = [
        { label: "Option 1", value: "1" },
        { label: "Option 2", value: "2" },
        { label: "Option 3", value: "3" },
      ];

      const { result } = renderHook(() =>
        useComboboxOptions({
          options,
          inputValue: "1",
        }),
      );

      expect(result.current).toStrictEqual({
        updateFilteredOptions: expect.any(Function),
        error: null,
        filteredOptions: [{ label: "Option 1", value: "1" }],
        isLoading: false,
        isError: false,
      });
    });

    it("should filter options case-insensitively", () => {
      const options = [
        { label: "Apple", value: "1" },
        { label: "Banana", value: "2" },
        { label: "Orange", value: "3" },
      ];

      const { result } = renderHook(() =>
        useComboboxOptions({
          options,
          inputValue: "aPp",
        }),
      );

      expect(result.current.filteredOptions).toStrictEqual([
        { label: "Apple", value: "1" },
      ]);
    });

    it.skip("should handle special characters in filter", () => {
      const options = [
        { label: "São Paulo", value: "1" },
        { label: "New York", value: "2" },
        { label: "München", value: "3" },
      ];

      const { result } = renderHook(() =>
        useComboboxOptions({
          options,
          inputValue: "sao",
        }),
      );

      expect(result.current.filteredOptions).toStrictEqual([
        { label: "São Paulo", value: "1" },
      ]);
    });
  });

  describe("async options stability", () => {
    it("should not re-fetch when asyncOptions reference changes but inputValue stays the same", async () => {
      const asyncOptionsV1 = vi
        .fn()
        .mockResolvedValue([{ label: "Result", value: "1" }]);
      const asyncOptionsV2 = vi
        .fn()
        .mockResolvedValue([{ label: "Result", value: "1" }]);

      const { rerender } = renderHook(
        ({ asyncOptions }) =>
          useComboboxOptions({
            asyncOptions,
            inputValue: "hello",
          }),
        { initialProps: { asyncOptions: asyncOptionsV1 } },
      );

      await act(() => Promise.resolve());

      expect(asyncOptionsV1).toHaveBeenCalledTimes(1);

      // Simulate parent re-render providing a new asyncOptions reference
      rerender({ asyncOptions: asyncOptionsV2 });

      await act(() => Promise.resolve());

      // Should NOT have triggered a new fetch since inputValue didn't change
      expect(asyncOptionsV2).not.toHaveBeenCalled();
    });
  });

  describe("async options handling", () => {
    it("should handle async options", async () => {
      const asyncOptions = vi.fn().mockResolvedValue([
        { label: "Async Option 1", value: "1" },
        { label: "Async Option 2", value: "2" },
      ]);

      const { result } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
        }),
      );

      // Wait for the async function to resolve
      await act(() => Promise.resolve());

      expect(asyncOptions).toHaveBeenCalledWith("Async", expect.any(Object));
      expect(result.current).toStrictEqual({
        updateFilteredOptions: expect.any(Function),
        error: null,
        filteredOptions: [
          { label: "Async Option 1", value: "1" },
          { label: "Async Option 2", value: "2" },
        ],
        isLoading: false,
        isError: false,
      });
    });

    it("should require a minimum input length for async options", () => {
      const asyncOptions = vi.fn().mockResolvedValue([
        { label: "Async Option 1", value: "1" },
        { label: "Async Option 2", value: "2" },
      ]);

      const { result } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "A",
          minSearchLength: 2,
        }),
      );

      expect(asyncOptions).not.toHaveBeenCalled();
      expect(result.current).toStrictEqual({
        updateFilteredOptions: expect.any(Function),
        error: null,
        filteredOptions: undefined,
        isLoading: false,
        isError: false,
      });
    });

    it("should debounce async options calls", async () => {
      const asyncOptions = vi.fn().mockResolvedValue([
        { label: "Async Option 1", value: "1" },
        { label: "Async Option 2", value: "2" },
      ]);

      const { rerender } = renderHook(
        ({ inputValue }) =>
          useComboboxOptions({
            asyncOptions,
            inputValue,
            delay: 200,
          }),
        { initialProps: { inputValue: "" } },
      );

      expect(asyncOptions).not.toHaveBeenCalled();

      rerender({ inputValue: "A" });
      rerender({ inputValue: "AS" });
      rerender({ inputValue: "ASY" });

      expect(asyncOptions).not.toHaveBeenCalled();

      await act(async () => {
        await vi.advanceTimersByTimeAsync(200);
      });

      expect(asyncOptions).toHaveBeenCalledTimes(1);
      expect(asyncOptions).toHaveBeenCalledWith("ASY", expect.any(Object));
    });

    it("should abort fetch on unmount", () => {
      const asyncOptions = vi.fn().mockImplementation(
        () =>
          new Promise<void>(resolve => {
            setTimeout(() => resolve(), 100);
          }),
      );

      const { unmount } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
        }),
      );

      unmount();

      expect(asyncOptions).toHaveBeenCalled();
      expect(asyncOptions.mock.calls[0][1].signal.aborted).toBe(true);
    });

    it("should ignore abort errors", async () => {
      const asyncOptions = vi.fn().mockImplementation(
        () =>
          new Promise((_, reject) => {
            setTimeout(
              () => reject(new DOMException("AbortError", "AbortError")),
              100,
            );
          }),
      );

      const { result } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
        }),
      );

      await act(async () => {
        await vi.runAllTimersAsync();
      });

      expect(result.current).toStrictEqual({
        updateFilteredOptions: expect.any(Function),
        error: null,
        filteredOptions: undefined,
        isLoading: false,
        isError: false,
      });
    });

    it("should call onSearched when async options have been fetched", async () => {
      const onSearched = vi.fn();
      const asyncOptions = vi.fn().mockResolvedValue([
        { label: "Async Option 1", value: "1" },
        { label: "Async Option 2", value: "2" },
      ]);

      renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
          onSearched,
        }),
      );

      // Wait for the async function to resolve
      await act(() => Promise.resolve());

      expect(onSearched).toHaveBeenCalledWith("Async");
    });

    it("should not call onSearched when input length is less than minSearchLength", async () => {
      const onSearched = vi.fn();
      const asyncOptions = vi.fn().mockResolvedValue([
        { label: "Async Option 1", value: "1" },
        { label: "Async Option 2", value: "2" },
      ]);

      renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "As",
          minSearchLength: 3,
          onSearched,
        }),
      );

      // Wait for the async function to resolve
      await act(() => Promise.resolve());

      expect(onSearched).not.toHaveBeenCalled();
    });

    it("should not call onSearched for synchronous options", async () => {
      const onSearched = vi.fn();
      const options = [
        { label: "Option 1", value: "1" },
        { label: "Option 2", value: "2" },
      ];

      renderHook(() =>
        useComboboxOptions({
          options,
          inputValue: "Option",
          onSearched,
        }),
      );

      // Wait for any potential async operations
      await act(() => Promise.resolve());

      expect(onSearched).not.toHaveBeenCalled();
    });
  });

  describe("loading states", () => {
    it("should set isLoading to true while fetching async options", async () => {
      const asyncOptions = vi.fn().mockImplementation(
        () =>
          new Promise(resolve => {
            setTimeout(
              () => resolve([{ label: "Async Option 1", value: "1" }]),
              100,
            );
          }),
      );

      const { result } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
        }),
      );

      expect(result.current.isLoading).toBe(true);

      await act(async () => {
        await vi.runAllTimersAsync();
      });

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

    it("should set isLoading to false after async options fetch success", async () => {
      const asyncOptions = vi
        .fn()
        .mockResolvedValue([{ label: "Async Option 1", value: "1" }]);

      const { result } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
        }),
      );

      expect(result.current.isLoading).toBe(true);

      await act(async () => {
        await vi.runAllTimersAsync();
      });

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

    it("should set isLoading to false after async options fetch error", async () => {
      const asyncOptions = vi.fn().mockImplementation(
        () =>
          new Promise((_, reject) => {
            setTimeout(() => reject(new Error("Fetch error")), 100);
          }),
      );

      const { result } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
        }),
      );

      expect(result.current.isLoading).toBe(true);

      await act(async () => {
        await vi.runAllTimersAsync();
      });

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

  describe("error states", () => {
    it("should set error state on async options fetch error", async () => {
      const asyncOptions = vi.fn().mockImplementation(
        () =>
          new Promise((_, reject) => {
            setTimeout(() => reject(new Error("Fetch error")), 100);
          }),
      );

      const { result } = renderHook(() =>
        useComboboxOptions({
          asyncOptions,
          inputValue: "Async",
        }),
      );

      await act(async () => {
        await vi.runAllTimersAsync();
      });

      expect(result.current).toStrictEqual({
        updateFilteredOptions: expect.any(Function),
        error: new Error("Fetch error"),
        filteredOptions: undefined,
        isLoading: false,
        isError: true,
      });
    });

    it("should clear error state when input value changes", async () => {
      const asyncOptions = vi
        .fn()
        .mockRejectedValueOnce(new Error("Fetch error"))
        .mockResolvedValueOnce([{ label: "Success", value: "1" }]);

      const { result, rerender } = renderHook(
        ({ inputValue }) =>
          useComboboxOptions({
            asyncOptions,
            inputValue,
          }),
        { initialProps: { inputValue: "error" } },
      );

      await act(async () => {
        await vi.runAllTimersAsync();
      });

      expect(result.current.isError).toBe(true);

      rerender({ inputValue: "success" });

      await act(async () => {
        await vi.runAllTimersAsync();
      });

      expect(result.current.isError).toBe(false);
      expect(result.current.error).toBe(null);
    });
  });

  it("should reset the error state on async options fetch start", async () => {
    const asyncOptions = vi.fn().mockImplementation(
      () =>
        new Promise((_, reject) => {
          setTimeout(() => reject(new Error("Fetch error")), 100);
        }),
    );

    const { result } = renderHook(() =>
      useComboboxOptions({
        asyncOptions,
        inputValue: "Async",
      }),
    );

    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(result.current).toStrictEqual({
      updateFilteredOptions: expect.any(Function),
      error: new Error("Fetch error"),
      filteredOptions: undefined,
      isLoading: false,
      isError: true,
    });
  });

  it("should reset the error state on successful async options fetch", async () => {
    const asyncOptions = vi
      .fn()
      .mockResolvedValue([{ label: "Async Option 1", value: "1" }]);

    const { result } = renderHook(() =>
      useComboboxOptions({
        asyncOptions,
        inputValue: "Async",
      }),
    );

    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(result.current).toStrictEqual({
      updateFilteredOptions: expect.any(Function),
      error: null,
      filteredOptions: [{ label: "Async Option 1", value: "1" }],
      isLoading: false,
      isError: false,
    });
  });
});
