/**
 * ImageUpload 组件测试
 *
 * 测试图片上传组件：
 * - 渲染拖拽区域
 * - 文件选择后显示预览
 * - 清除按钮工作
 */

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

/** 模拟 FileReader.readAsDataURL */
function mockFileReader(result = "data:image/png;base64,mock"): void {
  const mockReader = {
    onload: null as (() => void) | null,
    onerror: null as (() => void) | null,
    result: result,
    readAsDataURL: vi.fn(function (this: typeof mockReader) {
      // 异步触发 onload
      setTimeout(() => {
        this.onload?.();
      }, 0);
    }),
  };
  vi.stubGlobal("FileReader", vi.fn(() => mockReader));
}

/** 创建模拟图片文件 */
function createImageFile(name = "test.png", size = 1024, type = "image/png"): File {
  const file = new File(["mock-content"], name, { type });
  Object.defineProperty(file, "size", { value: size, configurable: true });
  return file;
}

describe("ImageUpload", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    mockFileReader();
  });

  describe("渲染", () => {
    it("应渲染拖拽上传区域", () => {
      render(<ImageUpload value={[]} onChange={() => {}} />);
      const dropZone = screen.getByRole("button", { name: "点击或拖拽上传图片" });
      expect(dropZone).toBeInTheDocument();
    });

    it("应渲染 file input（sr-only）", () => {
      render(<ImageUpload value={[]} onChange={() => {}} />);
      const input = screen.getByLabelText("选择图片文件");
      expect(input).toHaveAttribute("type", "file");
      expect(input).toHaveAttribute("accept", "image/*");
    });

    it("应渲染 group 容器", () => {
      render(<ImageUpload value={[]} onChange={() => {}} ariaLabel="作业附件" />);
      const group = screen.getByRole("group", { name: "作业附件" });
      expect(group).toBeInTheDocument();
    });

    it("无图片时不渲染缩略图列表", () => {
      render(<ImageUpload value={[]} onChange={() => {}} />);
      expect(screen.queryByLabelText("已上传图片列表")).not.toBeInTheDocument();
    });
  });

  describe("已有图片预览", () => {
    it("value 有值时应渲染缩略图列表", () => {
      render(
        <ImageUpload
          value={["data:image/png;base64,abc"]}
          onChange={() => {}}
        />,
      );
      expect(screen.getByLabelText("已上传图片列表")).toBeInTheDocument();
    });

    it("每个图片应渲染 img 元素", () => {
      render(
        <ImageUpload
          value={["data:image/png;base64,abc", "data:image/png;base64,def"]}
          onChange={() => {}}
        />,
      );
      // img 元素显式设置了 role="button"（可点击查看大图），覆盖了隐式 img 角色
      // 因此用 alt 文本查询而非 role="img"
      const imgs = screen.getAllByAltText(/已上传图片 \d/);
      // 2 个上传的图片缩略图
      expect(imgs.length).toBeGreaterThanOrEqual(2);
    });

    it("非禁用状态应显示移除按钮", () => {
      render(
        <ImageUpload
          value={["data:image/png;base64,abc"]}
          onChange={() => {}}
        />,
      );
      expect(screen.getByLabelText("移除图片 1")).toBeInTheDocument();
    });

    it("disabled 状态不显示移除按钮", () => {
      render(
        <ImageUpload
          value={["data:image/png;base64,abc"]}
          onChange={() => {}}
          disabled
        />,
      );
      expect(screen.queryByLabelText("移除图片 1")).not.toBeInTheDocument();
    });
  });

  describe("清除/移除图片", () => {
    it("点击移除按钮应调用 onChange 移除对应图片", () => {
      const onChange = vi.fn();
      render(
        <ImageUpload
          value={["data:image/png;base64,abc", "data:image/png;base64,def"]}
          onChange={onChange}
        />,
      );

      fireEvent.click(screen.getByLabelText("移除图片 1"));
      expect(onChange).toHaveBeenCalledWith(["data:image/png;base64,def"]);
    });

    it("移除第二张图片应保留第一张", () => {
      const onChange = vi.fn();
      render(
        <ImageUpload
          value={["data:image/png;base64,abc", "data:image/png;base64,def"]}
          onChange={onChange}
        />,
      );

      fireEvent.click(screen.getByLabelText("移除图片 2"));
      expect(onChange).toHaveBeenCalledWith(["data:image/png;base64,abc"]);
    });

    it("移除唯一图片后列表消失", () => {
      const onChange = vi.fn();
      const { rerender } = render(
        <ImageUpload
          value={["data:image/png;base64,abc"]}
          onChange={onChange}
        />,
      );

      fireEvent.click(screen.getByLabelText("移除图片 1"));
      expect(onChange).toHaveBeenCalledWith([]);

      // 模拟父组件更新
      rerender(<ImageUpload value={[]} onChange={onChange} />);
      expect(screen.queryByLabelText("已上传图片列表")).not.toBeInTheDocument();
    });
  });

  describe("文件选择", () => {
    it("选择图片文件后应调用 onChange", async () => {
      const onChange = vi.fn();
      render(<ImageUpload value={[]} onChange={onChange} />);
      const input = screen.getByLabelText("选择图片文件") as HTMLInputElement;

      const file = createImageFile();
      fireEvent.change(input, { target: { files: [file] } });

      await waitFor(() => {
        expect(onChange).toHaveBeenCalledWith(["data:image/png;base64,mock"]);
      });
    });

    it("选择非图片文件应显示错误", async () => {
      const onChange = vi.fn();
      render(<ImageUpload value={[]} onChange={onChange} />);
      const input = screen.getByLabelText("选择图片文件") as HTMLInputElement;

      const file = new File(["content"], "test.txt", { type: "text/plain" });
      fireEvent.change(input, { target: { files: [file] } });

      await waitFor(() => {
        expect(screen.getByRole("alert")).toHaveTextContent("请选择图片文件");
      });
      expect(onChange).not.toHaveBeenCalled();
    });

    it("超过 maxCount 应显示错误", async () => {
      const onChange = vi.fn();
      render(
        <ImageUpload
          value={["data:image/png;base64,existing"]}
          onChange={onChange}
          maxCount={1}
        />,
      );
      // 超过 maxCount 时拖拽区不渲染
      expect(screen.queryByRole("button", { name: "点击或拖拽上传图片" })).not.toBeInTheDocument();
    });
  });

  describe("拖拽", () => {
    it("dragOver 应设置拖拽状态", () => {
      render(<ImageUpload value={[]} onChange={() => {}} />);
      const dropZone = screen.getByRole("button", { name: "点击或拖拽上传图片" });

      fireEvent.dragOver(dropZone, {
        dataTransfer: { files: [] },
      });
      // 不应报错
      expect(dropZone).toBeInTheDocument();
    });

    it("drop 图片文件应调用 onChange", async () => {
      const onChange = vi.fn();
      render(<ImageUpload value={[]} onChange={onChange} />);
      const dropZone = screen.getByRole("button", { name: "点击或拖拽上传图片" });

      const file = createImageFile();
      fireEvent.drop(dropZone, {
        dataTransfer: { files: [file] },
      });

      await waitFor(() => {
        expect(onChange).toHaveBeenCalledWith(["data:image/png;base64,mock"]);
      });
    });

    it("dragLeave 应重置拖拽状态", () => {
      render(<ImageUpload value={[]} onChange={() => {}} />);
      const dropZone = screen.getByRole("button", { name: "点击或拖拽上传图片" });

      fireEvent.dragOver(dropZone, { dataTransfer: { files: [] } });
      fireEvent.dragLeave(dropZone, { dataTransfer: { files: [] } });

      expect(dropZone).toBeInTheDocument();
    });
  });

  describe("禁用状态", () => {
    it("disabled=true 时不渲染拖拽区", () => {
      render(<ImageUpload value={[]} onChange={() => {}} disabled />);
      expect(screen.queryByRole("button", { name: "点击或拖拽上传图片" })).not.toBeInTheDocument();
    });

    it("disabled=true 且有图片时仍显示缩略图但不显示移除按钮", () => {
      render(
        <ImageUpload
          value={["data:image/png;base64,abc"]}
          onChange={() => {}}
          disabled
        />,
      );
      expect(screen.getByLabelText("已上传图片列表")).toBeInTheDocument();
      expect(screen.queryByLabelText("移除图片 1")).not.toBeInTheDocument();
    });
  });

  describe("最大数量限制", () => {
    it("达到 maxCount 时隐藏拖拽区", () => {
      render(
        <ImageUpload
          value={["a", "b"]}
          onChange={() => {}}
          maxCount={2}
        />,
      );
      expect(screen.queryByRole("button", { name: "点击或拖拽上传图片" })).not.toBeInTheDocument();
    });

    it("未达到 maxCount 时显示拖拽区", () => {
      render(
        <ImageUpload
          value={["a"]}
          onChange={() => {}}
          maxCount={3}
        />,
      );
      expect(screen.getByRole("button", { name: "点击或拖拽上传图片" })).toBeInTheDocument();
    });
  });
});
