import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { expect, test } from "vitest";

import { SeriesForm } from "@/features/series/components/SeriesForm";
import type { SeriesGenreItem, SeriesListItem } from "@/features/series/model/types";
import type { CropSourceImage } from "@/shared/ui/image-crop-dialog";
import type { ApiClient, ApiRequestOptions } from "@/shared/api/client";

type CapturedRequest = { readonly body?: BodyInit | null; readonly method?: string; readonly path: string };

const genres = [
  { id: 77, genre: "로맨스", isAdult: false },
  { id: 88, genre: "일상", isAdult: false },
] as const satisfies readonly SeriesGenreItem[];

const existingSeries = {
  seriesId: 5001,
  title: "달빛 상담 시리즈",
  introduction: "밤마다 이어지는 상담 에피소드",
  coverImageUrl: "https://cdn.example.com/series/moon.png",
  publishedDaysOfWeek: ["SUN", "WED"],
  genreId: 77,
  isAdult: false,
  state: "PROCEEDING",
  isActive: true,
  writer: "스튜디오 루나",
  studio: "소다랩",
} as const satisfies SeriesListItem;

function createClient(requests: CapturedRequest[]): ApiClient {
  return { async request<Data>(options: ApiRequestOptions<Data>): Promise<Data> { requests.push({ body: options.body, method: options.method, path: options.path }); return options.responseSchema.parse(null); } };
}

function createImage(name = "series.png"): File {
  return new File(["image"], name, { type: "image/png" });
}

function renderEditForm(requests: CapturedRequest[], createCropSource: (file: File) => Promise<CropSourceImage>) {
  window.history.pushState({}, "", "/ai-characters/101/series/5001/edit");
  render(<SeriesForm apiClient={createClient(requests)} characterId="101" createCropSource={createCropSource} genres={genres} mode="edit" renderCrop={(request) => Promise.resolve(request.file)} series={existingSeries} />);
}

function requireFile(part: FormDataEntryValue | null): File {
  if (part instanceof File) {
    return part;
  }
  throw new TypeError("Expected File part");
}

test("SeriesForm blocks edit save while replacement image crop source is preparing", async () => {
  const requests: CapturedRequest[] = [];
  renderEditForm(requests, () => new Promise(() => undefined));

  fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [createImage("pending.png")] } });

  await waitFor(() => expect(screen.getByRole("button", { name: "저장" })).toBeDisabled());
  fireEvent.submit(screen.getByRole("form", { name: "시리즈 수정 입력 화면" }));
  expect(requests.filter((request) => request.method === "PUT")).toHaveLength(0);
});

test("SeriesForm ignores stale image crop sources and saves the latest applied replacement", async () => {
  const requests: CapturedRequest[] = [];
  const cropResolvers = new Map<string, (source: CropSourceImage) => void>();
  renderEditForm(requests, (file) => new Promise((resolve) => cropResolvers.set(file.name, resolve)));
  const staleFile = createImage("stale.png");
  const freshFile = createImage("fresh.png");

  fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [staleFile] } });
  fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [freshFile] } });
  cropResolvers.get("fresh.png")?.({ file: freshFile, height: 1414, previewUrl: "blob:fresh", width: 1000 });
  fireEvent.click(await screen.findByRole("button", { name: "적용" }));
  await screen.findByText("fresh.png");
  cropResolvers.get("stale.png")?.({ file: staleFile, height: 1414, previewUrl: "blob:stale", width: 1000 });

  await waitFor(() => expect(screen.queryByRole("dialog", { name: "이미지 crop" })).not.toBeInTheDocument());
  fireEvent.submit(screen.getByRole("form", { name: "시리즈 수정 입력 화면" }));
  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/series/5001"));
  const updateBody = requests.find((request) => request.method === "PUT")?.body as FormData;
  expect(requireFile(updateBody.get("image")).name).toBe("fresh.png");
});

test("SeriesForm keeps existing edit image when replacement crop is canceled", async () => {
  const requests: CapturedRequest[] = [];
  renderEditForm(requests, (file) => Promise.resolve({ file, height: 1414, previewUrl: "blob:series-preview", width: 1000 }));

  fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [createImage("replacement.png")] } });
  const cropDialog = await screen.findByRole("dialog", { name: "이미지 crop" });
  fireEvent.click(within(cropDialog).getByRole("button", { name: "취소" }));
  fireEvent.submit(screen.getByRole("form", { name: "시리즈 수정 입력 화면" }));

  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/series/5001"));
  const updateBody = requests.find((request) => request.method === "PUT")?.body as FormData;
  expect(updateBody.get("image")).toBeNull();
});

test.each(["create", "edit"] as const)("SeriesForm shows an image preparation error when %s preview creation rejects", async (mode) => {
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", mode === "create" ? "/ai-characters/101/series/new" : "/ai-characters/101/series/5001/edit");
  render(<SeriesForm apiClient={createClient(requests)} characterId="101" createCropSource={() => Promise.reject(new Error("preview failed"))} genres={genres} mode={mode} renderCrop={(request) => Promise.resolve(request.file)} series={mode === "edit" ? existingSeries : undefined} />);

  fireEvent.change(screen.getByLabelText("시리즈 이미지"), { target: { files: [createImage("broken.png")] } });

  expect(await screen.findByRole("alert")).toHaveTextContent("이미지 미리보기 준비에 실패했습니다.");
  expect(screen.queryByRole("dialog", { name: "이미지 crop" })).not.toBeInTheDocument();
});
