/**
 * HandwritingPad 组件测试
 *
 * 测试手写板组件：
 * - 渲染 canvas 元素
 * - 清空按钮工作
 * - 撤销按钮工作
 */

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

// jsdom 不实现 Canvas API，需手动 mock
function mockCanvasContext(): {
  ctx: Record<string, unknown>;
} {
  const ctx: Record<string, unknown> = {
    clearRect: vi.fn(),
    strokeStyle: "",
    lineWidth: 0,
    lineCap: "",
    lineJoin: "",
    beginPath: vi.fn(),
    moveTo: vi.fn(),
    lineTo: vi.fn(),
    quadraticCurveTo: vi.fn(),
    stroke: vi.fn(),
  };

  const canvasProto = HTMLCanvasElement.prototype;
  vi.spyOn(canvasProto, "getContext").mockReturnValue(ctx as unknown as CanvasRenderingContext2D);
  vi.spyOn(canvasProto, "toDataURL").mockReturnValue("data:image/png;base64,mock");
  // Pointer capture 方法在 jsdom 中不存在
  if (!Element.prototype.setPointerCapture) {
    Element.prototype.setPointerCapture = vi.fn();
  }
  if (!Element.prototype.releasePointerCapture) {
    Element.prototype.releasePointerCapture = vi.fn();
  }

  return { ctx };
}

describe("HandwritingPad", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    mockCanvasContext();
  });

  describe("渲染", () => {
    it("应渲染 canvas 元素", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });
      expect(canvas).toBeInTheDocument();
      expect(canvas.tagName).toBe("CANVAS");
    });

    it("应渲染手写板控件组", () => {
      render(<HandwritingPad />);
      const group = screen.getByRole("group", { name: "手写板控件" });
      expect(group).toBeInTheDocument();
    });

    it("应渲染撤销按钮", () => {
      render(<HandwritingPad />);
      const undoBtn = screen.getByRole("button", { name: "撤销上一笔" });
      expect(undoBtn).toBeInTheDocument();
    });

    it("应渲染清空按钮", () => {
      render(<HandwritingPad />);
      const clearBtn = screen.getByRole("button", { name: "清空手写板" });
      expect(clearBtn).toBeInTheDocument();
    });

    it("应显示笔画计数", () => {
      render(<HandwritingPad />);
      // 初始 0 笔
      expect(screen.getByText("0 笔")).toBeInTheDocument();
    });

    it("应支持自定义 ariaLabel", () => {
      render(<HandwritingPad ariaLabel="自定义手写区" />);
      expect(screen.getByRole("img", { name: "自定义手写区" })).toBeInTheDocument();
    });
  });

  describe("按钮初始状态", () => {
    it("无笔画时撤销按钮应禁用", () => {
      render(<HandwritingPad />);
      const undoBtn = screen.getByRole("button", { name: "撤销上一笔" });
      expect(undoBtn).toBeDisabled();
    });

    it("无笔画时清空按钮应禁用", () => {
      render(<HandwritingPad />);
      const clearBtn = screen.getByRole("button", { name: "清空手写板" });
      expect(clearBtn).toBeDisabled();
    });
  });

  describe("绘制功能", () => {
    it("pointerdown + pointerup 应添加一笔", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerMove(canvas, { clientX: 20, clientY: 20, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 20, clientY: 20, pointerId: 1 });

      expect(screen.getByText("1 笔")).toBeInTheDocument();
    });

    it("绘制后撤销按钮应启用", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });

      const undoBtn = screen.getByRole("button", { name: "撤销上一笔" });
      expect(undoBtn).not.toBeDisabled();
    });

    it("绘制后清空按钮应启用", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });

      const clearBtn = screen.getByRole("button", { name: "清空手写板" });
      expect(clearBtn).not.toBeDisabled();
    });
  });

  describe("撤销功能", () => {
    it("点击撤销应移除最后一笔", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      // 画两笔
      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30, pointerId: 2 });
      fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30, pointerId: 2 });

      expect(screen.getByText("2 笔")).toBeInTheDocument();

      // 撤销
      fireEvent.click(screen.getByRole("button", { name: "撤销上一笔" }));
      expect(screen.getByText("1 笔")).toBeInTheDocument();
    });

    it("撤销到 0 笔时撤销按钮应禁用", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });

      fireEvent.click(screen.getByRole("button", { name: "撤销上一笔" }));
      expect(screen.getByText("0 笔")).toBeInTheDocument();

      const undoBtn = screen.getByRole("button", { name: "撤销上一笔" });
      expect(undoBtn).toBeDisabled();
    });
  });

  describe("清空功能", () => {
    it("点击清空应移除所有笔画", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      // 画两笔
      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30, pointerId: 2 });
      fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30, pointerId: 2 });

      expect(screen.getByText("2 笔")).toBeInTheDocument();

      // 清空
      fireEvent.click(screen.getByRole("button", { name: "清空手写板" }));
      expect(screen.getByText("0 笔")).toBeInTheDocument();
    });

    it("清空后撤销和清空按钮应禁用", () => {
      render(<HandwritingPad />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });

      fireEvent.click(screen.getByRole("button", { name: "清空手写板" }));

      expect(screen.getByRole("button", { name: "撤销上一笔" })).toBeDisabled();
      expect(screen.getByRole("button", { name: "清空手写板" })).toBeDisabled();
    });

    it("清空应调用 onExport 回调", () => {
      const onExport = vi.fn();
      render(<HandwritingPad onExport={onExport} />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      // 画一笔（会触发 onExport）
      onExport.mockClear();
      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });

      // 清空
      onExport.mockClear();
      fireEvent.click(screen.getByRole("button", { name: "清空手写板" }));
      expect(onExport).toHaveBeenCalledWith("");
    });
  });

  describe("禁用状态", () => {
    it("disabled=true 时 canvas 应禁止绘制", () => {
      render(<HandwritingPad disabled />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });

      // 不应添加笔画
      expect(screen.getByText("0 笔")).toBeInTheDocument();
    });

    it("disabled=true 时按钮应禁用", () => {
      render(<HandwritingPad disabled />);
      expect(screen.getByRole("button", { name: "撤销上一笔" })).toBeDisabled();
      expect(screen.getByRole("button", { name: "清空手写板" })).toBeDisabled();
    });
  });

  describe("onExport 回调", () => {
    it("绘制后应调用 onExport", () => {
      const onExport = vi.fn();
      render(<HandwritingPad onExport={onExport} />);
      const canvas = screen.getByRole("img", { name: "手写作答区" });

      fireEvent.pointerDown(canvas, { clientX: 10, clientY: 10, pointerId: 1 });
      fireEvent.pointerUp(canvas, { clientX: 10, clientY: 10, pointerId: 1 });

      expect(onExport).toHaveBeenCalled();
      expect(onExport).toHaveBeenCalledWith(expect.stringContaining("data:image/png"));
    });
  });
});
