import { act } from "@testing-library/react";
import { Socket } from "@hyper-fetch/sockets";
import { createWebsocketMockingServer } from "@hyper-fetch/testing";

import { renderUseSocketState } from "../../../utils/use-socket-state.utils";

describe("useSocketState [ Base ]", () => {
  const spy = vi.fn();
  const { url, startServer, stopServer } = createWebsocketMockingServer();
  const socket = new Socket({ url });
  let view = renderUseSocketState(socket);

  beforeEach(async () => {
    view = renderUseSocketState(socket);
    vi.resetModules();
    vi.resetAllMocks();
    startServer();
  });

  afterEach(() => {
    stopServer();
  });

  describe("when using actions", () => {
    it("should set state with data", async () => {
      const value = 1;
      const [state, actions] = view.result.current;
      act(() => {
        actions.setData(value);
      });
      expect(state.data).toBe(value);
    });
    it("should set state with timestamp", async () => {
      const value = 1;
      const [state, actions] = view.result.current;
      act(() => {
        actions.setTimestamp(value);
      });
      expect(state.timestamp).toBe(value);
    });
    it("should set state with connected value", async () => {
      const value = true;
      const [state, actions] = view.result.current;
      act(() => {
        actions.setConnected(value);
      });
      expect(state.connected).toBe(value);
    });
    it("should set state with connecting value", async () => {
      const value = true;
      const [state, actions] = view.result.current;
      act(() => {
        actions.setConnecting(value);
      });
      expect(state.connecting).toBe(value);
    });
  });
  describe("when using callbacks", () => {
    it("should call onConnected callback", async () => {
      const [, , callbacks] = view.result.current;
      act(() => {
        socket.events.emitConnected();
        callbacks.onConnected(spy);
        socket.events.emitConnected();
      });
      expect(spy).toHaveBeenCalledTimes(1);
    });
    it("should call onDisconnected callback", async () => {
      const [, , callbacks] = view.result.current;
      act(() => {
        socket.events.emitDisconnected();
        callbacks.onDisconnected(spy);
        socket.events.emitDisconnected();
      });
      expect(spy).toHaveBeenCalledTimes(1);
    });
    it("should call onError callback", async () => {
      const value = {
        error: new Error("Test error"),
      };
      const [, , callbacks] = view.result.current;
      act(() => {
        socket.events.emitError(value);
        callbacks.onError(spy);
        socket.events.emitError(value);
      });
      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(value);
    });
    it("should call onConnecting callback", async () => {
      const [, , callbacks] = view.result.current;
      act(() => {
        socket.events.emitConnecting({ connecting: true });
        callbacks.onConnecting(spy);
        socket.events.emitConnecting({ connecting: true });
      });
      expect(spy).toHaveBeenCalledTimes(1);
    });
    it("should call onReconnecting callback", async () => {
      const value = 2;
      const [, , callbacks] = view.result.current;
      act(() => {
        socket.events.emitReconnecting({ attempts: value });
        callbacks.onReconnecting(spy);
        socket.events.emitReconnecting({ attempts: value });
      });
      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith({ attempts: value });
    });
    it("should call onReconnectingFailed callback", async () => {
      const value = 2;
      const [, , callbacks] = view.result.current;
      act(() => {
        socket.events.emitReconnectingFailed({ attempts: value });
        callbacks.onReconnectingFailed(spy);
        socket.events.emitReconnectingFailed({ attempts: value });
      });
      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith({ attempts: value });
    });
  });

  describe("when using dependency tracking", () => {
    it("should not trigger re-render when key is not tracked", async () => {
      const renderSpy = vi.fn();
      view = renderUseSocketState(socket, { onRender: renderSpy });
      const [, actions] = view.result.current;

      renderSpy.mockClear(); // Clear initial render count

      act(() => {
        actions.setData(123);
      });

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

    it("should trigger re-render when key is tracked", async () => {
      const renderSpy = vi.fn();
      view = renderUseSocketState(socket, { onRender: renderSpy });
      const [, actions, , { setRenderKey }] = view.result.current;

      act(() => {
        setRenderKey("data");
      });

      renderSpy.mockClear(); // Clear initial render count

      act(() => {
        actions.setData(123);
      });

      expect(renderSpy).toHaveBeenCalledTimes(1);
    });

    it("should not add duplicate render keys", async () => {
      const renderSpy = vi.fn();
      view = renderUseSocketState(socket, { onRender: renderSpy });
      const [, , , { setRenderKey }] = view.result.current;

      act(() => {
        setRenderKey("data");
        setRenderKey("data"); // Adding same key twice
      });

      renderSpy.mockClear(); // Clear initial render count

      act(() => {
        view.result.current[1].setData(123);
      });

      expect(renderSpy).toHaveBeenCalledTimes(1);
    });
  });

  describe("when using clearState", () => {
    it("should reset all state to initial values", async () => {
      view = renderUseSocketState(socket);
      const [, actions, , { setRenderKey }] = view.result.current;

      act(() => {
        setRenderKey("data");
        setRenderKey("extra");
        setRenderKey("connected");
        setRenderKey("connecting");
        setRenderKey("timestamp");
      });

      act(() => {
        actions.setData("some-data");
        actions.setExtra({ test: true } as any);
        actions.setConnected(true);
        actions.setConnecting(true);
        actions.setTimestamp(12345);
      });

      expect(view.result.current[0].data).toBe("some-data");
      expect(view.result.current[0].connected).toBe(true);
      expect(view.result.current[0].connecting).toBe(true);
      expect(view.result.current[0].timestamp).toBe(12345);

      act(() => {
        view.result.current[1].clearState();
      });

      expect(view.result.current[0].data).toBe(null);
      expect(view.result.current[0].extra).toBe(null);
      expect(view.result.current[0].connected).toBe(false);
      expect(view.result.current[0].connecting).toBe(false);
      expect(view.result.current[0].timestamp).toBe(null);
    });
  });
});
