// ExportGradesButton 组件单测
// 依据：参考项目差距补齐 - 成绩导出
// 覆盖：按钮渲染 / loading 状态 / 点击触发导出 / 错误显示 / 禁用态

import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
  afterEach,
  vi,
} from "vitest";
import { render, screen, cleanup, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
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 { ExportGradesButton } from "./ExportGradesButton";

function createTestClient(): Client {
  return new Client({
    url: "/api/v1/parent/v1/graphql",
    exchanges: [cacheExchange, fetchExchange],
    fetchOptions: { headers: { "X-Requested-With": "XMLHttpRequest" } },
  });
}

function renderExportGradesButton(props?: {
  childId?: string;
  subject?: string;
  childName?: string;
}) {
  const client = createTestClient();
  const wrapper = ({ children }: { children: ReactNode }) => (
    <UrqlProvider value={client}>{children}</UrqlProvider>
  );
  return render(
    <ExportGradesButton
      childId={props?.childId ?? "student-001"}
      subject={props?.subject}
      childName={props?.childName ?? "张小明"}
    />,
    { wrapper },
  );
}

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

describe("ExportGradesButton 渲染", () => {
  it("渲染导出成绩按钮", () => {
    renderExportGradesButton();
    expect(
      screen.getByRole("button", { name: "导出张小明成绩" }),
    ).toBeInTheDocument();
  });

  it("按钮文案为导出成绩", () => {
    renderExportGradesButton();
    expect(screen.getByText("导出成绩")).toBeInTheDocument();
  });

  it("初始状态按钮可点击（非禁用）", () => {
    renderExportGradesButton();
    const btn = screen.getByRole("button", { name: "导出张小明成绩" });
    expect(btn).not.toBeDisabled();
  });

  it("childId 为空时按钮禁用", () => {
    renderExportGradesButton({ childId: "" });
    const btn = screen.getByRole("button", { name: "导出张小明成绩" });
    expect(btn).toBeDisabled();
  });
});

describe("ExportGradesButton 点击导出", () => {
  it("点击按钮触发导出并打开下载链接", async () => {
    const user = userEvent.setup();
    const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);

    renderExportGradesButton();
    await user.click(screen.getByRole("button", { name: "导出张小明成绩" }));

    // 等待 mutation 完成
    await act(async () => {
      await new Promise((resolve) => setTimeout(resolve, 100));
    });

    expect(openSpy).toHaveBeenCalledTimes(1);
    const calledUrl = openSpy.mock.calls[0]?.[0] as string;
    expect(calledUrl).toContain("student-001");
    openSpy.mockRestore();
  });

  it("导出中按钮显示导出中文案", async () => {
    // 使用延迟响应保持 loading 状态
    server.resetHandlers(
      graphql.mutation("ExportChildGrades", async () => {
        await new Promise((resolve) => setTimeout(resolve, 500));
        return HttpResponse.json({
          data: {
            exportChildGrades: {
              downloadUrl: "https://example.com/exports/grades.xlsx",
              expiresAt: "2026-07-13T23:59:59Z",
            },
          },
        });
      }),
    );

    const user = userEvent.setup();
    const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);

    renderExportGradesButton();
    await user.click(screen.getByRole("button", { name: "导出张小明成绩" }));

    // 点击后立即检查 loading 文案
    expect(screen.getByText("导出中...")).toBeInTheDocument();

    // 等待完成
    await act(async () => {
      await new Promise((resolve) => setTimeout(resolve, 700));
    });

    openSpy.mockRestore();
  });

  it("导出失败时显示错误提示", async () => {
    server.resetHandlers(
      graphql.mutation("ExportChildGrades", () =>
        HttpResponse.json(
          { errors: [{ message: "导出失败：权限不足" }] },
          { status: 200 },
        ),
      ),
    );

    const user = userEvent.setup();
    renderExportGradesButton();
    await user.click(screen.getByRole("button", { name: "导出张小明成绩" }));

    await act(async () => {
      await new Promise((resolve) => setTimeout(resolve, 100));
    });

    expect(screen.getByText(/导出失败/)).toBeInTheDocument();
    expect(screen.getByText(/权限不足/)).toBeInTheDocument();
  });

  it("成功导出后清除之前的错误提示", async () => {
    // 第一次：失败
    server.resetHandlers(
      graphql.mutation("ExportChildGrades", () =>
        HttpResponse.json(
          { errors: [{ message: "导出失败：权限不足" }] },
          { status: 200 },
        ),
      ),
    );

    const user = userEvent.setup();
    const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);

    renderExportGradesButton();
    await user.click(screen.getByRole("button", { name: "导出张小明成绩" }));
    await act(async () => {
      await new Promise((resolve) => setTimeout(resolve, 100));
    });
    expect(screen.getByText(/导出失败/)).toBeInTheDocument();

    // 第二次：成功
    server.resetHandlers(
      graphql.mutation("ExportChildGrades", () =>
        HttpResponse.json({
          data: {
            exportChildGrades: {
              downloadUrl: "https://example.com/exports/grades.xlsx",
              expiresAt: "2026-07-13T23:59:59Z",
            },
          },
        }),
      ),
    );

    await user.click(screen.getByRole("button", { name: "导出张小明成绩" }));
    await act(async () => {
      await new Promise((resolve) => setTimeout(resolve, 100));
    });

    expect(screen.queryByText(/导出失败/)).not.toBeInTheDocument();

    openSpy.mockRestore();
  });
});
