// useNotificationPreferences Hook 单测
// 依据：02-architecture-design.md §14 通知偏好、ISSUE-033 P4 localStorage 降级
// 覆盖：localStorage 优先加载 / GraphQL 默认值降级 / updatePreference 写入 + mutation / 错误态

import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
  afterEach,
  beforeEach,
  vi,
} from "vitest";
import { renderHook, waitFor, act } from "@testing-library/react";
import {
  Provider as UrqlProvider,
  Client,
  cacheExchange,
  fetchExchange,
} from "urql";
import { graphql, HttpResponse } from "msw";
import type { ReactNode } from "react";
import { server } from "@/test/mocks/server";
import { useNotificationPreferences } from "./useNotificationPreferences";
import type { NotificationPreferences } from "@/types";

const PREFS_KEY = "parent_notification_preferences";

// 端点：/api/v1/parent/v1/graphql（双 /v1 前缀，ARB-022 §24.4）
function createTestClient(): Client {
  return new Client({
    url: "/api/v1/parent/v1/graphql",
    exchanges: [cacheExchange, fetchExchange],
    fetchOptions: { headers: { "X-Requested-With": "XMLHttpRequest" } },
  });
}

function renderUseNotificationPreferences(parentId = "parent-001") {
  const client = createTestClient();
  const wrapper = ({ children }: { children: ReactNode }) => (
    <UrqlProvider value={client}>{children}</UrqlProvider>
  );
  return renderHook(() => useNotificationPreferences(parentId), { wrapper });
}

// 构造一份本地偏好（用于 localStorage 预置）
function makeLocalPrefs(): NotificationPreferences {
  return {
    parentId: "parent-001",
    preferences: {
      "student-001": {
        grade_recorded: { in_app: false, push: false },
      },
    },
    defaults: {
      grade_recorded: { in_app: true, push: true },
      homework_graded: { in_app: true },
      homework_assigned: { in_app: true },
      exam_published: { in_app: true },
      attendance_alert: { in_app: true },
      school_announcement: { in_app: true },
      teacher_message: { in_app: true },
      fee_reminder: { in_app: true },
      event_invitation: { in_app: true },
    },
    updatedAt: "2026-01-01T00:00:00Z",
  };
}

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

beforeEach(() => {
  localStorage.clear();
});

describe("useNotificationPreferences", () => {
  it("初始加载态：preferences 为 null 且 loading=true", () => {
    const { result } = renderUseNotificationPreferences();
    expect(result.current.preferences).toBeNull();
    expect(result.current.loading).toBe(true);
    expect(result.current.error).toBeUndefined();
  });

  it("localStorage 有缓存时优先用 localStorage 数据", async () => {
    const local = makeLocalPrefs();
    localStorage.setItem(PREFS_KEY, JSON.stringify(local));

    const { result } = renderUseNotificationPreferences();

    // useEffect 在挂载后立即读取 localStorage
    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });
    expect(result.current.preferences?.parentId).toBe("parent-001");
    // 验证用的是 localStorage 数据，而非 GraphQL mock（mock 中 in_app=true）
    expect(
      result.current.preferences?.preferences["student-001"]?.grade_recorded
        ?.in_app,
    ).toBe(false);
    expect(result.current.loading).toBe(false);
  });

  it("localStorage 缓存无效 JSON 时降级到 GraphQL 数据", async () => {
    localStorage.setItem(PREFS_KEY, "{invalid json");

    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });
    // 来自 GraphQL mock：grade_recorded.in_app=true
    expect(
      result.current.preferences?.preferences["student-001"]?.grade_recorded
        ?.in_app,
    ).toBe(true);
  });

  it("无 localStorage 时使用 GraphQL 返回的默认偏好", async () => {
    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });
    expect(result.current.preferences?.parentId).toBe("parent-001");
    // 验证 mock 中两个子女的偏好均存在
    expect(
      result.current.preferences?.preferences["student-001"],
    ).toBeDefined();
    expect(
      result.current.preferences?.preferences["student-002"],
    ).toBeDefined();
    expect(result.current.preferences?.defaults).toBeDefined();
  });

  it("updatePreference 更新本地 state 并写入 localStorage", async () => {
    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });

    act(() => {
      result.current.updatePreference(
        "student-001",
        "grade_recorded",
        "in_app",
        false,
      );
    });

    // state 已更新
    expect(
      result.current.preferences?.preferences["student-001"]?.grade_recorded
        ?.in_app,
    ).toBe(false);
    // localStorage 已写入
    const stored = localStorage.getItem(PREFS_KEY);
    expect(stored).not.toBeNull();
    const parsed = JSON.parse(stored!) as NotificationPreferences;
    expect(parsed.preferences["student-001"]?.grade_recorded?.in_app).toBe(
      false,
    );
    // updatedAt 被刷新
    expect(parsed.updatedAt).not.toBe("2026-01-01T00:00:00Z");
  });

  it("updatePreference 触发 GraphQL mutation（fire-and-forget）", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch");

    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });

    fetchSpy.mockClear();

    act(() => {
      result.current.updatePreference(
        "student-001",
        "homework_graded",
        "push",
        false,
      );
    });

    // 等待 mutation 发出
    await waitFor(() => {
      expect(fetchSpy).toHaveBeenCalled();
    });

    // 最后一次调用应为 mutation（包含 updateNotificationPreferences）
    const lastCall = fetchSpy.mock.calls[fetchSpy.mock.calls.length - 1];
    const body = (lastCall?.[1] as RequestInit | undefined)?.body;
    expect(body).toBeDefined();
    expect(String(body)).toContain("updateNotificationPreferences");
    expect(String(body)).toContain("parent-001");

    fetchSpy.mockRestore();
  });

  it("updatePreference mutation 失败时不抛出（静默降级）", async () => {
    // 使用 server.use 覆盖 mutation 处理器，保留默认 query 处理器
    server.use(
      graphql.mutation("UpdateNotificationPreferences", () =>
        HttpResponse.json(
          { errors: [{ message: "后端不可用" }] },
          { status: 200 },
        ),
      ),
    );

    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });

    // 不应抛出
    expect(() => {
      act(() => {
        result.current.updatePreference(
          "student-001",
          "grade_recorded",
          "in_app",
          false,
        );
      });
    }).not.toThrow();

    // 本地 state 仍然更新成功（localStorage 降级）
    await waitFor(() => {
      expect(
        result.current.preferences?.preferences["student-001"]?.grade_recorded
          ?.in_app,
      ).toBe(false);
    });
  });

  it("updatePreference 在 localPrefs 为 null 时为 no-op", async () => {
    // 覆盖 query 返回 null data（保留其他 handler）
    server.use(
      graphql.query("MyNotificationPreferences", () =>
        HttpResponse.json({ data: { myNotificationPreferences: null } }),
      ),
    );

    const { result } = renderUseNotificationPreferences();

    // 等待查询完成，但 localPrefs 仍为 null（因 data 为 null）
    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
    expect(result.current.preferences).toBeNull();

    // 调用 updatePreference 不应抛出且不改变 null 状态
    expect(() => {
      act(() => {
        result.current.updatePreference(
          "student-001",
          "grade_recorded",
          "in_app",
          true,
        );
      });
    }).not.toThrow();

    expect(result.current.preferences).toBeNull();
  });

  it("updatePreference 处理新 eventType（不存在时创建）", async () => {
    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });

    act(() => {
      result.current.updatePreference(
        "student-001",
        "exam_published",
        "sms",
        true,
      );
    });

    expect(
      result.current.preferences?.preferences["student-001"]?.exam_published
        ?.sms,
    ).toBe(true);
  });

  it("updatePreference 处理新 childId（不存在时创建）", async () => {
    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });

    act(() => {
      result.current.updatePreference(
        "student-999",
        "grade_recorded",
        "in_app",
        true,
      );
    });

    expect(
      result.current.preferences?.preferences["student-999"]?.grade_recorded
        ?.in_app,
    ).toBe(true);
  });

  it("GraphQL 查询出错时透传 error", async () => {
    server.use(
      graphql.query("MyNotificationPreferences", () =>
        HttpResponse.json({ errors: [{ message: "未授权" }] }, { status: 200 }),
      ),
    );

    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.error).toBeDefined();
    });
    expect(result.current.error?.message).toContain("未授权");
    // 出错时 localPrefs 保持 null，loading=false（因 fetching=false）
    expect(result.current.preferences).toBeNull();
    expect(result.current.loading).toBe(false);
  });

  it("loading 在 localPrefs 已加载后为 false（即使 query 仍在 fetching）", async () => {
    const local = makeLocalPrefs();
    localStorage.setItem(PREFS_KEY, JSON.stringify(local));

    const { result } = renderUseNotificationPreferences();

    await waitFor(() => {
      expect(result.current.preferences).not.toBeNull();
    });
    // localPrefs 已就绪，loading 必为 false
    expect(result.current.loading).toBe(false);
  });
});
