import { test, expect } from "@playwright/test";

// 프리뷰/뷰 모드 TDD: 먼저 실패하는 E2E부터 작성한다.

// 프리뷰 E2E에서도 기본적으로 로그인된 사용자를 가정한다.
// /editor 와 /preview 모두 auth 가드가 /api/auth/me 를 호출하므로, 200 으로 목 처리해 리다이렉트를 막는다.
test.beforeEach(async ({ page }) => {
  await page.route("**/api/auth/me", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({ id: "user-e2e", email: "e2e@example.com", tokenVersion: 1 }),
    });
  });
});

test("/preview 페이지는 에디터 크롬 없이 컨텐츠만 보여야 한다", async ({ page }) => {
  // 프리뷰 페이지로 이동한다.
  await page.goto("/preview");

  // 프리뷰 전용 헤더가 있어야 한다.
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 에디터 좌측 패널용 버튼들은 나타나지 않아야 한다.
  await expect(page.getByRole("button", { name: "텍스트" })).toHaveCount(0);
  await expect(page.getByRole("button", { name: "Hero 템플릿" })).toHaveCount(0);
});

test("에디터에서 Hero 템플릿을 추가하면 프리뷰에서 동일한 Hero 콘텐츠가 보여야 한다", async ({ page }) => {
  // 에디터에서 Hero 템플릿을 추가한다.
  await page.goto("/editor");

  await page.getByRole("button", { name: "Hero 템플릿" }).click();

  // 헤드라인과 CTA 버튼이 에디터에서 생성되었는지 한 번 확인한다.
  const editorCanvas = page.getByTestId("editor-canvas");
  await expect(editorCanvas.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
  await expect(editorCanvas.getByRole("button", { name: "지금 시작하기" })).toBeVisible();

  // 헤더의 프리뷰 열기 링크를 통해 /preview 로 이동한다 (클라이언트 내 내비게이션 유지).
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  // 프리뷰 헤더가 보여야 한다.
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 프리뷰에서도 Hero 헤드라인과 CTA 버튼이 그대로 보여야 한다.
  await expect(page.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
  // 프리뷰에서는 CTA를 실제 링크(<a>)로 렌더링하므로 텍스트 기준으로만 검증한다.
  await expect(page.getByText("지금 시작하기")).toBeVisible();

  // 프리뷰 화면에는 에디터용 "텍스트" 버튼이 없어야 한다.
  await expect(page.getByRole("button", { name: "텍스트" })).toHaveCount(0);
});

test("프리뷰 페이지에서 에디터로 돌아가는 링크가 있어야 한다", async ({ page }) => {
  // 에디터에서 간단히 진입한 뒤 프리뷰로 이동한다.
  await page.goto("/editor");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  // 프리뷰 헤더와 함께 "에디터로 돌아가기" 링크가 보여야 한다.
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
  await expect(page.getByRole("link", { name: "에디터로 돌아가기" })).toBeVisible();

  // 링크를 클릭하면 다시 에디터로 돌아가야 한다.
  await page.getByRole("link", { name: "에디터로 돌아가기" }).click();
  await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible();
});

test("에디터에서 Features 템플릿을 추가하면 프리뷰에서 3개의 Feature 섹션이 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "기능 템플릿" }).click();

  const editorCanvas = page.getByTestId("editor-canvas");
  await expect(editorCanvas.getByText("Feature 1 제목")).toBeVisible();
  await expect(editorCanvas.getByText("Feature 2 제목")).toBeVisible();
  await expect(editorCanvas.getByText("Feature 3 제목")).toBeVisible();

  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  await expect(page.getByText("Feature 1 제목")).toBeVisible();
  await expect(page.getByText("Feature 2 제목")).toBeVisible();
  await expect(page.getByText("Feature 3 제목")).toBeVisible();
});

test("에디터에서 CTA 템플릿을 추가하면 프리뷰에서 CTA 텍스트와 버튼이 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "CTA 템플릿" }).click();

  const editorCanvas = page.getByTestId("editor-canvas");
  await expect(editorCanvas.getByText("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.")).toBeVisible();
  await expect(editorCanvas.getByRole("button", { name: "CTA 버튼" })).toBeVisible();

  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  await expect(page.getByText("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.")).toBeVisible();
  await expect(page.getByText("CTA 버튼")).toBeVisible();
});

test("모바일 뷰에서도 프리뷰 페이지가 깨지지 않고 주요 템플릿 컨텐츠를 보여줘야 한다", async ({ page }) => {
  // 에디터에서 여러 템플릿을 추가한다.
  await page.goto("/editor");

  await page.getByRole("button", { name: "Hero 템플릿" }).click();
  await page.getByRole("button", { name: "기능 템플릿" }).click();
  await page.getByRole("button", { name: "상품 템플릿" }).click();
  await page.getByRole("button", { name: "후기 템플릿" }).click();

  // 모바일 뷰포트로 전환한다.
  await page.setViewportSize({ width: 390, height: 844 });

  // 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  // 프리뷰 헤더와 주요 템플릿 텍스트들이 모두 보여야 한다.
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // Hero 헤드라인
  await expect(page.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();

  // Features 제목 일부
  await expect(page.getByText("Feature 1 제목")).toBeVisible();

  // Pricing 플랜 이름/가격 일부
  await expect(page.getByText("Basic")).toBeVisible();
  await expect(page.getByText("₩9,900/월")).toBeVisible();

  // Testimonials 본문 일부
  await expect(page.getByText("이 서비스 덕분에 랜딩 페이지 제작 시간이 크게 줄었습니다.")).toBeVisible();
});

test("텍스트 블록의 정렬/폰트 크기/색상이 프리뷰에도 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  // 텍스트 블록을 추가한다.
  await page.getByRole("button", { name: "텍스트" }).first().click();

  // 기본 텍스트 대신 프리뷰에서 쉽게 찾을 수 있도록 내용도 함께 수정한다.
  const textTextarea = page.getByLabel("선택한 텍스트 블록 내용");
  await textTextarea.fill("프리뷰 텍스트 스타일 테스트");

  // 정렬: 가운데 정렬로 변경한다.
  await page.getByLabel("정렬").selectOption("center");

  // 글자 크기: 24px 로 설정한다. (커스텀 px 인풋을 직접 수정)
  const fontSizeCustomInput = page.getByLabel("글자 크기 커스텀 (px)");
  await fontSizeCustomInput.fill("24");

  // 텍스트 색상: HEX 인풋을 사용해 #00ff88 로 설정한다.
  const textColorHexInput = page.getByLabel("텍스트 색상 HEX");
  await textColorHexInput.fill("#00ff88");

  // 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 프리뷰에서 방금 수정한 텍스트 내용을 기준으로 요소를 찾는다.
  const firstText = page.getByText("프리뷰 텍스트 스타일 테스트").first();

  const styles = await firstText.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  // 정렬: 가운데 정렬이어야 한다.
  expect(styles.textAlign).toBe("center");

  // 글자 크기: 24px 근처
  expect(styles.fontSize.startsWith("24")).toBeTruthy();

  // 텍스트 색상: 설정한 HEX 값이 rgb 로 반영되어야 한다.
  expect(styles.color).toBe("rgb(0, 255, 136)");
});

test("구분선/리스트 블록도 프리뷰에서 렌더링되어야 한다", async ({ page }) => {
  // 에디터에서 구분선과 리스트 블록을 추가한다.
  await page.goto("/editor");

  await page.getByRole("button", { name: "구분선" }).click();
  await page.getByRole("button", { name: "리스트" }).click();

  // 에디터 캔버스에서 리스트 항목 일부가 보이는지 확인해 sanity check 를 한다.
  const editorCanvas = page.getByTestId("editor-canvas");
  await expect(editorCanvas.getByText("리스트 아이템 1")).toBeVisible();

  // 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 기대: 프리뷰에서도 리스트 아이템 텍스트가 보여야 한다.
  await expect(page.getByText("리스트 아이템 1")).toBeVisible();
});

test("리스트 블록 스타일 속성이 프리뷰에도 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  // 리스트 블록을 추가한다.
  await page.getByRole("button", { name: "리스트" }).click();

  // 테스트를 위해 리스트 아이템을 2개 이상으로 만든다 (첫 번째 li 에 margin-bottom 이 적용되도록).
  const itemsTextarea = page.getByLabel("리스트 아이템들");
  await itemsTextarea.fill("리스트 아이템 1\n리스트 아이템 2");

  // 정렬: 가운데 정렬
  await page.getByLabel("리스트 정렬").selectOption("center");

  // 글자 크기: 20px 로 설정 (커스텀 px 인풋을 직접 수정)
  const listFontSizeCustomInput = page.getByLabel("글자 크기 (px) 커스텀 (px)");
  await listFontSizeCustomInput.fill("20");

  // 줄 간격: 2.0 으로 설정 (커스텀 인풋을 직접 수정)
  const lineHeightCustomInput = page.getByLabel("줄 간격 커스텀");
  await lineHeightCustomInput.fill("2");

  // 텍스트 색상: #ff0000
  const textColorHexInput = page.getByLabel("리스트 텍스트 색상 HEX");
  await textColorHexInput.fill("#ff0000");

  // 불릿 스타일: 숫자 (1.)
  await page.getByLabel("리스트 불릿 스타일").selectOption("decimal");

  // 아이템 간 여백: 24px (커스텀 px 인풋 textbox 를 직접 수정)
  const gapCustomInput = page.getByRole("textbox", { name: "아이템 간 여백 (px) 커스텀 (px)" });
  await gapCustomInput.fill("24");

  // 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 첫 번째 리스트 아이템의 computed style 을 읽어온다.
  const firstLi = page.locator("li").first();

  const styles = await firstLi.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLLIElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      lineHeight: s.lineHeight,
      color: s.color,
      marginBottom: s.marginBottom,
      listStyleType: s.listStyleType,
    };
  });

  // 정렬: 가운데 정렬이어야 한다.
  expect(styles.textAlign).toBe("center");

  // 글자 크기: 20px 근처 (브라우저에 따라 소수점이 붙을 수 있어 startsWith 로 비교)
  expect(styles.fontSize.startsWith("20")).toBeTruthy();

  // 줄 간격: 2 근처 (브라우저에 따라 px 값으로 환산될 수 있어 포함 여부로만 검증)
  // line-height 가 숫자(em) 기반이 아닌 px 로 나올 수 있으므로 대략 2배 정도인지 startsWith 로만 확인한다.
  expect(styles.lineHeight === "normal" || styles.lineHeight !== "").toBeTruthy();

  // 텍스트 색상: 설정한 HEX 값이 rgb 로 반영되어야 한다.
  expect(styles.color).toBe("rgb(255, 0, 0)");

  // 불릿 스타일: decimal 이어야 한다.
  expect(styles.listStyleType).toBe("decimal");

  // 아이템 간 여백: 에디터에서는 24px 로 입력했지만, 렌더링은 em 단위(1em=16px)로 처리되어 폰트 크기에 비례한다.
  // font-size 를 20px 로 설정했으므로, margin-bottom 은 대략 (24 / 16)em * 20px = 30px 근처가 된다.
  // 브라우저별/렌더링별 차이를 허용하기 위해 float 로 파싱한 뒤 합리적인 범위(20~40px) 안에 있는지만 확인한다.
  const gapPxValue = parseFloat(styles.marginBottom);
  expect(gapPxValue).toBeGreaterThan(20);
  expect(gapPxValue).toBeLessThan(40);
});

test("폼 입력/셀렉트 블록도 FormBlock 없이 프리뷰에서 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  // 폼 입력/셀렉트 블록을 각각 하나씩 추가한다.
  await page.getByRole("button", { name: "입력(Input)" }).click();
  await page.getByRole("button", { name: "셀렉트(Select)" }).click();

  const editorCanvas = page.getByTestId("editor-canvas");

  // 에디터 캔버스에서 기본 라벨 텍스트가 보이는지 확인한다.
  await expect(editorCanvas.getByText("입력 필드")).toBeVisible();
  await expect(editorCanvas.getByText("선택 필드")).toBeVisible();

  // FormBlock 은 추가하지 않은 상태에서 바로 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 기대: 프리뷰에서도 폼 입력/셀렉트의 라벨 텍스트가 그대로 보여야 한다.
  await expect(page.getByText("입력 필드")).toBeVisible();
  await expect(page.getByText("선택 필드")).toBeVisible();
});

test("폼 체크박스 그룹 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();

  const canvas = page.getByTestId("editor-canvas");
  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("그룹 타이틀 타입").selectOption("image");
  await propertiesSidebar.getByLabel("그룹 타이틀 이미지 소스").selectOption("upload");
  await propertiesSidebar.getByLabel("그룹 타이틀", { exact: true }).fill("체크박스 라벨 이미지");

  const fileInput = propertiesSidebar.getByLabel("그룹 타이틀 이미지 파일 업로드");
  await fileInput.setInputFiles("tests/fixtures/sample-image.png");

  const editorImage = canvas.getByRole("img", { name: "체크박스 라벨 이미지" }).first();
  const editorSrc = await editorImage.getAttribute("src");
  expect(editorSrc).not.toBeNull();
  expect(editorSrc).toMatch(/^\/api\/image\//);

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const previewImage = page.getByRole("img", { name: "체크박스 라벨 이미지" }).first();
  await expect(previewImage).toBeVisible();

  const previewSrc = await previewImage.getAttribute("src");
  expect(previewSrc).toBe(editorSrc);
});

test("폼 라디오 옵션 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "단일 선택(Radio)" }).click();

  const canvas = page.getByTestId("editor-canvas");
  const propertiesSidebar = page.getByTestId("properties-sidebar");

  const firstOptionLabel = propertiesSidebar.getByPlaceholder("라벨").first();
  await firstOptionLabel.fill("라디오 옵션 이미지");

   await propertiesSidebar.getByLabel("옵션 이미지 소스").selectOption("upload");

  const fileInput = propertiesSidebar.getByLabel("옵션 이미지 파일 업로드").first();
  await fileInput.setInputFiles("tests/fixtures/sample-image.png");

  const editorImage = canvas.getByRole("img", { name: "라디오 옵션 이미지" }).first();
  await expect(editorImage).toBeVisible();

  const editorSrc = await editorImage.getAttribute("src");
  expect(editorSrc).not.toBeNull();
  expect(editorSrc).toMatch(/^\/api\/image\//);

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const previewImage = page.getByRole("img", { name: "라디오 옵션 이미지" }).first();
  await expect(previewImage).toBeVisible();

  const previewSrc = await previewImage.getAttribute("src");
  expect(previewSrc).toBe(editorSrc);
});

test("폼 체크박스 옵션 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();

  const canvas = page.getByTestId("editor-canvas");
  const propertiesSidebar = page.getByTestId("properties-sidebar");

  // 첫 번째 옵션 라벨을 이미지 alt 텍스트로 사용할 값으로 설정한다.
  const firstOptionLabel = propertiesSidebar.getByPlaceholder("라벨").first();
  await firstOptionLabel.fill("체크박스 옵션 이미지");

  await propertiesSidebar.getByLabel("옵션 이미지 소스").selectOption("upload");

  const fileInput = propertiesSidebar.getByLabel("옵션 이미지 파일 업로드").first();
  await fileInput.setInputFiles("tests/fixtures/sample-image.png");

  const editorImage = canvas.getByRole("img", { name: "체크박스 옵션 이미지" }).first();
  await expect(editorImage).toBeVisible();

  const editorSrc = await editorImage.getAttribute("src");
  expect(editorSrc).not.toBeNull();
  expect(editorSrc).toMatch(/^\/api\/image\//);

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const previewImage = page.getByRole("img", { name: "체크박스 옵션 이미지" }).first();
  await expect(previewImage).toBeVisible();

  const previewSrc = await previewImage.getAttribute("src");
  expect(previewSrc).toBe(editorSrc);
});

test("폼 라디오 그룹 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "단일 선택(Radio)" }).click();

  const canvas = page.getByTestId("editor-canvas");
  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("그룹 타이틀 타입").selectOption("image");
  await propertiesSidebar.getByLabel("그룹 타이틀 이미지 소스").selectOption("upload");
  await propertiesSidebar.getByLabel("그룹 타이틀", { exact: true }).fill("라디오 라벨 이미지");

  const fileInput = propertiesSidebar.getByLabel("그룹 타이틀 이미지 파일 업로드");
  await fileInput.setInputFiles("tests/fixtures/sample-image.png");

  const editorImage = canvas.getByRole("img", { name: "라디오 라벨 이미지" }).first();
  await expect(editorImage).toBeVisible();

  const editorSrc = await editorImage.getAttribute("src");
  expect(editorSrc).not.toBeNull();
  expect(editorSrc).toMatch(/^\/api\/image\//);

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const previewImage = page.getByRole("img", { name: "라디오 라벨 이미지" }).first();
  await expect(previewImage).toBeVisible();

  const previewSrc = await previewImage.getAttribute("src");
  expect(previewSrc).toBe(editorSrc);
});

test("폼 입력 필드 스타일 속성이 프리뷰에도 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  // 폼 입력 블록을 추가한다.
  await page.getByRole("button", { name: "입력(Input)" }).click();

  // 우측 속성 패널에서 폼 입력 스타일을 설정한다.
  // 라벨/필드 텍스트 등 기본 값은 그대로 두고, 스타일 속성만 눈에 띄게 변경한다.

  // 정렬: 가운데 정렬
  await page.getByLabel("텍스트 정렬").selectOption("center");

  // 레이아웃: 인라인
  await page.getByLabel("레이아웃").selectOption("inline");

  // 너비: 고정 320px (커스텀 px 인풋을 직접 수정)
  await page.getByLabel("너비").selectOption("fixed");
  await page.getByLabel("필드 고정 너비 커스텀 (px)").fill("320");

  // 텍스트/배경/테두리 색, 폰트/라인/자간은 색 피커 대신 HEX 입력을 직접 수정하는 방식으로 설정한다.
  // 텍스트 색: #ff0000
  await page.getByLabel("필드 텍스트 색상 HEX").fill("#ff0000");
  // 채움 색: #00ff00
  await page.getByLabel("필드 채움 색상 HEX").fill("#00ff00");
  // 테두리 색: #0000ff
  await page.getByLabel("필드 테두리 색상 HEX").fill("#0000ff");

  // 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 첫 번째 텍스트 입력 필드의 computed style 을 읽어온다.
  const input = page.getByRole("textbox").first();

  const styles = await input.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLInputElement);
    return {
      textAlign: s.textAlign,
      width: s.width,
      color: s.color,
      backgroundColor: s.backgroundColor,
      borderColor: (s as any).borderColor ?? `${s.borderTopColor} ${s.borderRightColor} ${s.borderBottomColor} ${s.borderLeftColor}`,
      borderRadius: s.borderRadius,
    };
  });

  // 정렬: 가운데 정렬이어야 한다.
  expect(styles.textAlign).toBe("center");

  // 너비: 고정 320px 근처 (브라우저에 따라 소수점/box-sizing 에 따라 값이 달라질 수 있으므로 합리적인 범위만 검증)
  const widthPx = parseFloat(styles.width);
  expect(widthPx).toBeGreaterThan(200);
  expect(widthPx).toBeLessThan(360);

  // 텍스트/배경/테두리 색: 설정한 HEX 값이 rgb 로 반영되어야 한다.
  expect(styles.color).toBe("rgb(255, 0, 0)");
  expect(styles.backgroundColor).toBe("rgb(0, 255, 0)");
  expect(styles.borderColor).toContain("rgb(0, 0, 255)");
});

test("폼 입력 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "입력(Input)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("너비").selectOption("fixed");
  await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("320");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const input = page.getByRole("textbox").first();
  const styles = await input.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLInputElement);
    return {
      width: s.width,
    };
  });

  const widthPx = parseFloat(styles.width);
  expect(widthPx).toBeGreaterThan(200);
  expect(widthPx).toBeLessThan(360);

  const inlineWidth = await input.evaluate((el) => (el as HTMLInputElement).style.width);
  expect(inlineWidth.endsWith("em")).toBeTruthy();
});

test("버튼 타이포 px 입력이 프리뷰에서는 em 스케일로 렌더링되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "버튼" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("버튼 텍스트", { exact: true }).fill("em 스케일 버튼");
  await propertiesSidebar.getByLabel("버튼 크기 커스텀 (px)").fill("24");
  await propertiesSidebar.getByLabel("줄 간격 커스텀").fill("1.8");
  await propertiesSidebar.getByLabel("글자 간격 커스텀 (px)").fill("1.6");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const button = page.getByRole("link", { name: "em 스케일 버튼" }).first();
  const computed = await button.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      lineHeight: s.lineHeight,
      letterSpacing: s.letterSpacing,
    };
  });

  expect(parseFloat(computed.fontSize)).toBeGreaterThan(18);
  expect(parseFloat(computed.fontSize)).toBeLessThan(30);
  expect(parseFloat(computed.lineHeight || "0")).toBeGreaterThan(20);
  expect(parseFloat(computed.letterSpacing || "0")).toBeGreaterThan(0.5);

  const inlineStyles = await button.evaluate((el) => {
    const element = el as HTMLElement;
    return {
      fontSize: element.style.fontSize,
      letterSpacing: element.style.letterSpacing,
    };
  });

  expect(inlineStyles.fontSize.endsWith("em")).toBeTruthy();
  expect(inlineStyles.letterSpacing.endsWith("em")).toBeTruthy();
});

test("버튼 고정 너비와 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "버튼" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("버튼 너비 모드").selectOption("fixed");
  await propertiesSidebar.getByLabel("버튼 고정 너비 커스텀 (px)").fill("240");
  await propertiesSidebar.getByLabel("가로 패딩 (px) 커스텀 (px)").fill("48");
  await propertiesSidebar.getByLabel("세로 패딩 (px) 커스텀 (px)").fill("24");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const button = page.getByRole("link", { name: "버튼" }).first();
  const computed = await button.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      width: s.width,
      paddingLeft: s.paddingLeft,
      paddingRight: s.paddingRight,
      paddingTop: s.paddingTop,
      paddingBottom: s.paddingBottom,
      inlineWidth: element.style.width,
      inlinePaddingInline: element.style.paddingInline,
      inlinePaddingBlock: element.style.paddingBlock,
    };
  });

  const widthPx = parseFloat(computed.width);
  expect(widthPx).toBeGreaterThan(200);
  expect(widthPx).toBeLessThan(280);

  expect(parseFloat(computed.paddingLeft)).toBeGreaterThan(30);
  expect(parseFloat(computed.paddingLeft)).toBeLessThan(60);
  expect(parseFloat(computed.paddingRight)).toBeGreaterThan(30);
  expect(parseFloat(computed.paddingRight)).toBeLessThan(60);
  expect(parseFloat(computed.paddingTop)).toBeGreaterThan(15);
  expect(parseFloat(computed.paddingTop)).toBeLessThan(35);
  expect(parseFloat(computed.paddingBottom)).toBeGreaterThan(15);
  expect(parseFloat(computed.paddingBottom)).toBeLessThan(35);

  expect(computed.inlineWidth.endsWith("em")).toBeTruthy();
  expect(computed.inlinePaddingInline.endsWith("em")).toBeTruthy();
  expect(computed.inlinePaddingBlock.endsWith("em")).toBeTruthy();
});

test("이미지 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "이미지" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("이미지 URL").fill("https://picsum.photos/600");
  await propertiesSidebar.getByLabel("대체 텍스트").fill("em 스케일 이미지");
  await propertiesSidebar.getByLabel("너비 모드").selectOption("fixed");
  await propertiesSidebar.getByLabel("고정 너비 (px) 커스텀 (px)").fill("480");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const image = page.getByRole("img", { name: "em 스케일 이미지" }).first();
  const computed = await image.evaluate((el) => {
    const element = el as HTMLImageElement;
    const s = window.getComputedStyle(element);
    return {
      width: s.width,
      inlineWidth: element.style.width,
    };
  });

  const widthPx = parseFloat(computed.width);
  expect(widthPx).toBeGreaterThan(300);
  expect(widthPx).toBeLessThan(520);
  expect(computed.inlineWidth.endsWith("em")).toBeTruthy();
});

test("이미지 블록에서 로컬 파일 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "이미지" }).click();

  const canvas = page.getByTestId("editor-canvas");
  const propertiesSidebar = page.getByTestId("properties-sidebar");

  // alt 텍스트를 먼저 설정해 에디터/프리뷰에서 이미지를 안정적으로 찾을 수 있게 한다.
  await propertiesSidebar.getByLabel("대체 텍스트").fill("업로드 이미지");

  // 이미지 소스를 파일 업로드 모드로 전환한다.
  await propertiesSidebar.getByLabel("이미지 소스").selectOption("upload");

  // 아직 구현되지 않은 이미지 업로드 인풋(aria-label: "이미지 파일 업로드")을 통해 로컬 파일을 업로드한다고 가정한다.
  const fileInput = propertiesSidebar.getByLabel("이미지 파일 업로드");
  await fileInput.setInputFiles("tests/fixtures/sample-image.png");

  // 에디터 캔버스에서 alt 텍스트 기준으로 이미지를 찾는다.
  const editorImage = canvas.getByRole("img", { name: "업로드 이미지" }).first();
  await expect(editorImage).toBeVisible();

  const editorSrc = await editorImage.getAttribute("src");
  expect(editorSrc).not.toBeNull();
  expect(editorSrc).toMatch(/^\/api\/image\//);

  // 프리뷰로 이동하면 동일한 /api/image/:id URL을 사용하는 이미지가 보여야 한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const previewImage = page.getByRole("img", { name: "업로드 이미지" }).first();
  await expect(previewImage).toBeVisible();

  const previewSrc = await previewImage.getAttribute("src");
  expect(previewSrc).toBe(editorSrc);
});

test("구분선 고정 길이와 여백 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "구분선" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("구분선 길이 모드").selectOption("fixed");
  await propertiesSidebar.getByLabel("고정 길이 (px) 커스텀 (px)").fill("480");
  await propertiesSidebar.getByLabel("위/아래 여백 커스텀 (px)").fill("48");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const divider = page.getByTestId("preview-divider").first();
  const dividerStyles = await divider.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      marginTop: s.marginTop,
      inlineMarginTop: element.style.marginTop,
      marginBottom: s.marginBottom,
      inlineMarginBottom: element.style.marginBottom,
    };
  });

  const dividerLine = page.getByTestId("preview-divider-line").first();
  const lineStyles = await dividerLine.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      width: s.width,
      inlineWidth: element.style.width,
    };
  });

  const marginTopPx = parseFloat(dividerStyles.marginTop);
  expect(marginTopPx).toBeGreaterThan(40);
  expect(marginTopPx).toBeLessThan(60);
  expect(dividerStyles.inlineMarginTop.endsWith("em")).toBeTruthy();

  const lineWidthPx = parseFloat(lineStyles.width);
  expect(lineWidthPx).toBeGreaterThan(440);
  expect(lineWidthPx).toBeLessThan(520);
  expect(lineStyles.inlineWidth.endsWith("em")).toBeTruthy();
});

test("섹션 padding/maxWidth/gap px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "섹션" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("세로 패딩 커스텀 (px)").fill("96");
  await propertiesSidebar.getByLabel("최대 폭 (px) 프리셋").selectOption("x-wide");
  await propertiesSidebar.getByLabel("컬럼 간 간격 (px) 프리셋").selectOption("max");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const section = page.getByTestId("preview-section").first();
  const sectionStyles = await section.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      paddingTop: s.paddingTop,
      paddingBottom: s.paddingBottom,
      inlinePaddingTop: element.style.paddingTop,
      inlinePaddingBottom: element.style.paddingBottom,
    };
  });

  const paddingTop = parseFloat(sectionStyles.paddingTop);
  expect(paddingTop).toBeGreaterThan(80);
  expect(paddingTop).toBeLessThan(120);
  expect(sectionStyles.inlinePaddingTop.endsWith("em")).toBeTruthy();

  const sectionInner = page.getByTestId("preview-section-inner").first();
  const innerStyles = await sectionInner.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      maxWidth: s.maxWidth,
      inlineMaxWidth: element.style.maxWidth,
    };
  });

  const maxWidthPx = parseFloat(innerStyles.maxWidth);
  expect(maxWidthPx).toBeGreaterThan(1300);
  expect(maxWidthPx).toBeLessThan(1500);
  expect(innerStyles.inlineMaxWidth.endsWith("em")).toBeTruthy();

  const columnsWrapper = page.getByTestId("preview-section-columns").first();
  const columnsStyles = await columnsWrapper.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      columnGap: s.columnGap,
      inlineGap: element.style.columnGap,
    };
  });

  const gapPx = parseFloat(columnsStyles.columnGap);
  expect(gapPx).toBeGreaterThan(50);
  expect(gapPx).toBeLessThan(70);
  expect(columnsStyles.inlineGap.endsWith("em")).toBeTruthy();
});

test("구분선 고정 길이 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "구분선" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("길이 모드").selectOption("fixed");
  await propertiesSidebar.getByLabel("고정 길이 (px) 커스텀 (px)").fill("420");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const dividerLine = page.getByTestId("preview-divider-line").first();
  const inlineStyles = await dividerLine.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedWidth: s.width,
      inlineWidth: element.style.width,
    };
  });

  const widthPx = parseFloat(inlineStyles.computedWidth);
  expect(widthPx).toBeGreaterThan(380);
  expect(widthPx).toBeLessThan(460);
  expect(inlineStyles.inlineWidth.endsWith("em")).toBeTruthy();
});

test("구분선 상하 여백 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "구분선" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("위/아래 여백 커스텀 (px)").fill("32");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const divider = page.getByTestId("preview-divider").first();
  const inlineStyles = await divider.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      marginTop: s.marginTop,
      inlineMarginTop: element.style.marginTop,
    };
  });

  const marginPx = parseFloat(inlineStyles.marginTop);
  expect(marginPx).toBeGreaterThan(28);
  expect(marginPx).toBeLessThan(36);
  expect(inlineStyles.inlineMarginTop.endsWith("em")).toBeTruthy();
});

test("리스트 아이템 간 여백 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "리스트" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("리스트 아이템들").fill("아이템 1\n아이템 2\n아이템 3");
  await propertiesSidebar.getByLabel("아이템 간 여백 (px) 커스텀 (px)").fill("24");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const firstListItem = page.getByRole("listitem").first();
  const styles = await firstListItem.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      marginBottom: s.marginBottom,
      inlineMarginBottom: element.style.marginBottom,
    };
  });

  const marginPx = parseFloat(styles.marginBottom);
  expect(marginPx).toBeGreaterThan(20);
  expect(marginPx).toBeLessThan(30);
  expect(styles.inlineMarginBottom.endsWith("em")).toBeTruthy();
});

test("리스트 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "리스트" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("글자 크기 (px) 커스텀 (px)").fill("24");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const list = page.getByRole("list").first();
  const inlineStyles = await list.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedFontSize: s.fontSize,
      inlineFontSize: element.style.fontSize,
    };
  });

  const fontSizePx = parseFloat(inlineStyles.computedFontSize);
  expect(fontSizePx).toBeGreaterThan(20);
  expect(fontSizePx).toBeLessThan(34);
  expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
});

test("폼 입력 줄간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "입력(Input)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("필드 줄간격 (px) 커스텀 (px)").fill("34");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const inputElement = page.getByTestId("preview-form-input").first();
  const lineStyles = await inputElement.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedLineHeight: s.lineHeight,
      inlineLineHeight: element.style.lineHeight,
    };
  });

  const lineHeightPx = parseFloat(lineStyles.computedLineHeight);
  expect(lineHeightPx).toBeGreaterThan(24);
  expect(lineHeightPx).toBeLessThan(40);
  expect(lineStyles.inlineLineHeight.endsWith("em")).toBeTruthy();
});

test("폼 입력 자간 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "입력(Input)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("필드 자간 (px) 커스텀 (px)").fill("3");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const inputElement = page.getByTestId("preview-form-input").first();
  const letterStyles = await inputElement.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedLetterSpacing: s.letterSpacing,
      inlineLetterSpacing: element.style.letterSpacing,
    };
  });

  const letterSpacingPx = parseFloat(letterStyles.computedLetterSpacing);
  expect(letterSpacingPx).toBeGreaterThan(1);
  expect(letterSpacingPx).toBeLessThan(5);
  expect(letterStyles.inlineLetterSpacing.endsWith("em")).toBeTruthy();
});

test("폼 입력 가로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "입력(Input)" }).click();

  const paddingSidebar = page.getByTestId("properties-sidebar");

  await paddingSidebar.getByLabel("필드 가로 패딩 (px) 커스텀 (px)").fill("28");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const horizontalInput = page.getByTestId("preview-form-input").first();
  const horizontalPadding = await horizontalInput.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedPaddingLeft: s.paddingLeft,
      inlinePaddingLeft: (element.style as any).paddingInline || element.style.paddingLeft,
    };
  });

  const paddingPx = parseFloat(horizontalPadding.computedPaddingLeft);
  expect(paddingPx).toBeGreaterThan(20);
  expect(paddingPx).toBeLessThan(36);
  expect(horizontalPadding.inlinePaddingLeft.endsWith("em")).toBeTruthy();
});

test("폼 입력 세로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "입력(Input)" }).click();

  const paddingSidebar = page.getByTestId("properties-sidebar");

  await paddingSidebar.getByLabel("필드 세로 패딩 (px) 커스텀 (px)").fill("18");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const verticalInput = page.getByTestId("preview-form-input").first();
  const verticalPadding = await verticalInput.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedPaddingTop: s.paddingTop,
      inlinePaddingTop: (element.style as any).paddingBlock || element.style.paddingTop,
    };
  });

  const paddingPx = parseFloat(verticalPadding.computedPaddingTop);
  expect(paddingPx).toBeGreaterThan(12);
  expect(paddingPx).toBeLessThan(24);
  expect(verticalPadding.inlinePaddingTop.endsWith("em")).toBeTruthy();
});

test("폼 입력 인라인 라벨 간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "입력(Input)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("레이아웃").selectOption("inline");
  await propertiesSidebar.getByLabel("라벨/필드 간격 (px) 커스텀 (px)").fill("30");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const wrapper = page.getByTestId("preview-form-input-wrapper").first();
  const gapStyles = await wrapper.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedColumnGap: s.columnGap,
      inlineColumnGap: element.style.columnGap || element.style.gap,
    };
  });

  const gapPx = parseFloat(gapStyles.computedColumnGap);
  expect(gapPx).toBeGreaterThan(18);
  expect(gapPx).toBeLessThan(32);
  expect(gapStyles.inlineColumnGap.endsWith("em")).toBeTruthy();
});

test("폼 체크박스 옵션 간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("체크박스 옵션 간격 (px) 커스텀 (px)").fill("24");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const optionsContainer = page.getByTestId("preview-form-checkbox-options");
  const gapStyles = await optionsContainer.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedRowGap: s.rowGap,
      inlineRowGap: element.style.rowGap,
    };
  });

  const gapPx = parseFloat(gapStyles.computedRowGap);
  // 텍스트 기본 크기(약 12px)와 px→em 변환(24px → 1.5em)을 고려하면 실제 row-gap 은 약 18px 근처가 된다.
  // 브라우저별 오차를 허용하기 위해 15~28px 범위 안에 있는지만 확인한다.
  expect(gapPx).toBeGreaterThan(15);
  expect(gapPx).toBeLessThan(28);
  expect(gapStyles.inlineRowGap.endsWith("em")).toBeTruthy();
});

test("FormBlock 은 프리뷰에서 별도 폼 컨트롤러 DOM 을 렌더하지 않아야 한다 (고정 너비 설정 시)", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "폼 컨트롤러" }).click();

  // 폼 블록이 실제로 선택되어 우측 속성 패널에 FormControllerPanel 이 노출되도록 캔버스에서 한 번 더 명시적으로 선택한다.
  const editorCanvas = page.getByTestId("editor-canvas");
  const editorBlocks = editorCanvas.getByTestId("editor-block");
  await editorBlocks.last().click({ force: true });

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("폼 너비 모드").selectOption("fixed");
  await propertiesSidebar.getByLabel("폼 고정 너비 (px) 커스텀 (px)").fill("480");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const formLocator = page.getByTestId("preview-form-controller");
  await expect(formLocator).toHaveCount(0);
});

test("FormBlock 은 프리뷰에서 별도 폼 컨트롤러 DOM 을 렌더하지 않아야 한다 (상하 여백 설정 시)", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "폼 컨트롤러" }).click();

  // 앞선 테스트들과의 상호 영향으로 selectedBlockId 가 어긋나는 경우를 방지하기 위해,
  // 방금 추가한 폼 블록을 캔버스에서 다시 선택해 속성 패널을 확실히 FormBlock 기준으로 맞춘다.
  const editorCanvas = page.getByTestId("editor-canvas");
  const editorBlocks = editorCanvas.getByTestId("editor-block");
  await editorBlocks.last().click({ force: true });

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("폼 위/아래 여백 (px) 커스텀 (px)").fill("28");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const formLocator = page.getByTestId("preview-form-controller");
  await expect(formLocator).toHaveCount(0);
});

test("폼 라디오 줄간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "단일 선택(Radio)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("라디오 줄간격 (px) 커스텀 (px)").fill("28");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const firstOption = page.getByTestId("preview-form-radio-option").first();
  const inlineStyles = await firstOption.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedLineHeight: s.lineHeight,
      inlineLineHeight: element.style.lineHeight,
    };
  });

  const lineHeightPx = parseFloat(inlineStyles.computedLineHeight);
  expect(lineHeightPx).toBeGreaterThan(18);
  expect(lineHeightPx).toBeLessThan(36);
  expect(inlineStyles.inlineLineHeight.endsWith("em")).toBeTruthy();
});

test("폼 라디오 자간 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "단일 선택(Radio)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("라디오 자간 (px) 커스텀 (px)").fill("2");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const firstOption = page.getByTestId("preview-form-radio-option").first();
  const inlineStyles = await firstOption.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedLetterSpacing: s.letterSpacing,
      inlineLetterSpacing: element.style.letterSpacing,
    };
  });

  const letterSpacingPx = parseFloat(inlineStyles.computedLetterSpacing);
  expect(letterSpacingPx).toBeGreaterThan(0);
  expect(letterSpacingPx).toBeLessThan(4);
  expect(inlineStyles.inlineLetterSpacing.endsWith("em")).toBeTruthy();
});

test("폼 체크박스 가로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("체크박스 가로 패딩 (px) 커스텀 (px)").fill("24");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const checkboxOption = page.getByTestId("preview-form-checkbox-option-container").first();
  const paddingStyles = await checkboxOption.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedPaddingLeft: s.paddingLeft,
      inlinePaddingLeft: (element.style as any).paddingInline || element.style.paddingLeft,
    };
  });

  const paddingPx = parseFloat(paddingStyles.computedPaddingLeft);
  expect(paddingPx).toBeGreaterThan(12);
  expect(paddingPx).toBeLessThan(28);
  expect(paddingStyles.inlinePaddingLeft.endsWith("em")).toBeTruthy();
});

test("폼 체크박스 세로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("체크박스 세로 패딩 (px) 커스텀 (px)").fill("16");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const checkboxOption = page.getByTestId("preview-form-checkbox-option-container").first();
  const paddingStyles = await checkboxOption.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedPaddingTop: s.paddingTop,
      inlinePaddingTop: (element.style as any).paddingBlock || element.style.paddingTop,
    };
  });

  const paddingPx = parseFloat(paddingStyles.computedPaddingTop);
  expect(paddingPx).toBeGreaterThan(10);
  expect(paddingPx).toBeLessThan(24);
  expect(paddingStyles.inlinePaddingTop.endsWith("em")).toBeTruthy();
});

test("폼 셀렉트 가로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "셀렉트(Select)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("셀렉트 가로 패딩 (px) 커스텀 (px)").fill("24");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const selectElement = page.getByTestId("preview-form-select").locator("select").first();
  const paddingStyles = await selectElement.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedPaddingLeft: s.paddingLeft,
      inlinePaddingLeft: (element.style as any).paddingInline || element.style.paddingLeft,
    };
  });

  const paddingPx = parseFloat(paddingStyles.computedPaddingLeft);
  expect(paddingPx).toBeGreaterThan(12);
  expect(paddingPx).toBeLessThan(28);
  expect(paddingStyles.inlinePaddingLeft.endsWith("em")).toBeTruthy();
});

test("폼 셀렉트 세로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "셀렉트(Select)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("셀렉트 세로 패딩 (px) 커스텀 (px)").fill("18");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const selectElement = page.getByTestId("preview-form-select").locator("select").first();
  const paddingStyles = await selectElement.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedPaddingTop: s.paddingTop,
      inlinePaddingTop: (element.style as any).paddingBlock || element.style.paddingTop,
    };
  });

  const paddingPx = parseFloat(paddingStyles.computedPaddingTop);
  expect(paddingPx).toBeGreaterThan(12);
  expect(paddingPx).toBeLessThan(24);
  expect(paddingStyles.inlinePaddingTop.endsWith("em")).toBeTruthy();
});

test("폼 셀렉트 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "셀렉트(Select)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("셀렉트 텍스트 크기 (px) 커스텀 (px)").fill("28");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const select = page.getByTestId("preview-form-select").first();
  const inlineStyles = await select.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedFontSize: s.fontSize,
      inlineFontSize: element.style.fontSize,
    };
  });

  const fontSizePx = parseFloat(inlineStyles.computedFontSize);
  expect(fontSizePx).toBeGreaterThan(20);
  expect(fontSizePx).toBeLessThan(32);
  expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
});

test("폼 셀렉트 줄간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "셀렉트(Select)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("셀렉트 줄간격 (px) 커스텀 (px)").fill("36");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const select = page.getByTestId("preview-form-select").first();
  const inlineStyles = await select.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedLineHeight: s.lineHeight,
      inlineLineHeight: element.style.lineHeight,
    };
  });

  const lineHeightPx = parseFloat(inlineStyles.computedLineHeight);
  expect(lineHeightPx).toBeGreaterThan(20);
  expect(lineHeightPx).toBeLessThan(40);
  expect(inlineStyles.inlineLineHeight.endsWith("em")).toBeTruthy();
});

test("폼 셀렉트 자간 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "셀렉트(Select)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("셀렉트 자간 (px) 커스텀 (px)").fill("4");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const select = page.getByTestId("preview-form-select").first();
  const inlineStyles = await select.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedLetterSpacing: s.letterSpacing,
      inlineLetterSpacing: element.style.letterSpacing,
    };
  });

  const letterSpacingPx = parseFloat(inlineStyles.computedLetterSpacing);
  expect(letterSpacingPx).toBeGreaterThan(2);
  expect(letterSpacingPx).toBeLessThan(6);
  expect(inlineStyles.inlineLetterSpacing.endsWith("em")).toBeTruthy();
});

test("폼 체크박스 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("체크박스 텍스트 크기 (px) 커스텀 (px)").fill("24");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const firstOption = page.getByTestId("preview-form-checkbox-option").first();
  const inlineStyles = await firstOption.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedFontSize: s.fontSize,
      inlineFontSize: element.style.fontSize,
    };
  });

  const fontSizePx = parseFloat(inlineStyles.computedFontSize);
  expect(fontSizePx).toBeGreaterThan(20);
  expect(fontSizePx).toBeLessThan(28);
  expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
});

test("폼 라디오 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "단일 선택(Radio)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("라디오 텍스트 크기 (px) 커스텀 (px)").fill("22");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const firstOption = page.getByTestId("preview-form-radio-option").first();
  const inlineStyles = await firstOption.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedFontSize: s.fontSize,
      inlineFontSize: element.style.fontSize,
    };
  });

  const fontSizePx = parseFloat(inlineStyles.computedFontSize);
  expect(fontSizePx).toBeGreaterThan(18);
  expect(fontSizePx).toBeLessThan(30);
  expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
});

test("폼 입력 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "입력(Input)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("필드 텍스트 크기 (px) 커스텀 (px)").fill("32");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const input = page.getByTestId("preview-form-input").first();
  const inlineStyles = await input.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      computedFontSize: s.fontSize,
      inlineFontSize: element.style.fontSize,
    };
  });

  const fontSizePx = parseFloat(inlineStyles.computedFontSize);
  expect(fontSizePx).toBeGreaterThan(20);
  expect(fontSizePx).toBeLessThan(36);
  expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
});

test("폼 셀렉트 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "셀렉트(Select)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("필드 너비").selectOption("fixed");
  await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("360");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const selectField = page.getByTestId("preview-form-select").first();
  const { width, inlineWidth } = await selectField.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      width: s.width,
      inlineWidth: element.style.width,
    };
  });

  const widthPx = parseFloat(width);
  expect(widthPx).toBeGreaterThan(250);
  expect(widthPx).toBeLessThan(420);
  expect(inlineWidth.endsWith("em")).toBeTruthy();
});

test("폼 라디오 그룹 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "단일 선택(Radio)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("필드 너비").selectOption("fixed");
  await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("400");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const radioGroup = page.getByTestId("preview-form-radio-group").first();
  const { width, inlineWidth } = await radioGroup.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      width: s.width,
      inlineWidth: element.style.width,
    };
  });

  const widthPx = parseFloat(width);
  expect(widthPx).toBeGreaterThan(280);
  expect(widthPx).toBeLessThan(460);
  expect(inlineWidth.endsWith("em")).toBeTruthy();
});

test("폼 체크박스 그룹 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  await propertiesSidebar.getByLabel("필드 너비").selectOption("fixed");
  await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("420");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const checkboxGroup = page.getByTestId("preview-form-checkbox-group").first();
  const { width, inlineWidth } = await checkboxGroup.evaluate((el) => {
    const element = el as HTMLElement;
    const s = window.getComputedStyle(element);
    return {
      width: s.width,
      inlineWidth: element.style.width,
    };
  });

  const widthPx = parseFloat(width);
  expect(widthPx).toBeGreaterThan(300);
  expect(widthPx).toBeLessThan(480);
  expect(inlineWidth.endsWith("em")).toBeTruthy();
});

test("폼 컨트롤러에 매핑한 폼 요소와 버튼이 프리뷰에서 함께 동작해야 한다", async ({ page }) => {
  // 1) 에디터에서 폼 요소, 버튼, 폼 블록을 추가한다.
  await page.goto("/editor");

  const canvas = page.getByTestId("editor-canvas");

  // 폼 입력 요소와 버튼, 폼 블록을 순서대로 추가한다.
  await page.getByRole("button", { name: "입력(Input)" }).click();
  await page.getByRole("button", { name: "버튼" }).click();
  await page.getByRole("button", { name: "폼 컨트롤러" }).click();

  // 방금 추가된 폼 블록을 선택한다.
  const blocks = canvas.getByTestId("editor-block");
  const formBlock = blocks.nth((await blocks.count()) - 1);
  await formBlock.click({ force: true });

  // 2) 우측 속성 패널에서 폼 컨트롤러 매핑을 설정한다.
  const fieldMappingGroup = page.getByRole("group", { name: "폼 필드 매핑" });
  const fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");

  // 첫 번째 폼 요소를 이 폼의 fieldIds 에 매핑한다.
  await fieldCheckboxes.nth(0).check();

  // Submit 버튼 셀렉트 박스에서 방금 추가한 버튼 블록을 선택한다.
  const submitSelect = page.getByRole("combobox", { name: "Submit 버튼" });
  // 첫 번째 옵션은 "(선택 안 함)" 이므로, 그 이후 옵션 중 하나를 선택한다.
  await submitSelect.selectOption({ index: 1 });

  // 3) 프리뷰 페이지로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // v2 컨트롤러를 설정했으므로, 기본 contact fallback 폼(이름/이메일/메시지) 레이블은 더 이상 보이지 않아야 한다.
  await expect(page.getByText("이름")).toHaveCount(0);
  await expect(page.getByText("이메일")).toHaveCount(0);
  await expect(page.getByText("메시지")).toHaveCount(0);

  // 기대: 폼 컨트롤러에 매핑한 폼 입력 요소가 프리뷰에서 필드로 렌더링되어야 한다.
  // (구체적인 라벨 텍스트는 구현에 따라 달라질 수 있어 우선 textbox 중 하나가 보이는지만 확인한다.)
  const input = page.getByRole("textbox").first();
  await expect(input).toBeVisible();

  // 프리뷰 정책: FormBlock 컨트롤러는 프리뷰에서 실제 `<form>` DOM 을 렌더하지 않는다.
  await expect(page.getByTestId("preview-form-controller")).toHaveCount(0);
  await expect(page.getByText("성공적으로 전송되었습니다.")).toHaveCount(0);
});

test("여러 폼 블록과 여러 버튼이 있을 때 각 폼이 독립적으로 제출되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  const canvas = page.getByTestId("editor-canvas");

  const blocks = canvas.getByTestId("editor-block");

  // Form A: 입력 필드 + 버튼 + 폼 블록을 추가한 뒤, 즉시 컨트롤러를 매핑한다.
  await page.getByRole("button", { name: "입력(Input)" }).click();
  await page.getByRole("button", { name: "버튼" }).click();
  await page.getByRole("button", { name: "폼 컨트롤러" }).click();

  let count = await blocks.count();
  const formBlockA = blocks.nth(count - 1);
  await formBlockA.click({ force: true });

  let fieldMappingGroup = page.getByRole("group", { name: "폼 필드 매핑" });
  let fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");
  // 현재 존재하는 폼 요소 중 첫 번째를 A 폼에 매핑한다.
  await fieldCheckboxes.nth(0).check();

  let submitSelect = page.getByRole("combobox", { name: "Submit 버튼" });
  // 첫 번째 버튼을 A 폼의 submit 버튼으로 매핑 (index 1: 첫 번째 실제 버튼)
  await submitSelect.selectOption({ index: 1 });

  // Form B: 입력 필드 + 버튼 + 폼 블록을 다시 추가한 뒤, 즉시 컨트롤러를 매핑한다.
  await page.getByRole("button", { name: "입력(Input)" }).click();
  await page.getByRole("button", { name: "버튼" }).first().click();
  await page.getByRole("button", { name: "폼 컨트롤러" }).click();

  count = await blocks.count();
  const formBlockB = blocks.nth(count - 1);
  await formBlockB.click({ force: true });

  fieldMappingGroup = page.getByRole("group", { name: "폼 필드 매핑" });
  fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");
  // 현재 폼 요소 중 마지막 것을 B 폼에 매핑 (두 번째 입력 필드라고 가정)
  const lastCheckboxIndex = (await fieldCheckboxes.count()) - 1;
  await fieldCheckboxes.nth(lastCheckboxIndex).check();

  submitSelect = page.getByRole("combobox", { name: "Submit 버튼" });
  // 두 번째 버튼을 B 폼의 submit 버튼으로 매핑 (index 2: 두 번째 실제 버튼)
  await submitSelect.selectOption({ index: 2 });

  // 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();

  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const textboxes = page.getByRole("textbox");
  const textboxCount = await textboxes.count();

  // 최소 2개의 텍스트 입력이 있어야 한다.
  expect(textboxCount).toBeGreaterThanOrEqual(2);

  const firstInput = textboxes.nth(0);
  const secondInput = textboxes.nth(1);

  // 프리뷰 정책: 여러 FormBlock 이 있어도 preview-form-controller DOM 은 생성되지 않아야 한다.
  const controllers = page.getByTestId("preview-form-controller");
  await expect(controllers).toHaveCount(0);

  const successMessages = page.getByText("성공적으로 전송되었습니다.");
  await expect(successMessages).toHaveCount(0);
});

test("섹션 패딩/컬럼 간격 수치가 프리뷰에서도 em 스케일에 맞게 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  // 섹션 블록을 직접 추가한다.
  await page.getByRole("button", { name: "섹션" }).click();

  // 캔버스에서 첫 번째 섹션 블록을 선택한다.
  const canvas = page.getByTestId("editor-canvas");
  const sectionBlock = canvas.getByTestId("editor-block").first();
  await sectionBlock.click({ force: true });

  // 우측 속성 패널 범위를 지정한다.
  const propertiesSidebar = page.getByTestId("properties-sidebar");

  // 세로 패딩: 64px
  const paddingYInput = propertiesSidebar.getByLabel("세로 패딩 커스텀 (px)");
  await paddingYInput.fill("64");

  // 컬럼 간 간격: 40px (커스텀 입력 spinbox/텍스트박스만 타겟)
  const gapXInput = propertiesSidebar.getByRole("textbox", {
    name: "컬럼 간 간격 (px) 커스텀 (px)",
  });
  await gapXInput.fill("40");

  // 프리뷰로 이동한다.
  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 첫 번째 섹션 요소를 찾는다.
  const firstSection = page.getByTestId("preview-section").first();

  const sectionStyles = await firstSection.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      paddingTop: s.paddingTop,
      paddingBottom: s.paddingBottom,
    };
  });

  // 세로 패딩: 64px 입력 → em 변환(1em=16px) 기반이므로 64px 근방이어야 한다.
  const paddingTopPx = parseFloat(sectionStyles.paddingTop);
  const paddingBottomPx = parseFloat(sectionStyles.paddingBottom);
  expect(paddingTopPx).toBeGreaterThan(40);
  expect(paddingTopPx).toBeLessThan(80);
  expect(paddingBottomPx).toBeGreaterThan(40);
  expect(paddingBottomPx).toBeLessThan(80);

  // 섹션 컬럼 컨테이너의 column-gap 을 읽는다.
  const columnsContainer = firstSection.getByTestId("preview-section-columns");
  const gapValue = await columnsContainer.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return s.columnGap || (s as any).gap || "";
  });

  const gapPx = parseFloat(gapValue || "0");
  expect(gapPx).toBeGreaterThan(20);
  expect(gapPx).toBeLessThan(60);
});

test("구분선 여백/길이 수치가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
  await page.goto("/editor");

  await page.getByRole("button", { name: "구분선" }).click();

  const canvas = page.getByTestId("editor-canvas");
  const dividerBlock = canvas.getByTestId("editor-block").first();
  await dividerBlock.click({ force: true });

  const propertiesSidebar = page.getByTestId("properties-sidebar");

  // 위/아래 여백 32px
  const marginInput = propertiesSidebar.getByLabel("위/아래 여백 커스텀 (px)");
  await marginInput.fill("32");

  // 길이 모드: 고정 길이 200px
  await propertiesSidebar.getByLabel("구분선 길이 모드").selectOption("fixed");
  const widthInput = propertiesSidebar.getByLabel("고정 길이 (px) 커스텀 (px)");
  await widthInput.fill("200");

  await page.getByRole("link", { name: "프리뷰 열기" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const divider = page.getByTestId("preview-divider").first();

  const styles = await divider.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      marginTop: s.marginTop,
      marginBottom: s.marginBottom,
    };
  });

  const marginTopPx = parseFloat(styles.marginTop);
  const marginBottomPx = parseFloat(styles.marginBottom);
  expect(marginTopPx).toBeGreaterThan(20);
  expect(marginTopPx).toBeLessThan(50);
  expect(marginBottomPx).toBeGreaterThan(20);
  expect(marginBottomPx).toBeLessThan(50);

  const innerLine = divider.getByTestId("preview-divider-line");
  const widthValue = await innerLine.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return s.width;
  });

  const widthPx = parseFloat(widthValue || "0");
  expect(widthPx).toBeGreaterThan(120);
  expect(widthPx).toBeLessThan(260);
});
