import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
  SignalRConnection,
  getSharedConnection,
  _resetSharedConnections,
  type ConnectionStatus,
} from "./connection.js";

// ---- helpers ----

function createMockHub() {
  const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
  let onCloseCallback: ((error?: Error) => void) | null = null;
  let onReconnectingCallback: ((error?: Error) => void) | null = null;
  let onReconnectedCallback: ((connectionId?: string) => void) | null = null;

  const hub = {
    start: vi.fn().mockResolvedValue(undefined),
    stop: vi.fn().mockResolvedValue(undefined),
    invoke: vi.fn().mockResolvedValue(undefined),
    on: vi.fn((method: string, handler: (...args: unknown[]) => void) => {
      const list = handlers[method] ?? [];
      handlers[method] = list;
      list.push(handler);
    }),
    off: vi.fn((method: string, handler: (...args: unknown[]) => void) => {
      const list = handlers[method];
      if (list) {
        const idx = list.indexOf(handler);
        if (idx >= 0) list.splice(idx, 1);
      }
    }),
    onclose: vi.fn((cb: (error?: Error) => void) => {
      onCloseCallback = cb;
    }),
    onreconnecting: vi.fn((cb: (error?: Error) => void) => {
      onReconnectingCallback = cb;
    }),
    onreconnected: vi.fn((cb: (connectionId?: string) => void) => {
      onReconnectedCallback = cb;
    }),
    // Test helpers to simulate events
    _simulateMessage(channel: string, ...args: unknown[]) {
      for (const h of handlers[channel] ?? []) h(...args);
    },
    _simulateClose(error?: Error) {
      onCloseCallback?.(error);
    },
    _simulateReconnecting(error?: Error) {
      onReconnectingCallback?.(error);
    },
    _simulateReconnected(connectionId?: string) {
      onReconnectedCallback?.(connectionId);
    },
  };

  return hub;
}

function createConnection(
  hub: ReturnType<typeof createMockHub>,
  gracePeriodMs = 5000,
): SignalRConnection {
  const conn = new SignalRConnection({
    hubUrl: "https://hub.test/flights",
    gracePeriodMs,
  });
  conn._buildConnection = vi.fn().mockResolvedValue(hub);
  return conn;
}

// ---- tests ----

describe("SignalRConnection", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    _resetSharedConnections();
  });

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

  it("two rapid subscribes produce exactly one start() call", async () => {
    const hub = createMockHub();
    const conn = createConnection(hub);

    const handler1 = vi.fn();
    const handler2 = vi.fn();

    conn.subscribe("flights", handler1);
    conn.subscribe("flights", handler2);

    // Let the async build + start resolve
    await vi.runAllTimersAsync();

    expect(hub.start).toHaveBeenCalledTimes(1);
  });

  it("unmount + remount within grace period reuses connection", async () => {
    const hub = createMockHub();
    const conn = createConnection(hub, 5000);

    const handler1 = vi.fn();
    const unsub1 = conn.subscribe("flights", handler1);
    await vi.runAllTimersAsync();

    expect(hub.start).toHaveBeenCalledTimes(1);

    // Unsubscribe (starts grace timer)
    unsub1();

    // Advance time partially (within grace period)
    vi.advanceTimersByTime(2000);

    // Remount -- subscribe again
    const handler2 = vi.fn();
    conn.subscribe("flights", handler2);
    await vi.runAllTimersAsync();

    // Still only one start() -- connection was reused
    expect(hub.start).toHaveBeenCalledTimes(1);
    expect(hub.stop).not.toHaveBeenCalled();
  });

  it("unmount + remount after grace period creates fresh connection", async () => {
    const hub1 = createMockHub();
    const hub2 = createMockHub();
    let buildCallCount = 0;

    const conn = new SignalRConnection({
      hubUrl: "https://hub.test/flights",
      gracePeriodMs: 100,
    });
    conn._buildConnection = vi.fn().mockImplementation(async () => {
      buildCallCount++;
      return buildCallCount === 1 ? hub1 : hub2;
    });

    const handler1 = vi.fn();
    const unsub1 = conn.subscribe("flights", handler1);
    await vi.runAllTimersAsync();

    expect(hub1.start).toHaveBeenCalledTimes(1);

    // Unsubscribe
    unsub1();

    // Advance past grace period (100ms) so connection stops
    await vi.advanceTimersByTimeAsync(200);

    expect(hub1.stop).toHaveBeenCalledTimes(1);

    // Remount -- subscribe again, should create fresh connection
    const handler2 = vi.fn();
    conn.subscribe("flights", handler2);
    await vi.runAllTimersAsync();

    expect(hub2.start).toHaveBeenCalledTimes(1);
    expect(buildCallCount).toBe(2);
  });

  it("status transitions fire onStatusChange handlers", async () => {
    const hub = createMockHub();
    const conn = createConnection(hub);

    const statuses: ConnectionStatus[] = [];
    conn.onStatusChange((s) => statuses.push(s));

    expect(conn.status).toBe("idle");

    conn.subscribe("flights", vi.fn());
    await vi.runAllTimersAsync();

    expect(statuses).toContain("connecting");
    expect(statuses).toContain("live");
    expect(conn.status).toBe("live");

    // Simulate reconnecting
    hub._simulateReconnecting();
    expect(conn.status).toBe("reconnecting");
    expect(statuses).toContain("reconnecting");

    // Simulate reconnected
    hub._simulateReconnected("abc");
    expect(conn.status).toBe("live");

    // Simulate close
    hub._simulateClose();
    expect(conn.status).toBe("offline");
    expect(statuses).toContain("offline");
  });

  it("onStatusChange unsubscribe stops notifications", async () => {
    const hub = createMockHub();
    const conn = createConnection(hub);

    const statuses: ConnectionStatus[] = [];
    const unsub = conn.onStatusChange((s) => statuses.push(s));

    conn.subscribe("flights", vi.fn());
    await vi.runAllTimersAsync();

    unsub();
    hub._simulateClose();

    // Should not have "offline" since we unsubscribed
    expect(statuses).not.toContain("offline");
  });

  it("unsubscribe is idempotent", async () => {
    const hub = createMockHub();
    const conn = createConnection(hub, 100);

    const unsub = conn.subscribe("flights", vi.fn());
    await vi.runAllTimersAsync();

    unsub();
    unsub(); // Second call should be a no-op

    // Grace timer should only be started once
    await vi.advanceTimersByTimeAsync(200);
    expect(hub.stop).toHaveBeenCalledTimes(1);
  });

  it("destroy tears down immediately", async () => {
    const hub = createMockHub();
    const conn = createConnection(hub);

    conn.subscribe("flights", vi.fn());
    await vi.runAllTimersAsync();

    await conn.destroy();
    expect(hub.stop).toHaveBeenCalledTimes(1);
    expect(conn.status).toBe("idle");
  });

  it("invokes hub methods after the lazy connection starts", async () => {
    const hub = createMockHub();
    const conn = createConnection(hub);

    await conn.invoke("SubscribeDate", "20260506", "MOW", "LED");
    await vi.runAllTimersAsync();

    expect(hub.start).toHaveBeenCalledTimes(1);
    expect(hub.invoke).toHaveBeenCalledWith(
      "SubscribeDate",
      "20260506",
      "MOW",
      "LED",
    );
  });
});

describe("getSharedConnection", () => {
  beforeEach(() => {
    _resetSharedConnections();
  });

  it("returns same instance for same hubUrl", () => {
    const a = getSharedConnection({ hubUrl: "https://hub.test/a" });
    const b = getSharedConnection({ hubUrl: "https://hub.test/a" });
    expect(a).toBe(b);
  });

  it("returns different instances for different hubUrls", () => {
    const a = getSharedConnection({ hubUrl: "https://hub.test/a" });
    const b = getSharedConnection({ hubUrl: "https://hub.test/b" });
    expect(a).not.toBe(b);
  });
});
