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

import { CharacterCreatePage } from "@/features/characters/pages/CharacterCreatePage";
import { ApiError } from "@/shared/api/api-error";
import type { ApiClient, ApiRequestOptions } from "@/shared/api/client";
import type { CropRenderRequest } from "@/shared/lib/crop-image";
import type { CropSourceImage } from "@/shared/ui/image-crop-dialog";

// allow: SIZE_OK — P3 Character create page tests keep form validation, payload, crop, and retry contract coverage together.

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

function createPendingClient(requests: CapturedRequest[]) {
  let finishRequest: (() => void) | null = null;
  const finished = new Promise<void>((resolve) => {
    finishRequest = resolve;
  });
  const apiClient: ApiClient = {
    async request<Data>(options: ApiRequestOptions<Data>): Promise<Data> {
      requests.push({ body: options.body, method: options.method, path: options.path });
      await finished;
      return options.responseSchema.parse(null);
    },
  };

  return { apiClient, finishRequest: () => finishRequest?.() };
}

function createOriginalWorkClient(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.startsWith("/api/v2/admin/ai-characters/original-works/search")) {
        return options.responseSchema.parse([
          {
            id: 7,
            title: "달빛 상담소",
            contentType: "WEBTOON",
            category: "힐링",
            isAdult: false,
            description: "차분한 상담 원작",
            originalWork: "Moonlight Office",
            originalLink: "https://example.com/moonlight",
            writer: "하린",
            studio: "소다스튜디오",
            originalLinks: ["https://example.com/moonlight"],
            tags: ["상담", "힐링"],
            imageUrl: "https://example.com/moonlight.png",
          },
        ]);
      }

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

function createRejectOnceClient(requests: CapturedRequest[]): ApiClient {
  let submitCount = 0;

  return {
    async request<Data>(options: ApiRequestOptions<Data>): Promise<Data> {
      requests.push({ body: options.body, method: options.method, path: options.path });
      submitCount += 1;
      if (submitCount === 1) {
        throw new ApiError({ status: 500, message: "서버에서 캐릭터 생성을 거부했습니다.", errorProperty: null });
      }

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

function requireFormData(body: BodyInit | null | undefined): FormData {
  if (body instanceof FormData) {
    return body;
  }

  throw new TypeError("Expected FormData body");
}

async function readJsonPart(part: FormDataEntryValue | null): Promise<unknown> {
  if (part instanceof Blob) {
    return JSON.parse(await part.text());
  }
  if (typeof part === "string") {
    return JSON.parse(part);
  }

  return null;
}

function requireFile(part: FormDataEntryValue | null): File {
  if (part instanceof File) {
    return part;
  }

  throw new TypeError("Expected File part");
}

function createCropSource(file: File): Promise<CropSourceImage> {
  return Promise.resolve({ file, height: 1200, previewUrl: `blob:${file.name}`, width: 1200 });
}

function renderOriginalCrop(request: CropRenderRequest): Promise<File> {
  return Promise.resolve(request.file);
}

test("CharacterCreatePage shows required field errors without submitting", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  render(<CharacterCreatePage apiClient={apiClient} />);

  // When
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(await screen.findByText("이름을 입력하세요.")).toBeInTheDocument();
  expect(screen.getByText("시스템 프롬프트를 입력하세요.")).toBeInTheDocument();
  expect(screen.getByText("설명을 입력하세요.")).toBeInTheDocument();
  expect(screen.getByText("프로필 이미지를 선택하세요.")).toBeInTheDocument();
  expect(requests).toHaveLength(0);
});

test("CharacterCreatePage links required errors and focuses the first invalid field", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  render(<CharacterCreatePage apiClient={apiClient} />);

  // When
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  const nameInput = screen.getByLabelText("이름");
  const systemPromptInput = screen.getByLabelText("시스템 프롬프트");
  const descriptionInput = screen.getByLabelText("설명");
  expect(await screen.findByText("이름을 입력하세요.")).toHaveAttribute("id", "character-create-name-error");
  expect(screen.getByText("시스템 프롬프트를 입력하세요.")).toHaveAttribute("id", "character-create-system-prompt-error");
  expect(screen.getByText("설명을 입력하세요.")).toHaveAttribute("id", "character-create-description-error");
  expect(nameInput).toHaveAttribute("aria-describedby", "character-create-name-error");
  expect(systemPromptInput).toHaveAttribute("aria-describedby", "character-create-system-prompt-error");
  expect(descriptionInput).toHaveAttribute("aria-describedby", "character-create-description-error");
  expect(nameInput).toHaveAttribute("aria-invalid", "true");
  expect(systemPromptInput).toHaveAttribute("aria-invalid", "true");
  expect(descriptionInput).toHaveAttribute("aria-invalid", "true");
  expect(nameInput).toHaveFocus();
  expect(requests).toHaveLength(0);
});

test("CharacterCreatePage submits multipart once and returns to the list", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient, finishRequest } = createPendingClient(requests);
  const image = new File(["image"], "luna.png", { type: "image/png" });
  const croppedImage = new File(["cropped"], "luna-cropped.png", { type: "image/png" });
  window.history.pushState({}, "", "/ai-characters/new");
  render(<CharacterCreatePage apiClient={apiClient} createCropSource={createCropSource} renderCrop={() => Promise.resolve(croppedImage)} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });
  fireEvent.click(await screen.findByRole("button", { name: "적용" }));
  await waitFor(() => expect(screen.getByText("luna-cropped.png")).toBeInTheDocument());
  fireEvent.click(screen.getByRole("button", { name: "생성" }));
  await screen.findByRole("button", { name: "생성 중" });
  fireEvent.click(screen.getByRole("button", { name: "생성 중" }));

  // Then
  await waitFor(() => expect(requests).toHaveLength(1));
  expect(screen.getByRole("button", { name: "생성 중" })).toBeDisabled();
  const body = requireFormData(requests[0]?.body);
  expect(requests[0]).toMatchObject({ method: "POST", path: "/api/v2/admin/ai-characters" });
  expect(body.get("image")).toBe(croppedImage);
  expect(await readJsonPart(body.get("request"))).toEqual({ name: "루나", systemPrompt: "친절하게 답한다.", description: "차분한 상담형 캐릭터" });

  finishRequest();
  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters"));
});

test("CharacterCreatePage keeps entered values and retries after an ApiError rejection", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const image = new File(["image"], "luna.png", { type: "image/png" });
  window.history.pushState({}, "", "/ai-characters/new");
  render(<CharacterCreatePage apiClient={createRejectOnceClient(requests)} createCropSource={createCropSource} renderCrop={renderOriginalCrop} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });
  fireEvent.click(await screen.findByRole("button", { name: "적용" }));
  await waitFor(() => expect(screen.getByText("luna.png")).toBeInTheDocument());
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(await screen.findByText("서버에서 캐릭터 생성을 거부했습니다.")).toBeInTheDocument();
  expect(screen.getByRole("button", { name: "생성" })).toBeEnabled();
  expect(screen.getByLabelText("이름")).toHaveValue("루나");
  expect(screen.getByLabelText("시스템 프롬프트")).toHaveValue("친절하게 답한다.");
  expect(screen.getByLabelText("설명")).toHaveValue("차분한 상담형 캐릭터");

  // When
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  await waitFor(() => expect(window.location.pathname).toBe("/ai-characters"));
  expect(requests).toHaveLength(2);
});

test("CharacterCreatePage submits entered optional scalar and array fields", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const image = new File(["image"], "noa.png", { type: "image/png" });
  render(<CharacterCreatePage apiClient={apiClient} createCropSource={createCropSource} renderCrop={renderOriginalCrop} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: " 노아 " } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: " 차분하게 답한다. " } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: " 상담형 캐릭터 " } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });
  fireEvent.click(await screen.findByRole("button", { name: "적용" }));
  await waitFor(() => expect(screen.getByText("noa.png")).toBeInTheDocument());
  fireEvent.change(screen.getByLabelText("나이"), { target: { value: " 24 " } });
  fireEvent.change(screen.getByLabelText("성별"), { target: { value: " 여성 " } });
  fireEvent.change(screen.getByLabelText("MBTI"), { target: { value: " INFJ " } });
  fireEvent.change(screen.getByLabelText("말투 패턴"), { target: { value: " 존댓말 " } });
  fireEvent.change(screen.getByLabelText("말투 스타일"), { target: { value: " 부드러움 " } });
  fireEvent.change(screen.getByLabelText("외형"), { target: { value: " 긴 머리 " } });
  fireEvent.change(screen.getByLabelText("지역"), { target: { value: " KR " } });
  fireEvent.change(screen.getByLabelText("원작 제목"), { target: { value: " 달빛 상담소 " } });
  fireEvent.change(screen.getByLabelText("원작 링크"), { target: { value: " https://example.com/original " } });
  fireEvent.change(screen.getByLabelText("캐릭터 유형"), { target: { value: " Character " } });
  fireEvent.change(screen.getByLabelText("태그"), { target: { value: " 상담\n힐링 " } });
  fireEvent.change(screen.getByLabelText("취미"), { target: { value: " 독서\n산책 " } });
  fireEvent.change(screen.getByLabelText("가치관"), { target: { value: " 안전 " } });
  fireEvent.change(screen.getByLabelText("목표"), { target: { value: " 도움 " } });
  fireEvent.change(screen.getByLabelText("관계 인물명"), { target: { value: " 테오 " } });
  fireEvent.change(screen.getByLabelText("관계명"), { target: { value: " 친구 " } });
  fireEvent.change(screen.getByLabelText("관계 설명"), { target: { value: " 오랜 친구 " } });
  fireEvent.change(screen.getByLabelText("관계 중요도"), { target: { value: "3" } });
  fireEvent.change(screen.getByLabelText("관계 유형"), { target: { value: " friend " } });
  fireEvent.change(screen.getByLabelText("관계 현재 상태"), { target: { value: " active " } });
  fireEvent.change(screen.getByLabelText("성격 특성"), { target: { value: " 차분함 " } });
  fireEvent.change(screen.getByLabelText("성격 설명"), { target: { value: " 침착하게 응대 " } });
  fireEvent.change(screen.getByLabelText("배경 주제"), { target: { value: " 출신 " } });
  fireEvent.change(screen.getByLabelText("배경 설명"), { target: { value: " 달빛 상담소 " } });
  fireEvent.change(screen.getByLabelText("기억 제목"), { target: { value: " 첫 상담 " } });
  fireEvent.change(screen.getByLabelText("기억 내용"), { target: { value: " 따뜻한 기억 " } });
  fireEvent.change(screen.getByLabelText("기억 감정"), { target: { value: " calm " } });
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  await waitFor(() => expect(requests).toHaveLength(1));
  expect(await readJsonPart(requireFormData(requests[0]?.body).get("request"))).toEqual({
    name: "노아",
    systemPrompt: "차분하게 답한다.",
    description: "상담형 캐릭터",
    age: "24",
    gender: "여성",
    mbti: "INFJ",
    speechPattern: "존댓말",
    speechStyle: "부드러움",
    appearance: "긴 머리",
    region: "KR",
    originalTitle: "달빛 상담소",
    originalLink: "https://example.com/original",
    characterType: "Character",
    tags: ["상담", "힐링"],
    hobbies: ["독서", "산책"],
    values: ["안전"],
    goals: ["도움"],
    relationships: [{ personName: "테오", relationshipName: "친구", description: "오랜 친구", importance: 3, relationshipType: "friend", currentStatus: "active" }],
    personalities: [{ trait: "차분함", description: "침착하게 응대" }],
    backgrounds: [{ topic: "출신", description: "달빛 상담소" }],
    memories: [{ title: "첫 상담", content: "따뜻한 기억", emotion: "calm" }],
  });
});

test("CharacterCreatePage rejects unsupported image type and images over 10MB", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const unsupportedImage = new File(["image"], "luna.gif", { type: "image/gif" });
  const largeImage = new File([new Uint8Array(10 * 1024 * 1024 + 1)], "luna.png", { type: "image/png" });
  render(<CharacterCreatePage apiClient={apiClient} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [unsupportedImage] } });
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(await screen.findByText("JPEG 또는 PNG 파일만 업로드하세요.")).toBeInTheDocument();
  expect(requests).toHaveLength(0);

  // When
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [largeImage] } });
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(await screen.findByText("프로필 이미지는 10MB 이하만 업로드할 수 있습니다.")).toBeInTheDocument();
  expect(requests).toHaveLength(0);
});

test.each([
  ["jpg file with PNG MIME", new File(["image"], "luna.jpg", { type: "image/png" })],
  ["png file with JPEG MIME", new File(["image"], "luna.png", { type: "image/jpeg" })],
])("CharacterCreatePage rejects %s before crop or submit", async (_caseName, image) => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const createCropSource = vi.fn((file: File) => Promise.resolve({ file, height: 1200, previewUrl: "blob:luna", width: 1200 }));
  render(<CharacterCreatePage apiClient={apiClient} createCropSource={createCropSource} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(await screen.findByText("JPEG 또는 PNG 파일만 업로드하세요.")).toBeInTheDocument();
  expect(createCropSource).not.toHaveBeenCalled();
  expect(requests).toHaveLength(0);
});

test("CharacterCreatePage crops the selected image before submit", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const originalImage = new File(["original"], "luna.png", { type: "image/png" });
  const croppedImage = new File(["cropped"], "luna-cropped.png", { type: "image/png" });
  const renderCrop = () => Promise.resolve(croppedImage);
  render(
    <CharacterCreatePage
      apiClient={apiClient}
      createCropSource={(file) => Promise.resolve({ file, height: 1200, previewUrl: "blob:luna", width: 1200 })}
      renderCrop={renderCrop}
    />,
  );

  // When
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [originalImage] } });
  fireEvent.click(await screen.findByRole("button", { name: "적용" }));
  await waitFor(() => expect(screen.getByText("luna-cropped.png")).toBeInTheDocument());
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  await waitFor(() => expect(requests).toHaveLength(1));
  const submittedImage = requireFile(requireFormData(requests[0]?.body).get("image"));
  expect(submittedImage.name).toBe("luna-cropped.png");
  expect(await submittedImage.text()).toBe("cropped");
});

test("CharacterCreatePage blocks submit while crop source preparation is pending", async () => {
  // Given
  let resolveCropSource: (source: CropSourceImage) => void = () => undefined;
  const cropSourceReady = new Promise<CropSourceImage>((resolve) => {
    resolveCropSource = resolve;
  });
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const image = new File(["raw"], "luna.png", { type: "image/png" });
  render(<CharacterCreatePage apiClient={apiClient} createCropSource={() => cropSourceReady} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });

  // Then
  expect(screen.getByRole("button", { name: "생성" })).toBeDisabled();

  // When
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(requests).toHaveLength(0);

  // When
  resolveCropSource({ file: image, height: 1200, previewUrl: "blob:luna", width: 1200 });

  // Then
  expect(await screen.findByRole("dialog", { name: "이미지 crop" })).toBeInTheDocument();
});

test("CharacterCreatePage clears the selected image when crop is cancelled", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const image = new File(["raw"], "luna.png", { type: "image/png" });
  render(<CharacterCreatePage apiClient={apiClient} createCropSource={createCropSource} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });
  fireEvent.click(await screen.findByRole("button", { name: "취소" }));
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(await screen.findByText("프로필 이미지를 선택하세요.")).toBeInTheDocument();
  expect(screen.getByText("선택된 파일 없음")).toBeInTheDocument();
  expect(requests).toHaveLength(0);
});

test("CharacterCreatePage ignores a stale crop source from an earlier selection", async () => {
  // Given
  let resolveStaleCropSource: (source: CropSourceImage) => void = () => undefined;
  const staleCropSource = new Promise<CropSourceImage>((resolve) => {
    resolveStaleCropSource = resolve;
  });
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const staleImage = new File(["stale"], "stale.png", { type: "image/png" });
  const freshImage = new File(["fresh"], "fresh.png", { type: "image/png" });
  const createRaceCropSource = (file: File) => file.name === "stale.png" ? staleCropSource : createCropSource(file);
  render(<CharacterCreatePage apiClient={apiClient} createCropSource={createRaceCropSource} renderCrop={renderOriginalCrop} />);

  // When
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [staleImage] } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [freshImage] } });
  await screen.findByRole("dialog", { name: "이미지 crop" });
  await act(async () => {
    resolveStaleCropSource({ file: staleImage, height: 1200, previewUrl: "blob:stale", width: 1200 });
    await staleCropSource;
  });
  fireEvent.click(screen.getByRole("button", { name: "적용" }));

  // Then
  await waitFor(() => expect(screen.getByText("fresh.png")).toBeInTheDocument());
});

test("CharacterCreatePage shows an image error when crop source preparation rejects", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const { apiClient } = createPendingClient(requests);
  const image = new File(["raw"], "luna.png", { type: "image/png" });
  render(<CharacterCreatePage apiClient={apiClient} createCropSource={() => Promise.reject(new Error("preview failed"))} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  expect(await screen.findByText("이미지 미리보기 준비에 실패했습니다.")).toBeInTheDocument();
  expect(requests).toHaveLength(0);
});

test("P10-T1 CharacterCreatePage searches v2 original works after 300ms and submits selected originalWorkId", async () => {
  // Given
  const requests: CapturedRequest[] = [];
  const image = new File(["image"], "luna.png", { type: "image/png" });
  render(<CharacterCreatePage apiClient={createOriginalWorkClient(requests)} createCropSource={createCropSource} renderCrop={renderOriginalCrop} />);

  // When
  fireEvent.change(screen.getByLabelText("이름"), { target: { value: "루나" } });
  fireEvent.change(screen.getByLabelText("시스템 프롬프트"), { target: { value: "친절하게 답한다." } });
  fireEvent.change(screen.getByLabelText("설명"), { target: { value: "차분한 상담형 캐릭터" } });
  fireEvent.change(screen.getByLabelText("프로필 이미지"), { target: { files: [image] } });
  fireEvent.click(await screen.findByRole("button", { name: "적용" }));
  await waitFor(() => expect(screen.getByText("luna.png")).toBeInTheDocument());
  vi.useFakeTimers();
  fireEvent.change(screen.getByLabelText("원작 검색어"), { target: { value: "달빛" } });
  act(() => vi.advanceTimersByTime(299));
  expect(requests).toHaveLength(0);
  await act(async () => {
    vi.advanceTimersByTime(1);
    await Promise.resolve();
  });
  vi.useRealTimers();
  fireEvent.click(await screen.findByRole("button", { name: "달빛 상담소 선택" }));
  fireEvent.click(screen.getByRole("button", { name: "생성" }));

  // Then
  await waitFor(() => expect(requests).toHaveLength(2));
  expect(requests[0]?.path).toBe("/api/v2/admin/ai-characters/original-works/search?searchTerm=%EB%8B%AC%EB%B9%9B");
  expect(await readJsonPart(requireFormData(requests[1]?.body).get("request"))).toEqual({
    name: "루나",
    systemPrompt: "친절하게 답한다.",
    description: "차분한 상담형 캐릭터",
    originalWorkId: 7,
  });
});
