import { renderHook, act } from "@testing-library/react-hooks";
import { BehaviorSubject } from "rxjs";
import { useLoadingState } from "../useLoadingState";

jest.mock(
  "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue",
  () => ({
    useRefWithInitialValue: jest.fn((initializer) => ({
      current: initializer(),
    })),
  })
);

describe("useLoadingState", () => {
  let onLoadDone: jest.Mock;

  beforeEach(() => {
    onLoadDone = jest.fn();
    jest.clearAllMocks();
  });

  describe("initialization", () => {
    it("should initialize with correct default state for zero components", () => {
      const { result } = renderHook(() => useLoadingState(0, onLoadDone));

      const initialState = result.current.loadingState.getValue();

      expect(initialState).toEqual({
        index: -1,
        done: true,
        waitForAllComponents: false,
      });

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

    it("should initialize with correct default state for multiple components", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      const initialState = result.current.loadingState.getValue();

      expect(initialState).toEqual({
        index: -1,
        done: false,
        waitForAllComponents: false,
      });

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

    it("should return a BehaviorSubject for loadingState", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      expect(result.current.loadingState).toBeInstanceOf(BehaviorSubject);
    });
  });

  describe("arePreviousComponentsLoaded", () => {
    it("should return true for index 0 (first component)", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      expect(result.current.arePreviousComponentsLoaded(0)).toBe(true);
    });

    it("should return false when previous components are not loaded", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      expect(result.current.arePreviousComponentsLoaded(1)).toBe(false);
      expect(result.current.arePreviousComponentsLoaded(2)).toBe(false);
    });

    it("should return true when all previous components are loaded", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      act(() => {
        result.current.onLoadFinished(0);
      });

      expect(result.current.arePreviousComponentsLoaded(1)).toBe(true);
      expect(result.current.arePreviousComponentsLoaded(2)).toBe(false);

      act(() => {
        result.current.onLoadFinished(1);
      });

      expect(result.current.arePreviousComponentsLoaded(2)).toBe(true);
    });
  });

  describe("onLoadFinished", () => {
    it("should update component state and loading state when component finishes loading", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      act(() => {
        result.current.onLoadFinished(0);
      });

      const state = result.current.loadingState.getValue();
      expect(state.index).toBe(0);
      expect(state.done).toBe(false);
    });

    it("should update index to highest loaded component", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      act(() => {
        result.current.onLoadFinished(2);
      });

      let state = result.current.loadingState.getValue();
      expect(state.index).toBe(2);

      act(() => {
        result.current.onLoadFinished(1);
      });

      state = result.current.loadingState.getValue();
      expect(state.index).toBe(2); // Should remain 2, not decrease to 1
    });

    it("should mark as done when all components are loaded", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      act(() => {
        result.current.onLoadFinished(0);
      });

      let state = result.current.loadingState.getValue();
      expect(state.done).toBe(true); // True because arePreviousComponentsLoaded(1) returns true when component 0 is loaded
      expect(onLoadDone).toHaveBeenCalledTimes(1);

      act(() => {
        result.current.onLoadFinished(1);
      });

      state = result.current.loadingState.getValue();
      expect(state.done).toBe(true);
      expect(onLoadDone).toHaveBeenCalledTimes(2); // Called again
    });

    it("should call onLoadDone when count is 0", () => {
      const { result } = renderHook(() => useLoadingState(0, onLoadDone));

      const state = result.current.loadingState.getValue();
      expect(state.done).toBe(true);
      // onLoadDone is not called on initialization for count 0, only when all components are loaded via dispatch
    });

    it("should handle loading components out of order", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      // Load component 2 first
      act(() => {
        result.current.onLoadFinished(2);
      });

      let state = result.current.loadingState.getValue();
      expect(state.done).toBe(false);

      // Load component 0
      act(() => {
        result.current.onLoadFinished(0);
      });

      state = result.current.loadingState.getValue();
      expect(state.done).toBe(false);

      // Load component 1 - should complete loading
      act(() => {
        result.current.onLoadFinished(1);
      });

      state = result.current.loadingState.getValue();
      expect(state.done).toBe(true);
      expect(onLoadDone).toHaveBeenCalledTimes(1);
    });

    it("should call onLoadDone again on subsequent dispatches", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      act(() => {
        result.current.onLoadFinished(0);
        result.current.onLoadFinished(1);
      });

      expect(onLoadDone).toHaveBeenCalledTimes(2); // Called for each dispatch when done is true

      // Try loading again - onLoadDone will be called again because dispatch runs again
      act(() => {
        result.current.onLoadFinished(0);
      });

      expect(onLoadDone).toHaveBeenCalledTimes(3); // Will be called again
    });
  });

  describe("onLoadFailed", () => {
    it("should treat failed components as loaded when SHOULD_FAIL_ON_COMPONENT_LOADING is false", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      const error = new Error("Load failed");

      act(() => {
        result.current.onLoadFailed({ error, index: 0 });
      });

      const state = result.current.loadingState.getValue();
      expect(state.index).toBe(0);
      expect(result.current.shouldShowLoadingError).toBe(false);
    });

    it("should complete loading when all components fail", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      const error = new Error("Load failed");

      act(() => {
        result.current.onLoadFailed({ error, index: 0 });
        result.current.onLoadFailed({ error, index: 1 });
      });

      const state = result.current.loadingState.getValue();
      expect(state.done).toBe(true);
      expect(onLoadDone).toHaveBeenCalledTimes(2); // Called for each failed component
    });

    it("should handle mixed success and failure", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone));

      const error = new Error("Load failed");

      act(() => {
        result.current.onLoadFinished(0);
        result.current.onLoadFailed({ error, index: 1 });
        result.current.onLoadFinished(2);
      });

      const state = result.current.loadingState.getValue();
      expect(state.done).toBe(true);
      expect(onLoadDone).toHaveBeenCalledTimes(2); // Called when all components 0,1,2 are handled
    });
  });

  describe("loading state observable", () => {
    it("should emit state changes through BehaviorSubject", () => {
      const { result } = renderHook(() => useLoadingState(3, onLoadDone)); // Use 3 components so loading component 0 doesn't complete everything
      const mockSubscriber = jest.fn();

      result.current.loadingState.subscribe(mockSubscriber);

      act(() => {
        result.current.onLoadFinished(0);
      });

      // Should have been called twice: initial state + update
      expect(mockSubscriber).toHaveBeenCalledTimes(2);

      expect(mockSubscriber).toHaveBeenLastCalledWith({
        index: 0,
        done: false, // Will be false because we need components 1 and 2 as well
        waitForAllComponents: false,
      });
    });

    it("should preserve waitForAllComponents flag in state updates", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      act(() => {
        result.current.onLoadFinished(0);
      });

      const state = result.current.loadingState.getValue();
      expect(state.waitForAllComponents).toBe(false);
    });
  });

  describe("memoization", () => {
    it("should return stable references for functions", () => {
      const { result, rerender } = renderHook(() =>
        useLoadingState(2, onLoadDone)
      );

      const firstRender = {
        onLoadFinished: result.current.onLoadFinished,
        onLoadFailed: result.current.onLoadFailed,
        arePreviousComponentsLoaded: result.current.arePreviousComponentsLoaded,
      };

      rerender();

      expect(result.current.onLoadFinished).toBe(firstRender.onLoadFinished);
      expect(result.current.onLoadFailed).toBe(firstRender.onLoadFailed);

      expect(result.current.arePreviousComponentsLoaded).toBe(
        firstRender.arePreviousComponentsLoaded
      );
    });

    it("should return stable function references (current behavior)", () => {
      const { result, rerender } = renderHook(
        ({ onLoadDone }) => useLoadingState(2, onLoadDone),
        { initialProps: { onLoadDone } }
      );

      const firstResult = result.current;

      const newOnLoadDone = jest.fn();
      rerender({ onLoadDone: newOnLoadDone });

      // Functions should remain the same due to empty dependency arrays (this is the current behavior)
      expect(result.current.onLoadFinished).toBe(firstResult.onLoadFinished);
      expect(result.current.onLoadFailed).toBe(firstResult.onLoadFailed);
    });
  });

  describe("edge cases", () => {
    it("should handle duplicate load finished calls gracefully", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      act(() => {
        result.current.onLoadFinished(0);
        result.current.onLoadFinished(0); // Duplicate call
      });

      const state = result.current.loadingState.getValue();
      expect(state.index).toBe(0);
      expect(state.done).toBe(true); // True because loading component 0 makes it done in a 2-component setup
    });

    it("should handle loading index greater than component count", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      act(() => {
        result.current.onLoadFinished(5); // Index out of bounds
      });

      const state = result.current.loadingState.getValue();
      expect(state.index).toBe(5);
      expect(state.done).toBe(false); // Should not be done as not all components loaded
    });

    it("should handle negative indices", () => {
      const { result } = renderHook(() => useLoadingState(2, onLoadDone));

      act(() => {
        result.current.onLoadFinished(-1);
      });

      const state = result.current.loadingState.getValue();
      expect(state.index).toBe(-1); // Should remain -1
    });
  });

  describe("component count changes", () => {
    it("should handle changing component count", () => {
      const { result, rerender } = renderHook(
        ({ count }) => useLoadingState(count, onLoadDone),
        { initialProps: { count: 2 } }
      );

      act(() => {
        result.current.onLoadFinished(0);
      });

      // Change count
      rerender({ count: 3 });

      // The hook should work with the new count
      act(() => {
        result.current.onLoadFinished(1);
        result.current.onLoadFinished(2);
      });

      const state = result.current.loadingState.getValue();
      expect(state.done).toBe(true);
    });
  });
});
