// useCrossTabSync Hook 单测
// 依据：02-architecture-design.md §4.4 跨标签同步
// 覆盖：BroadcastChannel 消息处理 / storage 事件降级 / broadcastSync 广播 / 防回环 / 清理

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { renderHook, cleanup } from "@testing-library/react";
import {
  useCrossTabSync,
  broadcastSync,
  SYNC_SOURCE_ID,
} from "./useCrossTabSync";
import { useChildStore } from "@/store/child-store";

// ===== BroadcastChannel mock =====
interface MockBroadcastChannel {
  name: string;
  onmessage: ((event: { data: unknown }) => void) | null;
  postMessage: (data: unknown) => void;
  close: () => void;
}

const mockChannels: Map<string, MockBroadcastChannel[]> = new Map();

class MockBroadcastChannelImpl {
  name: string;
  onmessage: ((event: { data: unknown }) => void) | null = null;
  constructor(name: string) {
    this.name = name;
    const list = mockChannels.get(name) ?? [];
    list.push(this);
    mockChannels.set(name, list);
  }
  postMessage(data: unknown) {
    const list = mockChannels.get(this.name) ?? [];
    for (const ch of list) {
      if (ch !== this && ch.onmessage) {
        ch.onmessage({ data });
      }
    }
  }
  close() {
    const list = mockChannels.get(this.name) ?? [];
    const idx = list.indexOf(this);
    if (idx >= 0) list.splice(idx, 1);
    mockChannels.set(this.name, list);
  }
}

beforeEach(() => {
  localStorage.clear();
  mockChannels.clear();
  // 注入 mock BroadcastChannel
  Object.defineProperty(window, "BroadcastChannel", {
    writable: true,
    configurable: true,
    value: MockBroadcastChannelImpl,
  });
  useChildStore.setState({
    children: [],
    currentChildId: null,
    isLoading: false,
  });
});

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

describe("useCrossTabSync", () => {
  it("SYNC_SOURCE_ID 是非空字符串", () => {
    expect(SYNC_SOURCE_ID).toBeTruthy();
    expect(typeof SYNC_SOURCE_ID).toBe("string");
  });

  it("收到 child-switched 消息时更新 store currentChildId", () => {
    // 先在 store 中放入 children，确保 switchChild 校验通过
    useChildStore.setState({
      children: [
        {
          id: "student-001",
          name: "张小明",
          grade: "grade.7",
          schoolName: "实验中学",
          classId: "class-7-1",
          className: "初一(1)班",
        },
        {
          id: "student-002",
          name: "张小红",
          grade: "grade.5",
          schoolName: "实验小学",
          classId: "class-5-2",
          className: "五年级(2)班",
        },
      ],
      currentChildId: "student-001",
    });

    renderHook(() => useCrossTabSync());

    // 模拟其他标签广播 child-switched 消息
    const otherChannel = new MockBroadcastChannelImpl("parent-sync");
    otherChannel.onmessage = null; // 接收方
    // 通过 useCrossTabSync 创建的 channel 接收消息
    const hookChannels = mockChannels.get("parent-sync") ?? [];
    const hookChannel = hookChannels[0];
    expect(hookChannel).toBeDefined();
    expect(hookChannel!.onmessage).not.toBeNull();

    hookChannel!.onmessage!({
      data: {
        type: "child-switched",
        childId: "student-002",
        ts: Date.now(),
        source: "other-tab",
      },
    });

    expect(useChildStore.getState().currentChildId).toBe("student-002");
  });

  it("收到 preferences-updated 消息时清除 localStorage 缓存", () => {
    localStorage.setItem("parent_notification_preferences", '{"old":true}');

    renderHook(() => useCrossTabSync());

    const hookChannels = mockChannels.get("parent-sync") ?? [];
    const hookChannel = hookChannels[0];
    hookChannel!.onmessage!({
      data: {
        type: "preferences-updated",
        ts: Date.now(),
        source: "other-tab",
      },
    });

    expect(localStorage.getItem("parent_notification_preferences")).toBeNull();
  });

  it("收到 child-unbound 消息时不抛错（no-op）", () => {
    renderHook(() => useCrossTabSync());

    const hookChannels = mockChannels.get("parent-sync") ?? [];
    const hookChannel = hookChannels[0];
    expect(() =>
      hookChannel!.onmessage!({
        data: {
          type: "child-unbound",
          childId: "student-001",
          ts: Date.now(),
          source: "other-tab",
        },
      }),
    ).not.toThrow();
  });

  it("防回环：忽略自己发出的消息", () => {
    useChildStore.setState({ currentChildId: "student-001" });

    renderHook(() => useCrossTabSync());

    const hookChannels = mockChannels.get("parent-sync") ?? [];
    const hookChannel = hookChannels[0];
    // 用自己的 source ID 发消息
    hookChannel!.onmessage!({
      data: {
        type: "child-switched",
        childId: "student-002",
        ts: Date.now(),
        source: SYNC_SOURCE_ID,
      },
    });

    // 不应更新（防回环）
    expect(useChildStore.getState().currentChildId).toBe("student-001");
  });

  it("空消息被忽略", () => {
    renderHook(() => useCrossTabSync());

    const hookChannels = mockChannels.get("parent-sync") ?? [];
    const hookChannel = hookChannels[0];
    expect(() => hookChannel!.onmessage!({ data: null })).not.toThrow();
  });

  it("卸载时关闭 BroadcastChannel 并移除 storage 监听", () => {
    const removeEventListenerSpy = vi.spyOn(window, "removeEventListener");
    const { unmount } = renderHook(() => useCrossTabSync());

    const hookChannelsBefore = mockChannels.get("parent-sync") ?? [];
    expect(hookChannelsBefore.length).toBeGreaterThan(0);

    unmount();

    // storage 监听已移除
    expect(removeEventListenerSpy).toHaveBeenCalledWith(
      "storage",
      expect.any(Function),
    );
  });

  it("storage 事件降级：child-switched 更新 store", () => {
    useChildStore.setState({ currentChildId: "student-001" });

    renderHook(() => useCrossTabSync());

    // 模拟 storage 事件
    const storageEvent = new StorageEvent("storage", {
      key: "parent_sync_event",
      newValue: JSON.stringify({
        type: "child-switched",
        childId: "student-002",
        ts: Date.now(),
        source: "other-tab",
      }),
    });
    window.dispatchEvent(storageEvent);

    expect(useChildStore.getState().currentChildId).toBe("student-002");
  });

  it("storage 事件降级：preferences-updated 清除缓存", () => {
    localStorage.setItem("parent_notification_preferences", '{"old":true}');

    renderHook(() => useCrossTabSync());

    const storageEvent = new StorageEvent("storage", {
      key: "parent_sync_event",
      newValue: JSON.stringify({
        type: "preferences-updated",
        ts: Date.now(),
        source: "other-tab",
      }),
    });
    window.dispatchEvent(storageEvent);

    expect(localStorage.getItem("parent_notification_preferences")).toBeNull();
  });

  it("storage 事件降级：防回环忽略自己", () => {
    useChildStore.setState({ currentChildId: "student-001" });

    renderHook(() => useCrossTabSync());

    const storageEvent = new StorageEvent("storage", {
      key: "parent_sync_event",
      newValue: JSON.stringify({
        type: "child-switched",
        childId: "student-002",
        ts: Date.now(),
        source: SYNC_SOURCE_ID,
      }),
    });
    window.dispatchEvent(storageEvent);

    expect(useChildStore.getState().currentChildId).toBe("student-001");
  });

  it("storage 事件降级：忽略无关 key", () => {
    useChildStore.setState({ currentChildId: "student-001" });

    renderHook(() => useCrossTabSync());

    const storageEvent = new StorageEvent("storage", {
      key: "other_key",
      newValue: JSON.stringify({
        type: "child-switched",
        childId: "student-002",
        ts: Date.now(),
        source: "other-tab",
      }),
    });
    window.dispatchEvent(storageEvent);

    expect(useChildStore.getState().currentChildId).toBe("student-001");
  });

  it("storage 事件降级：忽略无效 JSON", () => {
    renderHook(() => useCrossTabSync());

    const storageEvent = new StorageEvent("storage", {
      key: "parent_sync_event",
      newValue: "{invalid json",
    });
    expect(() => window.dispatchEvent(storageEvent)).not.toThrow();
  });

  it("storage 事件降级：忽略空 newValue", () => {
    renderHook(() => useCrossTabSync());

    const storageEvent = new StorageEvent("storage", {
      key: "parent_sync_event",
      newValue: null,
    });
    expect(() => window.dispatchEvent(storageEvent)).not.toThrow();
  });
});

describe("broadcastSync", () => {
  it("通过 BroadcastChannel 广播消息并写入 localStorage", () => {
    renderHook(() => useCrossTabSync());

    // Omit<SyncMessage,"source"> 不分配联合类型，用变量避免 excess property check
    const msg = {
      type: "child-switched" as const,
      childId: "student-002",
      ts: Date.now(),
    };
    broadcastSync(msg);

    // localStorage 被写入（storage 降级路径）
    expect(localStorage.getItem("parent_sync_event")).toBeTruthy();
    const stored = JSON.parse(
      localStorage.getItem("parent_sync_event") ?? "{}",
    ) as { source: string; type: string };
    expect(stored.source).toBe(SYNC_SOURCE_ID);
    expect(stored.type).toBe("child-switched");
  });

  it("广播消息携带 source ID", () => {
    renderHook(() => useCrossTabSync());

    broadcastSync({
      type: "preferences-updated",
      ts: Date.now(),
    });

    const stored = JSON.parse(
      localStorage.getItem("parent_sync_event") ?? "{}",
    ) as { source: string };
    expect(stored.source).toBe(SYNC_SOURCE_ID);
  });

  it("BroadcastChannel 不可用时不报错", () => {
    // 移除 BroadcastChannel（删除属性使 "BroadcastChannel" in window 为 false）
    delete (window as { BroadcastChannel?: unknown }).BroadcastChannel;

    const msg = {
      type: "child-switched" as const,
      childId: "student-001",
      ts: Date.now(),
    };
    expect(() => broadcastSync(msg)).not.toThrow();
    // storage 降级路径仍写入 localStorage
    expect(localStorage.getItem("parent_sync_event")).toBeTruthy();
  });
});
