/**
 * Tests for useDraftSocket reconnection logic.
 *
 * Covers the Disconnect → State-Change-in-DB → Reconnect flow and ensures
 * reconnectCount (which the draft route uses to trigger revalidate()) behaves
 * correctly across reconnects, visibility changes, and network flapping.
 *
 * We mock socket.io-client so the tests run in jsdom with no real network.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";

// ---------------------------------------------------------------------------
// socket.io-client mock
// ---------------------------------------------------------------------------

type EventHandler = (...args: any[]) => void;

/**
 * Minimal, controllable EventEmitter that mimics the socket.io-client Socket
 * and its Manager (socket.io) interface.
 */
function makeSocketMock() {
  const socketHandlers: Record<string, EventHandler[]> = {};
  const managerHandlers: Record<string, EventHandler[]> = {};

  const socketMock = {
    connected: false,
    id: "socket-test-id",
    on: vi.fn((event: string, handler: EventHandler) => {
      socketHandlers[event] ??= [];
      socketHandlers[event].push(handler);
    }),
    off: vi.fn((event: string, handler?: EventHandler) => {
      if (!handler) {
        socketHandlers[event] = [];
      } else {
        socketHandlers[event] = (socketHandlers[event] ?? []).filter(
          (h) => h !== handler
        );
      }
    }),
    emit: vi.fn(),
    connect: vi.fn(() => {
      socketMock.connected = true;
      // Simulate the "connect" event firing on the next tick
      setTimeout(() => {
        socketMock._trigger("connect");
      }, 0);
    }),
    disconnect: vi.fn(() => {
      socketMock.connected = false;
    }),
    // Manager-level events (socket.io.on / socket.io.off)
    io: {
      on: vi.fn((event: string, handler: EventHandler) => {
        managerHandlers[event] ??= [];
        managerHandlers[event].push(handler);
      }),
      off: vi.fn((event: string, handler?: EventHandler) => {
        if (!handler) {
          managerHandlers[event] = [];
        } else {
          managerHandlers[event] = (managerHandlers[event] ?? []).filter(
            (h) => h !== handler
          );
        }
      }),
    },

    // Test helpers — call these to simulate server-side events
    _trigger(event: string, ...args: any[]) {
      (socketHandlers[event] ?? []).forEach((h) => h(...args));
    },
    _triggerManager(event: string, ...args: any[]) {
      (managerHandlers[event] ?? []).forEach((h) => h(...args));
    },
  };

  return socketMock;
}

type SocketMock = ReturnType<typeof makeSocketMock>;
let socketMock: SocketMock;

vi.mock("socket.io-client", () => ({
  io: vi.fn(() => socketMock),
}));

// ---------------------------------------------------------------------------
// Import the hook under test (after mocking)
// ---------------------------------------------------------------------------

import { useDraftSocket } from "../useDraftSocket";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function renderDraftSocket(seasonId = "season-1", teamId?: string) {
  return renderHook(() => useDraftSocket(seasonId, teamId));
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("useDraftSocket – reconnection", () => {
  beforeEach(() => {
    socketMock = makeSocketMock();
    vi.useFakeTimers();
  });

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

  // -------------------------------------------------------------------------
  // Basic connection
  // -------------------------------------------------------------------------

  it("starts disconnected and connects on mount", async () => {
    const { result } = renderDraftSocket();

    expect(result.current.isConnected).toBe(false);

    // Simulate the socket emitting "connect"
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });

    expect(result.current.isConnected).toBe(true);
    expect(result.current.reconnectCount).toBe(0);
    expect(result.current.isReconnecting).toBe(false);
  });

  // -------------------------------------------------------------------------
  // reconnectCount increments on true socket reconnect
  // -------------------------------------------------------------------------

  it("increments reconnectCount on socket reconnect (not initial connect)", async () => {
    const { result } = renderDraftSocket();

    // First connection — reconnectCount must stay 0
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });
    expect(result.current.reconnectCount).toBe(0);

    // Server-initiated disconnect
    act(() => {
      socketMock.connected = false;
      socketMock._trigger("disconnect", "transport error");
    });
    expect(result.current.isConnected).toBe(false);
    expect(result.current.isReconnecting).toBe(true);

    // Reconnect (second call to "connect" on the same socket instance)
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });

    expect(result.current.isConnected).toBe(true);
    expect(result.current.reconnectCount).toBe(1);
  });

  // -------------------------------------------------------------------------
  // Visibility-change triggers reconnect counter when socket is alive
  // -------------------------------------------------------------------------

  it("increments reconnectCount on visibilitychange when socket is already connected", () => {
    const { result } = renderDraftSocket("season-1", "team-1");

    // Initial connect
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });
    expect(result.current.reconnectCount).toBe(0);

    // App comes back to foreground while socket is still alive
    act(() => {
      Object.defineProperty(document, "visibilityState", {
        value: "visible",
        configurable: true,
      });
      document.dispatchEvent(new Event("visibilitychange"));
    });

    expect(result.current.reconnectCount).toBe(1);
    // join-draft re-emitted so the server knows we're back
    expect(socketMock.emit).toHaveBeenCalledWith("join-draft", "season-1", "team-1");
  });

  it("triggers socket.connect() on visibilitychange when socket is disconnected", () => {
    const { result } = renderDraftSocket();

    // Connect then lose the connection
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });
    act(() => {
      socketMock.connected = false;
      socketMock._trigger("disconnect", "transport close");
    });

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

    // Tab becomes visible — hook should call socket.connect()
    act(() => {
      Object.defineProperty(document, "visibilityState", {
        value: "visible",
        configurable: true,
      });
      document.dispatchEvent(new Event("visibilitychange"));
    });

    expect(socketMock.connect).toHaveBeenCalled();
  });

  // -------------------------------------------------------------------------
  // Network flapping — multiple rapid reconnects in succession
  // -------------------------------------------------------------------------

  it("correctly counts reconnects during network flapping (5 rapid cycles)", () => {
    const { result } = renderDraftSocket();

    // Initial connect
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });
    expect(result.current.reconnectCount).toBe(0);

    const FLAP_CYCLES = 5;
    for (let i = 0; i < FLAP_CYCLES; i++) {
      act(() => {
        socketMock.connected = false;
        socketMock._trigger("disconnect", "transport error");
      });
      act(() => {
        socketMock.connected = true;
        socketMock._trigger("connect");
      });
    }

    // Each reconnect (not the initial connect) increments the counter
    expect(result.current.reconnectCount).toBe(FLAP_CYCLES);
    expect(result.current.isConnected).toBe(true);
    expect(result.current.isReconnecting).toBe(false);
  });

  it("does not set a permanent error state during transient network flapping", () => {
    const { result } = renderDraftSocket();

    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });

    // Flap 3 times
    for (let i = 0; i < 3; i++) {
      act(() => {
        socketMock.connected = false;
        socketMock._trigger("disconnect", "transport error");
      });
      act(() => {
        socketMock.connected = true;
        socketMock._trigger("connect");
      });
    }

    expect(result.current.connectionError).toBeNull();
    expect(result.current.isReconnecting).toBe(false);
  });

  // -------------------------------------------------------------------------
  // Disconnect → DB state change → Reconnect simulation
  //
  // This is the primary regression test for the stale-state bug.
  // The hook's responsibility is to increment reconnectCount so that the
  // draft route's useEffect calls revalidate(). The actual state merge
  // (picks + currentPick sync) lives in the route component and is tested
  // via the sync effect in the route; here we verify the contract the hook
  // fulfils.
  // -------------------------------------------------------------------------

  it("emits join-draft on every reconnect so the server re-sends presence data", () => {
    const { result } = renderDraftSocket("season-42", "team-7");

    // Initial connect
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });

    const joinCallsBefore = socketMock.emit.mock.calls.filter(
      ([ev]) => ev === "join-draft"
    ).length;

    // Simulate DB picks being added while client is disconnected
    act(() => {
      socketMock.connected = false;
      socketMock._trigger("disconnect", "ping timeout");
    });

    // Client reconnects — the hook must re-emit join-draft
    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });

    const joinCallsAfter = socketMock.emit.mock.calls.filter(
      ([ev]) => ev === "join-draft"
    ).length;

    expect(joinCallsAfter).toBe(joinCallsBefore + 1);
    expect(result.current.reconnectCount).toBe(1);
  });

  // -------------------------------------------------------------------------
  // Error handling — exhausted reconnect attempts
  // -------------------------------------------------------------------------

  it("sets connectionError and clears isReconnecting on reconnect_failed", () => {
    const { result } = renderDraftSocket();

    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });
    act(() => {
      socketMock.connected = false;
      socketMock._trigger("disconnect", "transport error");
    });

    // All reconnect attempts exhausted
    act(() => {
      socketMock._triggerManager("reconnect_failed");
    });

    expect(result.current.connectionError).toBeTruthy();
    expect(result.current.isReconnecting).toBe(false);
  });

  // -------------------------------------------------------------------------
  // Cleanup — no listeners leak after unmount
  // -------------------------------------------------------------------------

  it("emits leave-draft and disconnects on unmount", () => {
    const { unmount } = renderDraftSocket("season-1", "team-1");

    act(() => {
      socketMock.connected = true;
      socketMock._trigger("connect");
    });

    unmount();

    expect(socketMock.emit).toHaveBeenCalledWith("leave-draft", "season-1");
    expect(socketMock.disconnect).toHaveBeenCalled();
  });
});
