/**
 * JoinClassModal 组件测试
 *
 * 测试加入班级弹窗：
 * - open=false 时不渲染
 * - 6 位邀请码输入
 * - 长度不足时显示错误
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";

// Mock useJoinClass hook（使用 vi.hoisted 确保 vi.mock 工厂能访问变量）
const { mockExecuteJoin, mockUseJoinClass } = vi.hoisted(() => ({
  mockExecuteJoin: vi.fn(),
  mockUseJoinClass: vi.fn(),
}));

vi.mock("@/lib/graphql/hooks", () => ({
  useJoinClass: () => mockUseJoinClass(),
}));

import { JoinClassModal } from "./join-class-modal";

describe("JoinClassModal", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockUseJoinClass.mockReturnValue({
      execute: mockExecuteJoin,
      fetching: false,
      data: null,
      success: false,
      errors: [],
    });
  });

  describe("渲染控制", () => {
    it("open=false 时应返回 null", () => {
      const { container } = render(
        <JoinClassModal open={false} onClose={() => {}} />,
      );
      expect(container.firstChild).toBeNull();
    });

    it("open=true 时应渲染 dialog", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      expect(screen.getByRole("dialog")).toBeInTheDocument();
    });

    it('应渲染标题"加入班级"', () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      // "加入班级"同时出现在 h3 标题和提交按钮中，用 heading 角色精确定位标题
      expect(screen.getByRole("heading", { name: "加入班级" })).toBeInTheDocument();
    });

    it("应渲染提示文字", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      expect(screen.getByText(/请输入老师提供的 6 位班级邀请码/)).toBeInTheDocument();
    });
  });

  describe("邀请码输入", () => {
    it("应渲染 6 个输入框", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const inputs = screen.getAllByRole("textbox");
      expect(inputs).toHaveLength(6);
    });

    it("每个输入框应正确标注 aria-label", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      for (let i = 1; i <= 6; i++) {
        expect(screen.getByLabelText(`邀请码第 ${i} 位`)).toBeInTheDocument();
      }
    });

    it("输入字母应转为大写", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const firstInput = screen.getByLabelText("邀请码第 1 位");
      fireEvent.change(firstInput, { target: { value: "a" } });
      expect(firstInput).toHaveValue("A");
    });

    it("输入非字母数字应被过滤", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const firstInput = screen.getByLabelText("邀请码第 1 位");
      fireEvent.change(firstInput, { target: { value: "-" } });
      expect(firstInput).toHaveValue("");
    });

    it("粘贴 6 位邀请码应自动填充所有格子", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const firstInput = screen.getByLabelText("邀请码第 1 位");
      fireEvent.change(firstInput, { target: { value: "ABC123" } });

      for (let i = 1; i <= 6; i++) {
        expect(screen.getByLabelText(`邀请码第 ${i} 位`)).toHaveValue(
          "ABC123"[i - 1],
        );
      }
    });

    it("maxLength 应为 1", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const firstInput = screen.getByLabelText("邀请码第 1 位");
      expect(firstInput).toHaveAttribute("maxLength", "1");
    });
  });

  describe("提交校验", () => {
    it("未填满 6 位时提交按钮应禁用", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const submitBtn = screen.getByRole("button", { name: "加入班级" });
      expect(submitBtn).toBeDisabled();
    });

    it("填满 6 位后提交按钮应启用", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const firstInput = screen.getByLabelText("邀请码第 1 位");
      fireEvent.change(firstInput, { target: { value: "ABCDEF" } });

      const submitBtn = screen.getByRole("button", { name: "加入班级" });
      expect(submitBtn).not.toBeDisabled();
    });

    it("点击提交应调用 executeJoin", () => {
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const firstInput = screen.getByLabelText("邀请码第 1 位");
      fireEvent.change(firstInput, { target: { value: "ABC123" } });

      fireEvent.click(screen.getByRole("button", { name: "加入班级" }));
      expect(mockExecuteJoin).toHaveBeenCalledWith({ inviteCode: "ABC123" });
    });

    it("未填满时点击提交不应调用 executeJoin", () => {
      // 先填满再清空一格
      render(<JoinClassModal open={true} onClose={() => {}} />);
      const firstInput = screen.getByLabelText("邀请码第 1 位");
      fireEvent.change(firstInput, { target: { value: "ABCDE" } }); // 5 位

      // 提交按钮应禁用
      expect(screen.getByRole("button", { name: "加入班级" })).toBeDisabled();
      expect(mockExecuteJoin).not.toHaveBeenCalled();
    });
  });

  describe("关闭行为", () => {
    it("点击取消按钮应调用 onClose", () => {
      const onClose = vi.fn();
      render(<JoinClassModal open={true} onClose={onClose} />);

      fireEvent.click(screen.getByRole("button", { name: "取消" }));
      expect(onClose).toHaveBeenCalled();
    });

    it("点击关闭图标应调用 onClose", () => {
      const onClose = vi.fn();
      render(<JoinClassModal open={true} onClose={onClose} />);

      fireEvent.click(screen.getByRole("button", { name: "关闭" }));
      expect(onClose).toHaveBeenCalled();
    });

    it("ESC 键应调用 onClose", () => {
      const onClose = vi.fn();
      render(<JoinClassModal open={true} onClose={onClose} />);

      fireEvent.keyDown(window, { key: "Escape" });
      expect(onClose).toHaveBeenCalled();
    });
  });

  describe("提交成功", () => {
    it("success=true 应显示成功消息", async () => {
      // 先以无结果状态渲染，让 open effect 完成重置
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: null,
        success: false,
        errors: [],
      });
      const { rerender } = render(<JoinClassModal open={true} onClose={() => {}} />);

      // 模拟 mutation 完成，触发 mutation effect 设置 feedback
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: { classId: "c1", className: "高三(1)班", grade: "高三", inviteCode: "ABC123" },
        success: true,
        errors: [],
      });
      rerender(<JoinClassModal open={true} onClose={() => {}} />);

      await waitFor(() => {
        expect(screen.getByText(/已加入班级：高三\(1\)班/)).toBeInTheDocument();
      });
    });

    it("success=true 应调用 onJoined 回调", async () => {
      const onJoined = vi.fn();
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: null,
        success: false,
        errors: [],
      });
      const { rerender } = render(
        <JoinClassModal open={true} onClose={() => {}} onJoined={onJoined} />,
      );

      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: { classId: "c1", className: "高三(1)班", grade: "高三", inviteCode: "ABC123" },
        success: true,
        errors: [],
      });
      rerender(
        <JoinClassModal open={true} onClose={() => {}} onJoined={onJoined} />,
      );

      await waitFor(() => {
        expect(onJoined).toHaveBeenCalledWith("高三(1)班");
      });
    });
  });

  describe("提交失败", () => {
    it("success=false 应显示错误消息", async () => {
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: null,
        success: false,
        errors: [],
      });
      const { rerender } = render(<JoinClassModal open={true} onClose={() => {}} />);

      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: null,
        success: false,
        errors: [{ code: "INVALID_CODE", message: "邀请码无效" }],
      });
      rerender(<JoinClassModal open={true} onClose={() => {}} />);

      await waitFor(() => {
        expect(screen.getByText("邀请码无效")).toBeInTheDocument();
      });
    });

    it("success=false 无 errors 时显示默认错误", async () => {
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: null,
        success: false,
        errors: [],
      });
      const { rerender } = render(<JoinClassModal open={true} onClose={() => {}} />);

      // data 非 null 才会触发 mutation effect（组件中检查 data !== null）
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: false,
        data: { classId: "", className: "", grade: "", inviteCode: "" },
        success: false,
        errors: [],
      });
      rerender(<JoinClassModal open={true} onClose={() => {}} />);

      await waitFor(() => {
        expect(screen.getByText("加入失败")).toBeInTheDocument();
      });
    });
  });

  describe("loading 状态", () => {
    it('fetching=true 时提交按钮应显示"加入中…"且禁用', () => {
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: true,
        data: null,
        success: false,
        errors: [],
      });

      render(<JoinClassModal open={true} onClose={() => {}} />);

      const submitBtn = screen.getByRole("button", { name: "加入中…" });
      expect(submitBtn).toBeDisabled();
    });

    it("fetching=true 时输入框应禁用", () => {
      mockUseJoinClass.mockReturnValue({
        execute: mockExecuteJoin,
        fetching: true,
        data: null,
        success: false,
        errors: [],
      });

      render(<JoinClassModal open={true} onClose={() => {}} />);

      const inputs = screen.getAllByRole("textbox");
      inputs.forEach((input) => {
        expect(input).toBeDisabled();
      });
    });
  });
});
