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

import { AudioContentFormPage } from "@/features/audio-contents/pages/AudioContentFormPage";
import type { CapturedRequest } from "@/features/audio-contents/tests/audio-form-test-support";
import { audioDetail, character, createFormClient, readJsonPart, requireFormData } from "@/features/audio-contents/tests/audio-form-test-support";
import type { ApiClient, ApiRequestOptions } from "@/shared/api/client";
import type { CropSourceImage } from "@/shared/ui/image-crop-dialog";

function createInactiveFormClient(requests: CapturedRequest[]): ApiClient {
  return {
    async request<Data>(options: ApiRequestOptions<Data>): Promise<Data> {
      requests.push({ body: options.body, method: options.method, path: options.path });
      if (options.path === "/api/v2/admin/ai-characters/101") {
        return options.responseSchema.parse({ ...character, isActive: false });
      }
      if (options.path === "/api/v2/admin/ai-characters/audio-content-themes") {
        return options.responseSchema.parse([{ id: 7, theme: "힐링", image: "https://cdn.example.com/theme/healing.png" }]);
      }
      if (options.path === "/api/v2/admin/ai-characters/101/audio-contents/9001") {
        return options.responseSchema.parse(audioDetail);
      }

      return options.responseSchema.parse(null);
    },
  };
}

test.each(["-1", "1.5", "100000"])("AudioContentFormPage rejects invalid edit price %s through the submit button", async (value) => {
  // Given
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(<AudioContentFormPage apiClient={createFormClient(requests)} characterId="101" contentId="9001" />);

  // When
  const priceInput = await screen.findByLabelText("가격");
  fireEvent.change(priceInput, { target: { value } });
  fireEvent.click(screen.getByRole("button", { name: "저장" }));

  // Then
  expect(priceInput).toHaveValue(Number(value));
  expect(await screen.findByText("가격은 0 이상 99,999 이하 정수 캔으로 입력하세요.")).toBeInTheDocument();
  expect(requests.filter((request) => request.method === "PUT")).toHaveLength(0);
});

test("AudioContentFormPage allows the maximum edit price", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(<AudioContentFormPage apiClient={createFormClient(requests)} characterId="101" contentId="9001" />);

  // When
  fireEvent.change(await screen.findByLabelText("가격"), { target: { value: "99999" } });
  fireEvent.click(screen.getByRole("button", { name: "저장" }));

  // Then
  await waitFor(() => expect(requests.filter((request) => request.method === "PUT")).toHaveLength(1));
  expect(await readJsonPart(requireFormData(requests.at(-1)?.body).get("request"))).toMatchObject({ price: 99999 });
});

test("AudioContentFormPage update omits unsupported controls and soft delete navigates to the audio list", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(
    <AudioContentFormPage
      apiClient={createFormClient(requests)}
      characterId="101"
      contentId="9001"
      createCropSource={(file) => Promise.resolve({ file, height: 800, previewUrl: "blob:replacement", width: 800 })}
    />,
  );

  // When
  fireEvent.change(await screen.findByLabelText("제목"), { target: { value: "수정 오디오" } });
  fireEvent.change(screen.getByLabelText("상세 설명"), { target: { value: "수정 설명" } });
  fireEvent.change(screen.getByLabelText("가격"), { target: { value: "0" } });
  fireEvent.click(screen.getByRole("button", { name: "저장" }));

  // Then
  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/audio-contents/9001"));
  expect(screen.queryByLabelText("오디오 파일")).not.toBeInTheDocument();
  expect(screen.queryByLabelText("오디오 테마")).not.toBeInTheDocument();
  expect(screen.queryByLabelText("예약 공개일")).not.toBeInTheDocument();
  expect(screen.getByText("시리즈: 현재 수정 화면에서는 변경할 수 없습니다.")).toBeInTheDocument();
  const updateBody = requireFormData(requests.at(-1)?.body);
  expect(updateBody.has("contentFile")).toBe(false);
  expect(await readJsonPart(updateBody.get("request"))).toEqual({
    title: "수정 오디오",
    detail: "수정 설명",
    tags: "상담,힐링",
    price: 0,
    isAdult: false,
    isPointAvailable: true,
    isCommentAvailable: true,
  });

  // When
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  fireEvent.click(screen.getByRole("button", { name: "비활성화" }));
  const dialog = screen.getByRole("alertdialog", { name: "오디오 콘텐츠 비활성화 확인" });
  expect(dialog).toHaveTextContent("수정 오디오");
  expect(dialog).toHaveTextContent("목록 노출만 중지하며 콘텐츠는 보관됩니다.");
  expect(dialog).not.toHaveTextContent("완전 삭제");
  fireEvent.click(screen.getByRole("button", { name: "비활성화 확인" }));

  // Then
  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/audio-contents"));
  expect(window.history.state).toMatchObject({ successNotification: "오디오 콘텐츠를 비활성화했습니다." });
  expect(await readJsonPart(requireFormData(requests.at(-1)?.body).get("request"))).toEqual({ isActive: false });
});

test("AudioContentFormPage sends one deactivate request while pending and shows retry guidance on failure", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  let rejectDeactivate: () => void = () => { throw new Error("Deactivate request was not started"); };
  const client: ApiClient = {
    async request<Data>(options: ApiRequestOptions<Data>): Promise<Data> {
      requests.push({ body: options.body, method: options.method, path: options.path });
      if (options.path === "/api/v2/admin/ai-characters/101") {
        return options.responseSchema.parse(character);
      }
      if (options.path === "/api/v2/admin/ai-characters/audio-content-themes") {
        return options.responseSchema.parse([{ id: 7, theme: "힐링", image: "https://cdn.example.com/theme/healing.png" }]);
      }
      if (options.path === "/api/v2/admin/ai-characters/101/audio-contents/9001" && options.method === undefined) {
        return options.responseSchema.parse(audioDetail);
      }
      await new Promise<void>((_, reject) => { rejectDeactivate = () => reject(new Error("fail")); });
      return options.responseSchema.parse(null);
    },
  };
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(<AudioContentFormPage apiClient={client} characterId="101" contentId="9001" />);
  fireEvent.click(await screen.findByRole("button", { name: "비활성화" }));

  // When
  fireEvent.click(screen.getByRole("button", { name: "비활성화 확인" }));
  fireEvent.click(screen.getByRole("button", { name: "처리 중" }));
  rejectDeactivate();

  // Then
  expect(requests.filter((request) => request.method === "PUT")).toHaveLength(1);
  expect(await screen.findByRole("alert")).toHaveTextContent("비활성화하지 못했습니다. 다시 시도하세요.");
});

test.each([
  ["create", undefined, "/ai-characters/101/audio-contents/new"],
  ["edit", "9001", "/ai-characters/101/audio-contents/9001/edit"],
])("AudioContentFormPage blocks the %s route for inactive characters", async (_mode, contentId, path) => {
  // Given
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", path);
  render(<AudioContentFormPage apiClient={createInactiveFormClient(requests)} characterId="101" contentId={contentId} />);

  // When
  await screen.findByText("비활성화된 AI 캐릭터에는 오디오 콘텐츠를 저장할 수 없습니다.");

  // Then
  expect(screen.queryByRole("form", { name: /오디오 콘텐츠/ })).not.toBeInTheDocument();
  expect(screen.queryByRole("button", { name: /생성|저장/ })).not.toBeInTheDocument();
  expect(requests.filter((request) => request.method === "POST" || request.method === "PUT")).toHaveLength(0);
});

test("AudioContentFormPage blocks edit save while replacement cover crop source is preparing", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(<AudioContentFormPage apiClient={createFormClient(requests)} characterId="101" contentId="9001" createCropSource={() => new Promise(() => undefined)} />);

  // When
  fireEvent.change(await screen.findByLabelText("커버 이미지"), { target: { files: [new File(["cover"], "pending.png", { type: "image/png" })] } });

  // Then
  await waitFor(() => expect(screen.getByRole("button", { name: "저장" })).toBeDisabled());
  fireEvent.click(screen.getByRole("button", { name: "저장" }));
  expect(requests.filter((request) => request.method === "PUT")).toHaveLength(0);
});

test("AudioContentFormPage keeps existing edit cover when replacement crop is canceled", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(
    <AudioContentFormPage
      apiClient={createFormClient(requests)}
      characterId="101"
      contentId="9001"
      createCropSource={(file) => Promise.resolve({ file, height: 800, previewUrl: "blob:replacement", width: 800 })}
    />,
  );

  // When
  fireEvent.change(await screen.findByLabelText("커버 이미지"), { target: { files: [new File(["cover"], "replacement.png", { type: "image/png" })] } });
  const cropDialog = await screen.findByRole("dialog", { name: "이미지 crop" });
  fireEvent.click(within(cropDialog).getByRole("button", { name: "취소" }));
  fireEvent.click(screen.getByRole("button", { name: "저장" }));

  // Then
  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/audio-contents/9001"));
  const updateRequest = requests.find((request) => request.method === "PUT");
  expect(requireFormData(updateRequest?.body).has("coverImage")).toBe(false);
});

test("AudioContentFormPage rejects a replacement cover MIME mismatch before crop preparation", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const createCropSource = vi.fn<(file: File) => Promise<CropSourceImage>>((file) => Promise.resolve({ file, height: 800, previewUrl: "blob:replacement", width: 800 }));
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(<AudioContentFormPage apiClient={createFormClient(requests)} characterId="101" contentId="9001" createCropSource={createCropSource} />);

  // When
  fireEvent.change(await screen.findByLabelText("커버 이미지"), { target: { files: [new File(["cover"], "replacement.png", { type: "image/jpeg" })] } });

  // Then
  expect(await screen.findByText("JPEG 또는 PNG 파일만 업로드하세요.")).toBeInTheDocument();
  expect(createCropSource).not.toHaveBeenCalled();
  expect(screen.queryByRole("dialog", { name: "이미지 crop" })).not.toBeInTheDocument();
});

test("AudioContentFormPage ignores stale edit cover sources and saves the latest applied replacement", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const cropResolvers = new Map<string, (source: CropSourceImage) => void>();
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(
    <AudioContentFormPage
      apiClient={createFormClient(requests)}
      characterId="101"
      contentId="9001"
      createCropSource={(file) => new Promise((resolve) => cropResolvers.set(file.name, resolve))}
      renderCrop={(request) => Promise.resolve(request.file)}
    />,
  );
  const staleFile = new File(["stale"], "stale.png", { type: "image/png" });
  const freshFile = new File(["fresh"], "fresh.png", { type: "image/png" });

  // When
  fireEvent.change(await screen.findByLabelText("커버 이미지"), { target: { files: [staleFile] } });
  fireEvent.change(screen.getByLabelText("커버 이미지"), { target: { files: [freshFile] } });
  cropResolvers.get("fresh.png")?.({ file: freshFile, height: 800, previewUrl: "blob:fresh", width: 800 });
  fireEvent.click(await screen.findByRole("button", { name: "적용" }));
  await waitFor(() => expect(screen.getByText("fresh.png")).toBeInTheDocument());
  cropResolvers.get("stale.png")?.({ file: staleFile, height: 800, previewUrl: "blob:stale", width: 800 });

  // Then
  await waitFor(() => expect(screen.queryByRole("dialog", { name: "이미지 crop" })).not.toBeInTheDocument());
  fireEvent.click(screen.getByRole("button", { name: "저장" }));
  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters/101/audio-contents/9001"));
  const coverPart = requireFormData(requests.at(-1)?.body).get("coverImage");
  if (!(coverPart instanceof File)) {
    throw new TypeError("Expected cover image file");
  }
  expect(coverPart.name).toBe("fresh.png");
});

test("AudioContentFormPage shows an edit cover preparation error when preview creation rejects", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  window.history.pushState({}, "", "/ai-characters/101/audio-contents/9001/edit");
  render(<AudioContentFormPage apiClient={createFormClient(requests)} characterId="101" contentId="9001" createCropSource={() => Promise.reject(new Error("preview failed"))} />);

  // When
  fireEvent.change(await screen.findByLabelText("커버 이미지"), { target: { files: [new File(["cover"], "broken.png", { type: "image/png" })] } });

  // Then
  expect(await screen.findByRole("alert")).toHaveTextContent("이미지 미리보기 준비에 실패했습니다.");
  expect(screen.getByRole("button", { name: "저장" })).toBeEnabled();
});
