// useWebSocket Hook 单测
// 依据：02-architecture-design.md §5 实时推送（P5）
// 覆盖：连接生命周期 / 重连指数退避 / 轮询降级 / 事件分发 / 清理 / token 缺失 / enabled 开关

import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
  afterEach,
  beforeEach,
  vi,
} from "vitest";
import { renderHook, cleanup, act } from "@testing-library/react";
import type { WebSocketEvent } from "@/types";

// ===== 在模块导入前设置环境变量 =====
// vi.hoisted 在所有 import 之前执行
vi.hoisted(() => {
  process.env.NEXT_PUBLIC_PUSH_GATEWAY_WS_URL = "ws://localhost:8081/ws";
});

// ===== Mock getToken =====
// vi.mock 被提升到文件顶部，必须用 vi.hoisted 定义变量
const { mockGetToken } = vi.hoisted(() => ({
  mockGetToken: vi.fn<() => string | null>(() => "mock-access-token"),
}));
vi.mock("@/lib/auth", () => ({
  getToken: mockGetToken,
}));

// ===== Mock WebSocket =====
interface MockWebSocketInstance {
  url: string;
  onopen: (() => void) | null;
  onmessage: ((event: { data: string }) => void) | null;
  onerror: (() => void) | null;
  onclose: (() => void) | null;
  readyState: number;
  close: () => void;
}

class MockWebSocketImpl implements MockWebSocketInstance {
  url: string;
  onopen: (() => void) | null = null;
  onmessage: ((event: { data: string }) => void) | null = null;
  onerror: (() => void) | null = null;
  onclose: (() => void) | null = null;
  readyState = 0;
  static instances: MockWebSocketImpl[] = [];
  static shouldThrow = false;

  constructor(url: string) {
    this.url = url;
    if (MockWebSocketImpl.shouldThrow) {
      MockWebSocketImpl.shouldThrow = false;
      throw new Error("WebSocket construction failed");
    }
    MockWebSocketImpl.instances.push(this);
  }
  close(): void {
    this.readyState = 3;
  }
  // 测试辅助方法
  fireOpen(): void {
    this.readyState = 1;
    this.onopen?.();
  }
  fireMessage(data: unknown): void {
    this.onmessage?.({
      data: typeof data === "string" ? data : JSON.stringify(data),
    });
  }
  fireError(): void {
    this.onerror?.();
  }
  fireClose(): void {
    this.readyState = 3;
    this.onclose?.();
  }
}

beforeAll(() => {
  // 注入全局 WebSocket mock
  (globalThis as unknown as { WebSocket: typeof MockWebSocketImpl }).WebSocket =
    MockWebSocketImpl;
});

afterAll(() => {
  // 恢复
  vi.restoreAllMocks();
});

import { useWebSocket } from "./useWebSocket";

beforeEach(() => {
  vi.useFakeTimers();
  mockGetToken.mockReturnValue("mock-access-token");
  MockWebSocketImpl.instances.length = 0;
  MockWebSocketImpl.shouldThrow = false;
});

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

describe("useWebSocket", () => {
  it("返回 status 和 reconnect 函数", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: false }),
    );
    expect(result.current).toHaveProperty("status");
    expect(result.current).toHaveProperty("reconnect");
    expect(typeof result.current.reconnect).toBe("function");
  });

  it("enabled=false 时进入轮询模式", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: false }),
    );
    expect(result.current.status).toBe("polling");
  });

  it("无 token 时 status 为 disconnected", () => {
    mockGetToken.mockReturnValue(null);
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );
    expect(result.current.status).toBe("disconnected");
  });

  it("有 token 时创建 WebSocket 并设置 status=connecting", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );
    expect(result.current.status).toBe("connecting");
    expect(MockWebSocketImpl.instances).toHaveLength(1);
    const ws = MockWebSocketImpl.instances[0]!;
    expect(ws.url).toContain("ws://localhost:8081/ws");
    expect(ws.url).toContain("token=mock-access-token");
  });

  it("ws.onopen 触发后 status=connected", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );
    expect(result.current.status).toBe("connecting");

    const ws = MockWebSocketImpl.instances[0]!;
    act(() => {
      ws.fireOpen();
    });

    expect(result.current.status).toBe("connected");
  });

  it("ws.onmessage 有效 JSON 时调用 onEvent", () => {
    const onEvent = vi.fn();
    renderHook(() => useWebSocket({ onEvent, enabled: true }));

    const ws = MockWebSocketImpl.instances[0]!;
    const event: WebSocketEvent = {
      type: "NotificationRequested",
      notification: {
        id: "notif-001",
        childId: "student-001",
        eventType: "grade_recorded",
        title: "test",
        body: "test body",
        read: false,
        createdAt: new Date().toISOString(),
      },
    };

    act(() => {
      ws.fireMessage(event);
    });

    expect(onEvent).toHaveBeenCalledWith(event);
  });

  it("ws.onmessage 无效 JSON 时静默忽略（不调用 onEvent）", () => {
    const onEvent = vi.fn();
    renderHook(() => useWebSocket({ onEvent, enabled: true }));

    const ws = MockWebSocketImpl.instances[0]!;
    act(() => {
      ws.onmessage?.({ data: "{invalid json" });
    });

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

  it("ws.onmessage 非字符串 data 时静默忽略", () => {
    const onEvent = vi.fn();
    renderHook(() => useWebSocket({ onEvent, enabled: true }));

    const ws = MockWebSocketImpl.instances[0]!;
    act(() => {
      // JSON.parse 接收非字符串会先 toString，可能解析失败
      ws.onmessage?.({ data: "undefined" });
    });

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

  it("ws.onerror 不立即改变 status", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );

    const ws = MockWebSocketImpl.instances[0]!;
    act(() => {
      ws.fireOpen();
    });
    expect(result.current.status).toBe("connected");

    act(() => {
      ws.fireError();
    });
    // onerror 不改变 status，等 onclose 处理
    expect(result.current.status).toBe("connected");
  });

  it("ws.onclose 触发重连（首次重连 attempt=0）", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );

    const ws = MockWebSocketImpl.instances[0]!;
    act(() => {
      ws.fireOpen();
    });
    expect(result.current.status).toBe("connected");

    act(() => {
      ws.fireClose();
    });
    expect(result.current.status).toBe("disconnected");

    // 等待重连定时器（getBackoffDelay(0) = 1000ms）
    act(() => {
      vi.advanceTimersByTime(1000);
    });

    // 重连后创建新 WebSocket
    expect(MockWebSocketImpl.instances).toHaveLength(2);
    expect(result.current.status).toBe("connecting");
  });

  it("重连指数退避：第二次重连延迟 2000ms", () => {
    renderHook(() => useWebSocket({ onEvent: vi.fn(), enabled: true }));

    // 第一次关闭 → 重连
    const ws1 = MockWebSocketImpl.instances[0]!;
    act(() => {
      ws1.fireClose();
    });
    act(() => {
      vi.advanceTimersByTime(1000); // getBackoffDelay(0) = 1000
    });
    expect(MockWebSocketImpl.instances).toHaveLength(2);

    // 第二次关闭 → 重连
    const ws2 = MockWebSocketImpl.instances[1]!;
    act(() => {
      ws2.fireClose();
    });

    // 在 1000ms 时不应重连（需要 2000ms）
    act(() => {
      vi.advanceTimersByTime(1000);
    });
    expect(MockWebSocketImpl.instances).toHaveLength(2);

    // 在 2000ms 时重连
    act(() => {
      vi.advanceTimersByTime(1000);
    });
    expect(MockWebSocketImpl.instances).toHaveLength(3);
  });

  it("ws.onopen 重置 reconnectAttempts 为 0", () => {
    renderHook(() => useWebSocket({ onEvent: vi.fn(), enabled: true }));

    // 第一次关闭 → 重连
    const ws1 = MockWebSocketImpl.instances[0]!;
    act(() => {
      ws1.fireClose();
    });
    act(() => {
      vi.advanceTimersByTime(1000);
    });

    // 第二个 WebSocket 连接成功
    const ws2 = MockWebSocketImpl.instances[1]!;
    act(() => {
      ws2.fireOpen();
    });

    // 再次关闭 → 应从 attempt=0 开始（延迟 1000ms 而非 2000ms）
    act(() => {
      ws2.fireClose();
    });
    act(() => {
      vi.advanceTimersByTime(1000);
    });

    // 1000ms 后应已重连
    expect(MockWebSocketImpl.instances).toHaveLength(3);
  });

  it("超过 MAX_RECONNECT_ATTEMPTS(10) 后降级为轮询", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );

    // 模拟 10 次关闭重连
    for (let i = 0; i < 10; i++) {
      const ws =
        MockWebSocketImpl.instances[MockWebSocketImpl.instances.length - 1]!;
      act(() => {
        ws.fireClose();
      });
      // 推进足够时间让重连触发
      act(() => {
        vi.advanceTimersByTime(30000);
      });
    }

    // 第 11 次关闭应触发轮询降级
    const lastWs =
      MockWebSocketImpl.instances[MockWebSocketImpl.instances.length - 1]!;
    act(() => {
      lastWs.fireClose();
    });

    expect(result.current.status).toBe("polling");
  });

  it("WebSocket 创建失败时降级为轮询", () => {
    MockWebSocketImpl.shouldThrow = true;
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );
    expect(result.current.status).toBe("polling");
  });

  it("reconnect 函数可手动触发重连", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );

    // 初始连接
    expect(MockWebSocketImpl.instances).toHaveLength(1);

    act(() => {
      result.current.reconnect();
    });

    // reconnect 调用 connect，创建新 WebSocket
    expect(MockWebSocketImpl.instances.length).toBeGreaterThanOrEqual(2);
  });

  it("onEvent 引用更新时 onEventRef 同步（useEffect）", () => {
    const onEvent1 = vi.fn();
    const onEvent2 = vi.fn();
    const { rerender } = renderHook(
      ({ onEvent }) => useWebSocket({ onEvent, enabled: true }),
      { initialProps: { onEvent: onEvent1 } },
    );

    const ws = MockWebSocketImpl.instances[0]!;
    const event: WebSocketEvent = {
      type: "ChildUnbound",
      childId: "student-001",
    };

    act(() => {
      ws.fireMessage(event);
    });
    expect(onEvent1).toHaveBeenCalledTimes(1);
    expect(onEvent2).not.toHaveBeenCalled();

    // 更新 onEvent 引用
    rerender({ onEvent: onEvent2 });

    act(() => {
      ws.fireMessage(event);
    });
    expect(onEvent1).toHaveBeenCalledTimes(1);
    expect(onEvent2).toHaveBeenCalledTimes(1);
  });

  it("卸载时关闭 WebSocket 并清理定时器", () => {
    const { unmount } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );

    const ws = MockWebSocketImpl.instances[0]!;
    expect(MockWebSocketImpl.instances).toHaveLength(1);

    unmount();

    // WebSocket 应被关闭（readyState 变为 3）
    // close() 是 mock 实现，readyState 设为 3
    expect(ws.readyState).toBe(3);
  });

  it("卸载后不触发状态更新（不抛出）", () => {
    const { unmount } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );

    unmount();

    // 推进时间不应抛出（清理后的定时器已清除）
    expect(() => {
      act(() => {
        vi.advanceTimersByTime(60000);
      });
    }).not.toThrow();
  });

  it("WebSocket URL 中 token 被 encodeURIComponent 编码", () => {
    mockGetToken.mockReturnValue("token with special chars");

    renderHook(() => useWebSocket({ onEvent: vi.fn(), enabled: true }));

    const ws = MockWebSocketImpl.instances[0]!;
    expect(ws.url).toContain("token=token%20with%20special%20chars");
  });

  it("轮询模式调用 fetch 拉取通知", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        data: {
          notifications: [
            {
              id: "poll-notif-001",
              childId: "student-001",
              eventType: "grade_recorded",
              title: "poll test",
              body: "polled notification",
              read: false,
              createdAt: new Date().toISOString(),
            },
          ],
        },
      }),
    });
    vi.stubGlobal("fetch", mockFetch);

    const onEvent = vi.fn();
    renderHook(() => useWebSocket({ onEvent, enabled: false }));

    // 轮询立即拉取一次
    await vi.waitFor(() => {
      expect(mockFetch).toHaveBeenCalled();
    });

    const callUrl = mockFetch.mock.calls[0]?.[0];
    expect(String(callUrl)).toContain(
      "/api/v1/parent/v1/notifications?since=true",
    );

    // 验证 Authorization header
    const callOpts = mockFetch.mock.calls[0]?.[1] as RequestInit | undefined;
    const headers = callOpts?.headers as Record<string, string> | undefined;
    expect(headers?.Authorization).toContain("Bearer");

    // 等待 onEvent 被调用（pollNotifications 是 async）
    await vi.waitFor(() => {
      expect(onEvent).toHaveBeenCalled();
    });

    const event = onEvent.mock.calls[0]?.[0] as WebSocketEvent;
    expect(event.type).toBe("NotificationRequested");

    vi.unstubAllGlobals();
  });

  it("轮询 fetch 失败时静默忽略", async () => {
    const mockFetch = vi.fn().mockRejectedValue(new Error("network error"));
    vi.stubGlobal("fetch", mockFetch);

    const onEvent = vi.fn();
    renderHook(() => useWebSocket({ onEvent, enabled: false }));

    await vi.waitFor(() => {
      expect(mockFetch).toHaveBeenCalled();
    });

    // 不应抛出，onEvent 不被调用
    await vi.waitFor(() => {
      expect(onEvent).not.toHaveBeenCalled();
    });

    vi.unstubAllGlobals();
  });

  it("轮询 fetch 返回非 ok 时静默忽略", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      json: async () => ({}),
    });
    vi.stubGlobal("fetch", mockFetch);

    const onEvent = vi.fn();
    renderHook(() => useWebSocket({ onEvent, enabled: false }));

    await vi.waitFor(() => {
      expect(mockFetch).toHaveBeenCalled();
    });

    await vi.waitFor(() => {
      expect(onEvent).not.toHaveBeenCalled();
    });

    vi.unstubAllGlobals();
  });

  it("轮询定时器按 60s 间隔执行", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ data: { notifications: [] } }),
    });
    vi.stubGlobal("fetch", mockFetch);

    renderHook(() => useWebSocket({ onEvent: vi.fn(), enabled: false }));

    // 立即拉取一次
    await vi.waitFor(() => {
      expect(mockFetch).toHaveBeenCalledTimes(1);
    });

    // 推进 60s
    act(() => {
      vi.advanceTimersByTime(60_000);
    });

    await vi.waitFor(() => {
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    vi.unstubAllGlobals();
  });

  it("connected 状态下 onclose 后 stopPolling 被调用（若之前在轮询）", () => {
    const { result } = renderHook(() =>
      useWebSocket({ onEvent: vi.fn(), enabled: true }),
    );

    const ws = MockWebSocketImpl.instances[0]!;
    act(() => {
      ws.fireOpen();
    });
    expect(result.current.status).toBe("connected");

    // 连接成功后应停止轮询
    // 关闭后进入 disconnected（非 polling），等待重连
    act(() => {
      ws.fireClose();
    });
    expect(result.current.status).toBe("disconnected");
  });

  it("无 WS_URL 时降级为轮询", async () => {
    // 动态测试无 WS_URL 场景
    vi.resetModules();
    vi.stubEnv("NEXT_PUBLIC_PUSH_GATEWAY_WS_URL", "");

    const { useWebSocket: useWsNoUrl } = await import("./useWebSocket");

    const { result } = renderHook(() =>
      useWsNoUrl({ onEvent: vi.fn(), enabled: true }),
    );
    expect(result.current.status).toBe("polling");

    // 恢复
    vi.stubEnv("NEXT_PUBLIC_PUSH_GATEWAY_WS_URL", "ws://localhost:8081/ws");
    vi.resetModules();
  });

  it("默认参数 enabled=true", () => {
    // 不传 options，默认 enabled=true
    const { result } = renderHook(() => useWebSocket());
    // 有 token + WS_URL → connecting
    expect(result.current.status).toBe("connecting");
  });

  it("onEvent 未传时不报错", () => {
    const { result } = renderHook(() => useWebSocket({ enabled: true }));
    expect(result.current.status).toBe("connecting");

    const ws = MockWebSocketImpl.instances[0]!;
    const event: WebSocketEvent = {
      type: "ChildBound",
      child: {
        id: "student-003",
        name: "test",
        grade: "grade.1",
        schoolName: "test school",
        classId: "class-1",
      },
    };

    expect(() => {
      act(() => {
        ws.fireMessage(event);
      });
    }).not.toThrow();
  });
});
