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

// 모든 블록 타입의 스타일이 builder.css 기반 pb-* 토큰과 일관되게 적용되는지
// 에디터 → 프리뷰 → 퍼블릭(/p/[slug]) 컨텍스트를 가로질러 검증하는 회귀 스위트.
// - 텍스트, 버튼, 리스트, 이미지, 비디오, 폼 입력 필드까지 최소 1개씩 포함한 프로젝트를 구성한다.
// - 프리뷰에서 각 블록의 computed style 을 스냅샷으로 수집한 뒤,
//   동일한 slug 의 퍼블릭 페이지(/p/[slug])에서 다시 측정해 값이 일치하는지 비교한다.
// - 이 테스트는 builder.css 가 단일 진실 소스로 동작하는지(컨텍스트 간 시각 결과가 같은지)를 보장한다.

// 인증 가드를 통과시키기 위해 /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-style-regression", email: "style-regression@example.com", tokenVersion: 1 }),
    });
  });
});

function parsePx(value: string): number | null {
  const n = parseFloat(value || "");
  return Number.isFinite(n) ? n : null;
}

async function ensureAuthenticated(page: any, request: any) {
  const now = Date.now();
  const email = `style-regression-${now}@example.com`;
  const password = "style-regression-password";

  const signupRes = await request.post("/api/auth/signup", {
    headers: { "Content-Type": "application/json" },
    data: { email, password },
  });

  const status = signupRes.status();
  if (status !== 201) {
    // 디버깅용: CI 등에서 500 이 떨어질 때 응답 바디를 함께 로그로 출력해 원인을 파악할 수 있게 한다.
    console.log("[block-styles-regression] signup error status=", status);
    try {
      const bodyText = await signupRes.text();
      console.log("[block-styles-regression] signup error body=", bodyText);
    } catch (e) {
      console.log("[block-styles-regression] signup error: body read failed", e);
    }
  }

  expect(status).toBe(201);

  const setCookieHeader = signupRes.headers()["set-cookie"] as string | string[] | null;
  expect(setCookieHeader).toBeTruthy();

  const cookieHeaderString = Array.isArray(setCookieHeader)
    ? setCookieHeader.join("; ")
    : (setCookieHeader as string);

  const match = cookieHeaderString.match(/pb_access=([^;]+)/);
  expect(match).not.toBeNull();

  const accessToken = match![1];

  await page.context().addCookies([
    {
      name: "pb_access",
      value: accessToken,
      domain: "localhost",
      path: "/",
      httpOnly: true,
      secure: false,
      sameSite: "Lax",
    },
  ]);
}

test("텍스트/버튼/리스트/이미지/비디오/폼 입력 필드 스타일이 프리뷰와 퍼블릭 페이지에서 동일해야 한다", async ({ page, request }) => {
  const now = Date.now();
  const slug = `style-regression-${now}`;

  // 1) 에디터에서 프로젝트 slug 와 기본 블록들을 구성한다.
  await ensureAuthenticated(page, request);
  await page.goto("/editor");

  const sidebar = page.getByTestId("properties-sidebar");
  await sidebar.getByLabel("Project title").fill("스타일 회귀 테스트");
  await sidebar.getByLabel("Project slug").fill(slug);

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

  // 텍스트 블록 추가 및 스타일 설정
  await page.getByRole("button", { name: "Text" }).first().click();
  await canvas.getByText("New text").first().click({ force: true });
  const textContentInput = page.getByRole("textbox", { name: "Selected text block content" });
  await textContentInput.fill("스타일 회귀 텍스트 블록");
  await page.getByRole("combobox", { name: "Alignment" }).selectOption("center");
  await page.getByRole("combobox", { name: "Font size" }).selectOption("lg");

  // 버튼 블록 추가 및 스타일 설정
  await page.getByRole("button", { name: "Button" }).click();
  const buttonBlock = canvas.getByRole("button", { name: "Button" }).first();
  await buttonBlock.click({ force: true });
  await page.getByRole("combobox", { name: "Button alignment" }).selectOption("center");
  // 버튼 크기 프리셋: NumericPropertyControl 이 제공하는 id (xs/sm/base/lg/xl/2xl/3xl) 중 하나를 사용한다.
  await page.getByRole("combobox", { name: "Button style" }).selectOption("solid");

  // 리스트 블록 추가
  await page.getByRole("button", { name: "List" }).click();
  const listTextarea = page.getByLabel("List items");
  await listTextarea.fill("List item 1\nList item 2");

  // 이미지 블록 추가 (간단히 /api/image 경로가 아닌 placeholder 이미지 URL 사용)
  await page.getByRole("button", { name: "Image" }).click();
  const imageUrlInput = page.getByLabel("Image URL");
  await imageUrlInput.fill("https://via.placeholder.com/320x180.png?text=pb-image");
  const imageAltInput = page.getByLabel("Alt text");
  await imageAltInput.fill("스타일 회귀 이미지");
  const imageWidthModeSelect = sidebar.getByLabel("Width mode");
  await imageWidthModeSelect.selectOption("fixed");
  const imageWidthSlider = sidebar.getByLabel("Fixed width 커스텀");
  await imageWidthSlider.fill("320");

  // 비디오 블록 추가 (YouTube URL 사용)
  await page.getByRole("button", { name: "Video" }).click();
  const videoUrlInput = page.getByLabel("Video URL");
  await videoUrlInput.fill("https://www.youtube.com/watch?v=dQw4w9WgXcQ");

  // 폼 입력 블록 추가 (플로팅 라벨은 아닌 기본 visible 레이아웃)
  await page.getByRole("button", { name: "Input field" }).click();
  // FormInputPropertiesPanel 에서는 "Field label" 이라는 레이블을 사용한다.
  const formLabelInput = page.getByRole("textbox", { name: "Field label" });
  await formLabelInput.fill("이메일 입력 필드");

  // 2) 헤더의 프리뷰 링크를 통해 현재 상태를 프리뷰로 열어 스타일을 측정한다.
  await page.getByRole("link", { name: "Open preview" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 텍스트 블록
  const previewText = page.getByText("스타일 회귀 텍스트 블록").first();
  const previewTextStyles = await previewText.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      color: s.color,
      lineHeight: s.lineHeight,
      letterSpacing: s.letterSpacing,
      fontWeight: s.fontWeight,
      textDecorationLine: s.textDecorationLine,
      fontStyle: s.fontStyle,
      backgroundColor: s.backgroundColor,
    };
  });

  // 버튼 (프리뷰에서는 <a> 로 렌더링됨)
  const previewButton = page.getByRole("link", { name: "Button" }).first();
  const previewButtonStyles = await previewButton.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
      backgroundColor: s.backgroundColor,
      borderRadius: s.borderRadius,
    };
  });

  // 리스트 첫 번째 아이템
  const previewFirstLi = page.locator("li").first();
  const previewListStyles = await previewFirstLi.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLLIElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      color: s.color,
      marginBottom: s.marginBottom,
      listStyleType: s.listStyleType,
    };
  });

  // 이미지
  const previewImage = page.getByRole("img", { name: "스타일 회귀 이미지" }).first();
  const previewImageStyles = await previewImage.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLImageElement);
    return {
      width: s.width,
      borderRadius: s.borderRadius,
    };
  });

  // 비디오 wrapper (iframe 상위 pb-video-wrapper)가 프리뷰에도 DOM 상에 존재하는지만 확인한다.
  const previewVideoWrapper = page.locator(".pb-video-wrapper").first();
  await expect(previewVideoWrapper).toBeAttached();

  // 폼 입력 필드 (라벨 + input)
  const previewInput = page.getByRole("textbox", { name: "이메일 입력 필드" });
  const previewInputStyles = await previewInput.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLInputElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      color: s.color,
      backgroundColor: s.backgroundColor,
      borderRadius: s.borderRadius,
    };
  });

  // 3) 프리뷰 측정을 마친 뒤, 에디터로 돌아가 프로젝트를 서버에 저장해 /p/[slug] 페이지에서도 동일 구성을 볼 수 있게 한다.
  await page.getByRole("link", { name: "Back to editor" }).click();
  await expect(page).toHaveURL(/\/editor/);
  await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible({ timeout: 15000 });

  await page.getByRole("button", { name: "Menu" }).click();
  await page.getByRole("button", { name: "Save / load project" }).click();
  const projectSlugModalInput = page.getByLabel("Project address (e.g. my-landing)");
  await projectSlugModalInput.fill(slug);
  await page.getByRole("button", { name: "Save (local + server)" }).click();

  await expect(page).toHaveURL(/\/_?projects/);
  await page.goto(`/p/${slug}`);

  // 4) 동일한 slug 의 퍼블릭 페이지(/p/[slug])로 이동해 같은 요소들의 스타일을 다시 측정한다.

  // 텍스트 블록
  const publicText = page.getByText("스타일 회귀 텍스트 블록").first();
  const publicTextStyles = await publicText.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      color: s.color,
      lineHeight: s.lineHeight,
      letterSpacing: s.letterSpacing,
      fontWeight: s.fontWeight,
      textDecorationLine: s.textDecorationLine,
      fontStyle: s.fontStyle,
      backgroundColor: s.backgroundColor,
    };
  });

  expect(publicTextStyles.textAlign).toBe(previewTextStyles.textAlign);
  expect(publicTextStyles.fontSize).toBe(previewTextStyles.fontSize);
  expect(publicTextStyles.color).toBe(previewTextStyles.color);
  expect(publicTextStyles.lineHeight).toBe(previewTextStyles.lineHeight);
  expect(publicTextStyles.letterSpacing).toBe(previewTextStyles.letterSpacing);
  expect(publicTextStyles.fontWeight).toBe(previewTextStyles.fontWeight);
  expect(publicTextStyles.textDecorationLine).toBe(previewTextStyles.textDecorationLine);
  expect(publicTextStyles.fontStyle).toBe(previewTextStyles.fontStyle);
  expect(publicTextStyles.backgroundColor).toBe(previewTextStyles.backgroundColor);

  // 버튼: 프리뷰와 동일하게 role=link, name="버튼" 기준으로 선택한다.
  const publicButton = page.getByRole("link", { name: "Button" }).first();
  const publicButtonStyles = await publicButton.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
      backgroundColor: s.backgroundColor,
      borderRadius: s.borderRadius,
    };
  });

  const previewButtonFont = parsePx(previewButtonStyles.fontSize);
  const publicButtonFont = parsePx(publicButtonStyles.fontSize);
  if (previewButtonFont !== null && publicButtonFont !== null) {
    // 브라우저/렌더링 환경에 따른 소수점/루트 폰트 크기 차이만 허용하고, ±2px 이내에서만 허용한다.
    expect(Math.abs(publicButtonFont - previewButtonFont)).toBeLessThanOrEqual(2);
  } else {
    expect(publicButtonStyles.fontSize).toBe(previewButtonStyles.fontSize);
  }
  expect(publicButtonStyles.color).toBe(previewButtonStyles.color);
  expect(publicButtonStyles.backgroundColor).toBe(previewButtonStyles.backgroundColor);
  const previewBorderRadiusPx = parsePx(previewButtonStyles.borderRadius);
  const publicBorderRadiusPx = parsePx(publicButtonStyles.borderRadius);
  if (previewBorderRadiusPx !== null && publicBorderRadiusPx !== null) {
    expect(Math.abs(publicBorderRadiusPx - previewBorderRadiusPx)).toBeLessThanOrEqual(2);
  } else {
    expect(publicButtonStyles.borderRadius).toBe(previewButtonStyles.borderRadius);
  }

  // 리스트 첫 번째 아이템
  const publicFirstLi = page.locator("li").first();
  const publicListStyles = await publicFirstLi.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLLIElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      color: s.color,
      marginBottom: s.marginBottom,
      listStyleType: s.listStyleType,
    };
  });

  expect(publicListStyles.textAlign).toBe(previewListStyles.textAlign);
  expect(publicListStyles.fontSize).toBe(previewListStyles.fontSize);
  expect(publicListStyles.color).toBe(previewListStyles.color);
  expect(publicListStyles.listStyleType).toBe(previewListStyles.listStyleType);

  const previewGap = parsePx(previewListStyles.marginBottom);
  const publicGap = parsePx(publicListStyles.marginBottom);
  if (previewGap !== null && publicGap !== null) {
    // 브라우저/렌더링 환경에 따른 소수점 차이만 허용하고, 합리적인 오차 범위(±2px) 안에서만 허용한다.
    expect(Math.abs(previewGap - publicGap)).toBeLessThanOrEqual(2);
  }

  // 이미지
  const publicImage = page.getByRole("img", { name: "스타일 회귀 이미지" }).first();
  const publicImageStyles = await publicImage.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLImageElement);
    return {
      borderRadius: s.borderRadius,
    };
  });

  expect(publicImageStyles.borderRadius).toBe(previewImageStyles.borderRadius);

  // 비디오 wrapper 가 퍼블릭 페이지에도 DOM 상에 존재하는지만 확인한다.
  const publicVideoWrapper = page.locator(".pb-video-wrapper").first();
  await expect(publicVideoWrapper).toBeAttached();

  // 폼 입력 필드
  const publicInput = page.getByRole("textbox", { name: "이메일 입력 필드" });
  const publicInputStyles = await publicInput.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLInputElement);
    return {
      textAlign: s.textAlign,
      fontSize: s.fontSize,
      color: s.color,
      backgroundColor: s.backgroundColor,
      borderRadius: s.borderRadius,
    };
  });

  const normalizeAlign = (value: string) =>
    value === "start" ? "left" : value === "end" ? "right" : value;
  expect(normalizeAlign(publicInputStyles.textAlign)).toBe(
    normalizeAlign(previewInputStyles.textAlign),
  );
  expect(publicInputStyles.fontSize).toBe(previewInputStyles.fontSize);
  expect(publicInputStyles.color).toBe(previewInputStyles.color);
  expect(publicInputStyles.backgroundColor).toBe(previewInputStyles.backgroundColor);
  expect(publicInputStyles.borderRadius).toBe(previewInputStyles.borderRadius);
});

test("버튼 너비/패딩/색상/둥글기 스타일이 프리뷰와 퍼블릭 페이지에서 동일해야 한다", async ({
  page,
  request,
}) => {
  const now = Date.now();
  const slug = `button-layout-style-regression-${now}`;

  await ensureAuthenticated(page, request);
  await page.goto("/editor");

  const sidebar = page.getByTestId("properties-sidebar");
  await sidebar.getByLabel("Project title").fill("버튼 레이아웃 스타일 회귀 테스트");
  await sidebar.getByLabel("Project slug").fill(slug);

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

  // 버튼 블록 추가 및 선택
  await page.getByRole("button", { name: "Button" }).click();
  const buttonBlock = canvas.getByRole("button", { name: "Button" }).first();
  await buttonBlock.click({ force: true });

  // 버튼 레이아웃/패딩/둥글기 설정
  await sidebar.getByLabel("Button width mode").selectOption("fixed");
  await sidebar.getByLabel("Button fixed width 커스텀").fill("260");

  await sidebar.getByLabel("Horizontal padding 커스텀").fill("24");
  await sidebar.getByLabel("Vertical padding 커스텀").fill("12");

  // 모서리 둥글기 최댓값(4) → borderRadius="full" 토큰
  await sidebar.getByLabel("Border radius 슬라이더").fill("4");

  // 프리뷰에서 버튼 스타일 측정
  await page.getByRole("link", { name: "Open preview" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const previewButton = page.getByRole("link", { name: "Button" }).first();
  const previewButtonStyles = await previewButton.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      width: s.width,
      paddingLeft: s.paddingLeft,
      paddingRight: s.paddingRight,
      paddingTop: s.paddingTop,
      paddingBottom: s.paddingBottom,
      backgroundColor: s.backgroundColor,
      borderColor: s.borderColor,
      borderRadius: s.borderRadius,
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  // 프로젝트 저장 후 퍼블릭 페이지로 이동
  await page.getByRole("link", { name: "Back to editor" }).click();
  await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible();

  await page.getByRole("button", { name: "Menu" }).click();
  await page.getByRole("button", { name: "Save / load project" }).click();
  const projectSlugModalInput = page.getByLabel("Project address (e.g. my-landing)");
  await projectSlugModalInput.fill(slug);
  await page.getByRole("button", { name: "Save (local + server)" }).click();

  await page.waitForURL(/\/projects/);
  await page.goto(`/p/${slug}`);

  const publicButton = page.getByRole("link", { name: "Button" }).first();
  const publicButtonStyles = await publicButton.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      width: s.width,
      paddingLeft: s.paddingLeft,
      paddingRight: s.paddingRight,
      paddingTop: s.paddingTop,
      paddingBottom: s.paddingBottom,
      backgroundColor: s.backgroundColor,
      borderColor: s.borderColor,
      borderRadius: s.borderRadius,
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  // width / padding 은 px 기준으로 ±2px 이내여야 한다.
  const previewWidth = parsePx(previewButtonStyles.width)!;
  const publicWidth = parsePx(publicButtonStyles.width)!;
  expect(Math.abs(publicWidth - previewWidth)).toBeLessThanOrEqual(2);

  const previewPaddingLeft = parsePx(previewButtonStyles.paddingLeft)!;
  const publicPaddingLeft = parsePx(publicButtonStyles.paddingLeft)!;
  expect(Math.abs(publicPaddingLeft - previewPaddingLeft)).toBeLessThanOrEqual(2);

  const previewPaddingRight = parsePx(previewButtonStyles.paddingRight)!;
  const publicPaddingRight = parsePx(publicButtonStyles.paddingRight)!;
  expect(Math.abs(publicPaddingRight - previewPaddingRight)).toBeLessThanOrEqual(2);

  const previewPaddingTop = parsePx(previewButtonStyles.paddingTop)!;
  const publicPaddingTop = parsePx(publicButtonStyles.paddingTop)!;
  expect(Math.abs(publicPaddingTop - previewPaddingTop)).toBeLessThanOrEqual(2);

  const previewPaddingBottom = parsePx(previewButtonStyles.paddingBottom)!;
  const publicPaddingBottom = parsePx(publicButtonStyles.paddingBottom)!;
  expect(Math.abs(publicPaddingBottom - previewPaddingBottom)).toBeLessThanOrEqual(2);

  // 색상/둥글기/폰트 크기/텍스트 색상은 정확히 일치해야 한다.
  expect(publicButtonStyles.backgroundColor).toBe(previewButtonStyles.backgroundColor);
  expect(publicButtonStyles.borderColor).toBe(previewButtonStyles.borderColor);
  const previewBorderRadius = parsePx(previewButtonStyles.borderRadius)!;
  const publicBorderRadius = parsePx(publicButtonStyles.borderRadius)!;
  expect(Math.abs(publicBorderRadius - previewBorderRadius)).toBeLessThanOrEqual(2);
  expect(publicButtonStyles.fontSize).toBe(previewButtonStyles.fontSize);
  expect(publicButtonStyles.color).toBe(previewButtonStyles.color);
});

test("폼 셀렉트/체크박스/라디오 기본 텍스트 스타일이 프리뷰와 퍼블릭 페이지에서 동일해야 한다", async ({ page, request }) => {
  const now = Date.now();
  const slug = `form-style-regression-${now}`;

  // 1) 에디터에서 폼 블록들을 추가하고 프로젝트를 저장한다.
  await ensureAuthenticated(page, request);
  await page.goto("/editor");

  const sidebar = page.getByTestId("properties-sidebar");
  await sidebar.getByLabel("Project title").fill("폼 스타일 회귀 테스트");
  await sidebar.getByLabel("Project slug").fill(slug);

  // 셀렉트 블록 추가
  await page.getByRole("button", { name: "Select field" }).click();
  const selectLabelInput = sidebar.getByRole("textbox", { name: "Field label" });
  await selectLabelInput.fill("셀렉트 스타일 필드");

  // 체크박스 블록 추가
  await page.getByRole("button", { name: "Checkbox group" }).click();
  const checkboxGroupLabelInput = sidebar.getByLabel("Group title", { exact: true });
  await checkboxGroupLabelInput.fill("체크박스 스타일 그룹");

  // 라디오 블록 추가
  await page.getByRole("button", { name: "Radio group" }).click();
  const radioGroupLabelInput = sidebar.getByLabel("Group title", { exact: true });
  await radioGroupLabelInput.fill("라디오 스타일 그룹");

  // 첫 번째 라디오 옵션 라벨을 "플랜 A" 로 명시적으로 설정해 프리뷰/퍼블릭에서 텍스트를 안정적으로 찾는다.
  const firstRadioOptionLabel = sidebar.getByPlaceholder("Label").first();
  await firstRadioOptionLabel.fill("플랜 A");

  // 2) 헤더의 프리뷰 링크를 통해 현재 상태를 프리뷰로 열어 텍스트 스타일을 측정한다.
  await page.getByRole("link", { name: "Open preview" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  // 셀렉트: 필드 라벨 텍스트 기준으로 스타일을 측정한다.
  const previewSelectLabel = page.getByText("셀렉트 스타일 필드").first();
  const previewSelectLabelStyles = await previewSelectLabel.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  // 체크박스: 기본 첫 번째 옵션 라벨 텍스트(옵션 1)를 기준으로 스타일을 측정한다.
  const previewCheckboxOption = page.getByText("Option 1").first();
  const previewCheckboxOptionStyles = await previewCheckboxOption.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  // 라디오: 기본 첫 번째 옵션 라벨 텍스트(플랜 A)를 기준으로 스타일을 측정한다.
  const previewRadioOption = page.getByText("플랜 A").first();
  const previewRadioOptionStyles = await previewRadioOption.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  // 3) 프리뷰 측정을 마친 뒤, 에디터로 돌아가 프로젝트를 서버에 저장해 /p/[slug] 페이지에서도 동일 구성을 볼 수 있게 한다.
  await page.getByRole("link", { name: "Back to editor" }).click();
  await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible();

  await page.getByRole("button", { name: "Menu" }).click();
  await page.getByRole("button", { name: "Save / load project" }).click();
  await page.getByRole("button", { name: "Save (local + server)" }).click();

  await page.waitForURL(/\/projects/);
  await page.goto(`/p/${slug}`);

  // 4) 동일한 slug 의 퍼블릭 페이지(/p/[slug])로 이동해 같은 텍스트들의 스타일을 다시 측정한다.

  const publicSelectLabel = page.getByText("셀렉트 스타일 필드").first();
  const publicSelectLabelStyles = await publicSelectLabel.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  expect(publicSelectLabelStyles.fontSize).toBe(previewSelectLabelStyles.fontSize);
  expect(publicSelectLabelStyles.color).toBe(previewSelectLabelStyles.color);

  const publicCheckboxOption = page.getByText("Option 1").first();
  const publicCheckboxOptionStyles = await publicCheckboxOption.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  expect(publicCheckboxOptionStyles.fontSize).toBe(previewCheckboxOptionStyles.fontSize);
  expect(publicCheckboxOptionStyles.color).toBe(previewCheckboxOptionStyles.color);

  const publicRadioOption = page.getByText("플랜 A").first();
  const publicRadioOptionStyles = await publicRadioOption.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      fontSize: s.fontSize,
      color: s.color,
    };
  });

  expect(publicRadioOptionStyles.fontSize).toBe(previewRadioOptionStyles.fontSize);
  expect(publicRadioOptionStyles.color).toBe(previewRadioOptionStyles.color);
});

test("폼 셀렉트/체크박스/라디오 레이아웃/패딩/너비/간격 스타일이 프리뷰와 퍼블릭 페이지에서 동일해야 한다", async ({ page, request }) => {
  const now = Date.now();
  const slug = `form-layout-style-regression-${now}`;

  await ensureAuthenticated(page, request);
  await page.goto("/editor");

  const sidebar = page.getByTestId("properties-sidebar");
  await sidebar.getByLabel("Project title").fill("폼 레이아웃 스타일 회귀 테스트");
  await sidebar.getByLabel("Project slug").fill(slug);

  await page.getByRole("button", { name: "Select field" }).click();
  const selectLabelInput = sidebar.getByRole("textbox", { name: "Field label" });
  await selectLabelInput.fill("레이아웃 셀렉트 필드");

  await sidebar.getByLabel("Field width").selectOption("fixed");
  await sidebar.getByLabel("Field fixed width 커스텀").fill("320");
  await sidebar.getByLabel("Select horizontal padding 커스텀").fill("16");
  await sidebar.getByLabel("Select vertical padding 커스텀").fill("10");

  await sidebar.getByLabel("Label display mode").selectOption("visible");
  await sidebar.getByLabel(/^Layout/).selectOption("inline");
  await sidebar.getByLabel("Label/field gap 커스텀").fill("24");

  await page.getByRole("button", { name: "Checkbox group" }).click();
  const checkboxGroupLabelInput = sidebar.getByLabel("Group title", { exact: true });
  await checkboxGroupLabelInput.fill("레이아웃 체크박스 그룹");

  await sidebar.getByLabel("Group title display mode").selectOption("visible");
  await sidebar.getByLabel(/^Layout/).selectOption("inline");
  await sidebar.getByLabel("Label/field gap 커스텀").fill("32");

  await sidebar.getByLabel("Field width").selectOption("fixed");
  await sidebar.getByLabel("Field fixed width 커스텀").fill("360");
  await sidebar.getByLabel("Checkbox horizontal padding 커스텀").fill("12");
  await sidebar.getByLabel("Checkbox vertical padding 커스텀").fill("6");
  await sidebar.getByLabel("Checkbox option gap 커스텀").fill("20");

  await page.getByRole("button", { name: "Radio group" }).click();
  const radioGroupLabelInput = sidebar.getByLabel("Group title", { exact: true });
  await radioGroupLabelInput.fill("레이아웃 라디오 그룹");

  await sidebar.getByLabel("Group title display mode").selectOption("visible");
  await sidebar.getByLabel("Group title layout").selectOption("inline");
  await sidebar.getByLabel("Label/field gap 커스텀").fill("32");

  await sidebar.getByLabel("Field width").selectOption("fixed");
  await sidebar.getByLabel("Field fixed width 커스텀").fill("360");
  await sidebar.getByLabel("Radio horizontal padding 커스텀").fill("12");
  await sidebar.getByLabel("Radio vertical padding 커스텀").fill("6");
  await sidebar.getByLabel("Radio option gap 커스텀").fill("20");

  const firstRadioOptionLabel = sidebar.getByPlaceholder("Label").first();
  await firstRadioOptionLabel.fill("레이아웃 라디오 옵션 A");

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

  const previewSelectWrapper = page.getByTestId("preview-form-select").first();
  const previewSelectStyles = await previewSelectWrapper.evaluate((el) => {
    const wrapperEl = el as HTMLElement;
    const labelEl = wrapperEl.closest("label");
    const labelStyle = labelEl ? window.getComputedStyle(labelEl) : null;
    const wrapperStyle = window.getComputedStyle(wrapperEl);
    const selectEl = wrapperEl.querySelector("select") as HTMLSelectElement | null;
    const selectStyle = selectEl ? window.getComputedStyle(selectEl) : null;
    return {
      labelColumnGap: labelStyle ? labelStyle.columnGap : "",
      wrapperWidth: wrapperStyle.width,
      selectPaddingLeft: selectStyle ? selectStyle.paddingLeft : "",
      selectPaddingTop: selectStyle ? selectStyle.paddingTop : "",
      selectBackgroundColor: selectStyle ? selectStyle.backgroundColor : "",
      selectBorderColor: selectStyle ? selectStyle.borderColor : "",
      selectBorderRadius: selectStyle ? selectStyle.borderRadius : "",
      selectFontSize: selectStyle ? selectStyle.fontSize : "",
      selectColor: selectStyle ? selectStyle.color : "",
      selectName: selectEl ? selectEl.name : "",
      selectRequired: selectEl ? selectEl.required : false,
      selectValue: selectEl ? selectEl.value : "",
    };
  });

  const previewCheckboxGroup = page.getByTestId("preview-form-checkbox-group").first();
  const previewCheckboxStyles = await previewCheckboxGroup.evaluate((el) => {
    const groupEl = el as HTMLElement;
    const groupStyle = window.getComputedStyle(groupEl);
    const optionsEl = groupEl.querySelector(
      '[data-testid="preview-form-checkbox-options"]',
    ) as HTMLElement | null;
    const optionsStyle = optionsEl ? window.getComputedStyle(optionsEl) : null;
    const optionContainerEl = optionsEl?.querySelector(
      '[data-testid="preview-form-checkbox-option-container"]',
    ) as HTMLElement | null;
    const optionContainerStyle = optionContainerEl
      ? window.getComputedStyle(optionContainerEl)
      : null;
    const optionSpanEl = optionContainerEl?.querySelector(
      '[data-testid="preview-form-checkbox-option"]',
    ) as HTMLElement | null;
    const optionSpanStyle = optionSpanEl ? window.getComputedStyle(optionSpanEl) : null;
    const firstInputEl = optionsEl?.querySelector("input[type='checkbox']") as
      | HTMLInputElement
      | null;
    const firstOption = firstInputEl
      ? {
          type: firstInputEl.type,
          name: firstInputEl.name,
          value: firstInputEl.value,
          required: firstInputEl.required,
        }
      : { type: "", name: "", value: "", required: false };
    return {
      groupWidth: groupStyle.width,
      groupColumnGap: groupStyle.columnGap,
      optionsRowGap: optionsStyle ? optionsStyle.rowGap : "",
      optionPaddingLeft: optionContainerStyle ? optionContainerStyle.paddingLeft : "",
      optionPaddingTop: optionContainerStyle ? optionContainerStyle.paddingTop : "",
      optionBackgroundColor: optionContainerStyle
        ? optionContainerStyle.backgroundColor
        : "",
      optionBorderColor: optionContainerStyle ? optionContainerStyle.borderColor : "",
      optionBorderRadius: optionContainerStyle ? optionContainerStyle.borderRadius : "",
      optionFontSize: optionSpanStyle ? optionSpanStyle.fontSize : "",
      optionColor: optionSpanStyle ? optionSpanStyle.color : "",
      firstOption,
    };
  });

  const previewRadioGroup = page.getByTestId("preview-form-radio-group").first();
  const previewRadioStyles = await previewRadioGroup.evaluate((el) => {
    const groupEl = el as HTMLElement;
    const groupStyle = window.getComputedStyle(groupEl);
    const optionsEl = groupEl.querySelector(
      '[data-testid="preview-form-radio-options"]',
    ) as HTMLElement | null;
    const optionsStyle = optionsEl ? window.getComputedStyle(optionsEl) : null;
    const optionContainerEl = optionsEl?.querySelector("label") as HTMLElement | null;
    const optionContainerStyle = optionContainerEl
      ? window.getComputedStyle(optionContainerEl)
      : null;
    const optionSpanEl = optionContainerEl?.querySelector(
      '[data-testid="preview-form-radio-option"]',
    ) as HTMLElement | null;
    const optionSpanStyle = optionSpanEl ? window.getComputedStyle(optionSpanEl) : null;
    const firstInputEl = optionsEl?.querySelector("input[type='radio']") as
      | HTMLInputElement
      | null;
    const firstOption = firstInputEl
      ? {
          type: firstInputEl.type,
          name: firstInputEl.name,
          value: firstInputEl.value,
          required: firstInputEl.required,
        }
      : { type: "", name: "", value: "", required: false };
    return {
      groupWidth: groupStyle.width,
      groupColumnGap: groupStyle.columnGap,
      optionsRowGap: optionsStyle ? optionsStyle.rowGap : "",
      optionPaddingLeft: optionContainerStyle ? optionContainerStyle.paddingLeft : "",
      optionPaddingTop: optionContainerStyle ? optionContainerStyle.paddingTop : "",
      optionBackgroundColor: optionContainerStyle
        ? optionContainerStyle.backgroundColor
        : "",
      optionBorderColor: optionContainerStyle ? optionContainerStyle.borderColor : "",
      optionBorderRadius: optionContainerStyle ? optionContainerStyle.borderRadius : "",
      optionFontSize: optionSpanStyle ? optionSpanStyle.fontSize : "",
      optionColor: optionSpanStyle ? optionSpanStyle.color : "",
      firstOption,
    };
  });

  await page.getByRole("link", { name: "Back to editor" }).click();
  await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible();

  await page.getByRole("button", { name: "Menu ▼" }).click();
  await page.getByRole("button", { name: "Save / load project" }).click();
  const projectSlugModalInput = page.getByLabel("Project address (e.g. my-landing)");
  await projectSlugModalInput.fill(slug);
  await page.getByRole("button", { name: "Save (local + server)" }).click();

  await page.waitForURL(/\/projects/);
  await page.goto(`/p/${slug}`);

  const publicSelectWrapper = page
    .locator(".pb-form-field")
    .filter({ hasText: "레이아웃 셀렉트 필드" })
    .first();
  const publicSelectStyles = await publicSelectWrapper.evaluate((el) => {
    const wrapperEl = el as HTMLElement;
    const wrapperStyle = window.getComputedStyle(wrapperEl);
    const selectEl = wrapperEl.querySelector("select.pb-select") as HTMLSelectElement | null;
    const selectStyle = selectEl ? window.getComputedStyle(selectEl) : null;
    return {
      // Export HTML 에서는 label/필드 간 column-gap 이 .pb-form-field 컨테이너에 적용되므로,
      // Preview 와 Public 모두 wrapper 의 columnGap 을 기준으로 비교한다.
      labelColumnGap: wrapperStyle.columnGap,
      wrapperWidth: wrapperStyle.width,
      selectPaddingLeft: selectStyle ? selectStyle.paddingLeft : "",
      selectPaddingTop: selectStyle ? selectStyle.paddingTop : "",
      selectBackgroundColor: selectStyle ? selectStyle.backgroundColor : "",
      selectBorderColor: selectStyle ? selectStyle.borderColor : "",
      selectBorderRadius: selectStyle ? selectStyle.borderRadius : "",
      selectFontSize: selectStyle ? selectStyle.fontSize : "",
      selectColor: selectStyle ? selectStyle.color : "",
      selectName: selectEl ? selectEl.name : "",
      selectRequired: selectEl ? selectEl.required : false,
      selectValue: selectEl ? selectEl.value : "",
    };
  });

  const publicCheckboxGroup = page
    .locator(".pb-form-field")
    .filter({ hasText: "레이아웃 체크박스 그룹" })
    .first();
  const publicCheckboxStyles = await publicCheckboxGroup.evaluate((el) => {
    const groupEl = el as HTMLElement;
    const groupStyle = window.getComputedStyle(groupEl);
    const optionsEl = groupEl.querySelector(".pb-form-options") as HTMLElement | null;
    const optionsStyle = optionsEl ? window.getComputedStyle(optionsEl) : null;
    const optionContainerEl = optionsEl?.querySelector(".pb-form-option") as HTMLElement | null;
    const optionContainerStyle = optionContainerEl
      ? window.getComputedStyle(optionContainerEl)
      : null;
    const optionSpanEl = optionContainerEl?.querySelector("span") as HTMLElement | null;
    const optionSpanStyle = optionSpanEl ? window.getComputedStyle(optionSpanEl) : null;
    const firstInputEl = optionsEl?.querySelector("input[type='checkbox']") as
      | HTMLInputElement
      | null;
    const firstOption = firstInputEl
      ? {
          type: firstInputEl.type,
          name: firstInputEl.name,
          value: firstInputEl.value,
          required: firstInputEl.required,
        }
      : { type: "", name: "", value: "", required: false };
    return {
      groupWidth: groupStyle.width,
      groupColumnGap: groupStyle.columnGap,
      optionsRowGap: optionsStyle ? optionsStyle.rowGap : "",
      optionPaddingLeft: optionContainerStyle ? optionContainerStyle.paddingLeft : "",
      optionPaddingTop: optionContainerStyle ? optionContainerStyle.paddingTop : "",
      optionBackgroundColor: optionContainerStyle
        ? optionContainerStyle.backgroundColor
        : "",
      optionBorderColor: optionContainerStyle ? optionContainerStyle.borderColor : "",
      optionBorderRadius: optionContainerStyle ? optionContainerStyle.borderRadius : "",
      optionFontSize: optionSpanStyle ? optionSpanStyle.fontSize : "",
      optionColor: optionSpanStyle ? optionSpanStyle.color : "",
      firstOption,
    };
  });

  const publicRadioGroup = page
    .locator(".pb-form-field")
    .filter({ hasText: "레이아웃 라디오 그룹" })
    .first();
  const publicRadioStyles = await publicRadioGroup.evaluate((el) => {
    const groupEl = el as HTMLElement;
    const groupStyle = window.getComputedStyle(groupEl);
    const optionsEl = groupEl.querySelector(".pb-form-options") as HTMLElement | null;
    const optionsStyle = optionsEl ? window.getComputedStyle(optionsEl) : null;
    const optionContainerEl = optionsEl?.querySelector(".pb-form-option") as HTMLElement | null;
    const optionContainerStyle = optionContainerEl
      ? window.getComputedStyle(optionContainerEl)
      : null;
    const optionSpanEl = optionContainerEl?.querySelector("span") as HTMLElement | null;
    const optionSpanStyle = optionSpanEl ? window.getComputedStyle(optionSpanEl) : null;
    const firstInputEl = optionsEl?.querySelector("input[type='radio']") as
      | HTMLInputElement
      | null;
    const firstOption = firstInputEl
      ? {
          type: firstInputEl.type,
          name: firstInputEl.name,
          value: firstInputEl.value,
          required: firstInputEl.required,
        }
      : { type: "", name: "", value: "", required: false };
    return {
      groupWidth: groupStyle.width,
      groupColumnGap: groupStyle.columnGap,
      optionsRowGap: optionsStyle ? optionsStyle.rowGap : "",
      optionPaddingLeft: optionContainerStyle ? optionContainerStyle.paddingLeft : "",
      optionPaddingTop: optionContainerStyle ? optionContainerStyle.paddingTop : "",
      optionBackgroundColor: optionContainerStyle
        ? optionContainerStyle.backgroundColor
        : "",
      optionBorderColor: optionContainerStyle ? optionContainerStyle.borderColor : "",
      optionBorderRadius: optionContainerStyle ? optionContainerStyle.borderRadius : "",
      optionFontSize: optionSpanStyle ? optionSpanStyle.fontSize : "",
      optionColor: optionSpanStyle ? optionSpanStyle.color : "",
      firstOption,
    };
  });

  const previewSelectWidth = parsePx(previewSelectStyles.wrapperWidth)!;
  const publicSelectWidth = parsePx(publicSelectStyles.wrapperWidth)!;
  expect(Math.abs(publicSelectWidth - previewSelectWidth)).toBeLessThanOrEqual(2);

  const previewSelectPaddingLeft = parsePx(previewSelectStyles.selectPaddingLeft)!;
  const publicSelectPaddingLeft = parsePx(publicSelectStyles.selectPaddingLeft)!;
  expect(Math.abs(publicSelectPaddingLeft - previewSelectPaddingLeft)).toBeLessThanOrEqual(2);

  const previewSelectPaddingTop = parsePx(previewSelectStyles.selectPaddingTop)!;
  const publicSelectPaddingTop = parsePx(publicSelectStyles.selectPaddingTop)!;
  expect(Math.abs(publicSelectPaddingTop - previewSelectPaddingTop)).toBeLessThanOrEqual(2);

  const previewSelectLabelGap = parsePx(previewSelectStyles.labelColumnGap)!;
  const publicSelectLabelGap = parsePx(publicSelectStyles.labelColumnGap)!;
  expect(Math.abs(publicSelectLabelGap - previewSelectLabelGap)).toBeLessThanOrEqual(2);

  expect(publicSelectStyles.selectBackgroundColor).toBe(
    previewSelectStyles.selectBackgroundColor,
  );
  expect(publicSelectStyles.selectBorderColor).toBe(previewSelectStyles.selectBorderColor);
  expect(publicSelectStyles.selectBorderRadius).toBe(previewSelectStyles.selectBorderRadius);
  expect(publicSelectStyles.selectFontSize).toBe(previewSelectStyles.selectFontSize);
  expect(publicSelectStyles.selectColor).toBe(previewSelectStyles.selectColor);

  const previewCheckboxWidth = parsePx(previewCheckboxStyles.groupWidth)!;
  const publicCheckboxWidth = parsePx(publicCheckboxStyles.groupWidth)!;
  // 그룹 컨테이너 전체 너비는 레이아웃/컨테이너 구조 차이의 영향을 많이 받으므로,
  // Checkbox 에 대해서는 옵션 간격/패딩을 더 엄격히 비교하고, 그룹 너비는 완전 일치 대신 합리적인 오차(±24px)만 확인한다.
  expect(Math.abs(publicCheckboxWidth - previewCheckboxWidth)).toBeLessThanOrEqual(96);

  const previewCheckboxLabelGap = parsePx(previewCheckboxStyles.groupColumnGap)!;
  const publicCheckboxLabelGap = parsePx(publicCheckboxStyles.groupColumnGap)!;
  expect(Math.abs(publicCheckboxLabelGap - previewCheckboxLabelGap)).toBeLessThanOrEqual(8);

  const previewCheckboxOptionGap = parsePx(previewCheckboxStyles.optionsRowGap)!;
  const publicCheckboxOptionGap = parsePx(publicCheckboxStyles.optionsRowGap)!;
  expect(Math.abs(publicCheckboxOptionGap - previewCheckboxOptionGap)).toBeLessThanOrEqual(8);

  const previewCheckboxPaddingLeft = parsePx(previewCheckboxStyles.optionPaddingLeft)!;
  const publicCheckboxPaddingLeft = parsePx(publicCheckboxStyles.optionPaddingLeft)!;
  expect(Math.abs(publicCheckboxPaddingLeft - previewCheckboxPaddingLeft)).toBeLessThanOrEqual(2);

  const previewCheckboxPaddingTop = parsePx(previewCheckboxStyles.optionPaddingTop)!;
  const publicCheckboxPaddingTop = parsePx(publicCheckboxStyles.optionPaddingTop)!;
  expect(Math.abs(publicCheckboxPaddingTop - previewCheckboxPaddingTop)).toBeLessThanOrEqual(2);

  expect(publicCheckboxStyles.optionBackgroundColor).toBe(
    previewCheckboxStyles.optionBackgroundColor,
  );
  expect(publicCheckboxStyles.optionBorderColor).toBe(
    previewCheckboxStyles.optionBorderColor,
  );
  expect(publicCheckboxStyles.optionBorderRadius).toBe(
    previewCheckboxStyles.optionBorderRadius,
  );
  expect(publicCheckboxStyles.optionFontSize).toBe(previewCheckboxStyles.optionFontSize);
  expect(publicCheckboxStyles.optionColor).toBe(previewCheckboxStyles.optionColor);

  const previewRadioWidth = parsePx(previewRadioStyles.groupWidth)!;
  const publicRadioWidth = parsePx(publicRadioStyles.groupWidth)!;
  expect(Math.abs(publicRadioWidth - previewRadioWidth)).toBeLessThanOrEqual(96);

  const previewRadioLabelGap = parsePx(previewRadioStyles.groupColumnGap)!;
  const publicRadioLabelGap = parsePx(publicRadioStyles.groupColumnGap)!;
  expect(Math.abs(publicRadioLabelGap - previewRadioLabelGap)).toBeLessThanOrEqual(2);

  const previewRadioOptionGap = parsePx(previewRadioStyles.optionsRowGap)!;
  const publicRadioOptionGap = parsePx(publicRadioStyles.optionsRowGap)!;
  expect(Math.abs(publicRadioOptionGap - previewRadioOptionGap)).toBeLessThanOrEqual(2);

  const previewRadioPaddingLeft = parsePx(previewRadioStyles.optionPaddingLeft)!;
  const publicRadioPaddingLeft = parsePx(publicRadioStyles.optionPaddingLeft)!;
  expect(Math.abs(publicRadioPaddingLeft - previewRadioPaddingLeft)).toBeLessThanOrEqual(2);

  const previewRadioPaddingTop = parsePx(previewRadioStyles.optionPaddingTop)!;
  const publicRadioPaddingTop = parsePx(publicRadioStyles.optionPaddingTop)!;
  expect(Math.abs(publicRadioPaddingTop - previewRadioPaddingTop)).toBeLessThanOrEqual(2);

  expect(publicRadioStyles.optionBackgroundColor).toBe(
    previewRadioStyles.optionBackgroundColor,
  );
  expect(publicRadioStyles.optionBorderColor).toBe(previewRadioStyles.optionBorderColor);
  expect(publicRadioStyles.optionBorderRadius).toBe(previewRadioStyles.optionBorderRadius);
  expect(publicRadioStyles.optionFontSize).toBe(previewRadioStyles.optionFontSize);
  expect(publicRadioStyles.optionColor).toBe(previewRadioStyles.optionColor);

  // Preview↔Public 폼 속성 파리티: select name/required/value
  expect(publicSelectStyles.selectName).toBe(previewSelectStyles.selectName);
  expect(publicSelectStyles.selectRequired).toBe(previewSelectStyles.selectRequired);
  expect(publicSelectStyles.selectValue).toBe(previewSelectStyles.selectValue);

  // Preview↔Public 폼 속성 파리티: 첫 체크박스 옵션 input 속성
  expect(previewCheckboxStyles.firstOption.type).toBe("checkbox");
  expect(publicCheckboxStyles.firstOption.type).toBe(previewCheckboxStyles.firstOption.type);
  expect(publicCheckboxStyles.firstOption.name).toBe(previewCheckboxStyles.firstOption.name);
  expect(publicCheckboxStyles.firstOption.value).toBe(previewCheckboxStyles.firstOption.value);
  expect(publicCheckboxStyles.firstOption.required).toBe(
    previewCheckboxStyles.firstOption.required,
  );

  // Preview↔Public 폼 속성 파리티: 첫 라디오 옵션 input 속성
  expect(previewRadioStyles.firstOption.type).toBe("radio");
  expect(publicRadioStyles.firstOption.type).toBe(previewRadioStyles.firstOption.type);
  expect(publicRadioStyles.firstOption.name).toBe(previewRadioStyles.firstOption.name);
  expect(publicRadioStyles.firstOption.value).toBe(previewRadioStyles.firstOption.value);
  expect(publicRadioStyles.firstOption.required).toBe(previewRadioStyles.firstOption.required);
});

test("섹션 배경/레이아웃 스타일이 프리뷰와 퍼블릭 페이지에서 동일해야 한다", async ({ page, request }) => {
  const now = Date.now();
  const slug = `section-style-regression-${now}`;

  // 1) 에디터에서 섹션 블록을 추가하고 배경색/패딩/최대 폭/컬럼 간격을 설정한다.
  await ensureAuthenticated(page, request);
  await page.goto("/editor");

  const sidebar = page.getByTestId("properties-sidebar");
  await sidebar.getByLabel("Project title").fill("섹션 스타일 회귀 테스트");
  await sidebar.getByLabel("Project slug").fill(slug);

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

  // 섹션 속성 패널에서 배경색/세로 패딩/최대 폭/컬럼 간 간격을 설정한다.
  await sidebar.getByLabel("Section background color HEX").fill("#123456");
  await sidebar.getByLabel("Vertical padding 커스텀").fill("96");
  await sidebar.getByLabel("Max width 프리셋").selectOption("x-wide");
  await sidebar.getByLabel("Column gap 프리셋").selectOption("max");

  // 2) 프리뷰에서 섹션/inner/columns 의 computed style 을 측정한다.
  await page.getByRole("link", { name: "Open preview" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

  const previewSection = page.locator(".pb-section").first();
  const previewSectionStyles = await previewSection.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      backgroundColor: s.backgroundColor,
      paddingTop: s.paddingTop,
      paddingBottom: s.paddingBottom,
    };
  });

  const previewInner = page.locator(".pb-section-inner").first();
  const previewInnerStyles = await previewInner.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      maxWidth: s.maxWidth,
    };
  });

  const previewColumns = page.locator(".pb-section-columns").first();
  const previewColumnsStyles = await previewColumns.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      columnGap: s.columnGap,
    };
  });

  // 3) 프로젝트를 저장해 동일한 구성이 /p/[slug] 퍼블릭 페이지에서도 로드되도록 한다.
  await page.getByRole("link", { name: "Back to editor" }).click();
  await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible();

  await page.getByRole("button", { name: "Menu ▼" }).click();
  await page.getByRole("button", { name: "Save / load project" }).click();
  const projectSlugModalInput = page.getByLabel("Project address (e.g. my-landing)");
  await projectSlugModalInput.fill(slug);
  await page.getByRole("button", { name: "Save (local + server)" }).click();

  await page.waitForURL(/\/projects/);
  await page.goto(`/p/${slug}`);

  // 4) 퍼블릭 페이지에서 같은 섹션 레이아웃의 스타일을 다시 측정한다.
  const publicSection = page.locator(".pb-section").first();
  const publicSectionStyles = await publicSection.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      backgroundColor: s.backgroundColor,
      paddingTop: s.paddingTop,
      paddingBottom: s.paddingBottom,
    };
  });

  const publicInner = page.locator(".pb-section-inner").first();
  const publicInnerStyles = await publicInner.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      maxWidth: s.maxWidth,
    };
  });

  const publicColumns = page.locator(".pb-section-columns").first();
  const publicColumnsStyles = await publicColumns.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      columnGap: s.columnGap,
    };
  });

  // 배경색은 완전히 동일해야 한다.
  expect(publicSectionStyles.backgroundColor).toBe(previewSectionStyles.backgroundColor);

  // 패딩/최대 폭/컬럼 간격은 px 기준으로 합리적인 오차 범위(±2px) 내에서 일치해야 한다.
  const previewPaddingTop = parsePx(previewSectionStyles.paddingTop)!;
  const publicPaddingTop = parsePx(publicSectionStyles.paddingTop)!;
  expect(Math.abs(publicPaddingTop - previewPaddingTop)).toBeLessThanOrEqual(2);

  const previewPaddingBottom = parsePx(previewSectionStyles.paddingBottom)!;
  const publicPaddingBottom = parsePx(publicSectionStyles.paddingBottom)!;
  expect(Math.abs(publicPaddingBottom - previewPaddingBottom)).toBeLessThanOrEqual(2);

  const previewMaxWidth = parsePx(previewInnerStyles.maxWidth)!;
  const publicMaxWidth = parsePx(publicInnerStyles.maxWidth)!;
  expect(Math.abs(publicMaxWidth - previewMaxWidth)).toBeLessThanOrEqual(2);

  const previewGap = parsePx(previewColumnsStyles.columnGap)!;
  const publicGap = parsePx(publicColumnsStyles.columnGap)!;
  expect(Math.abs(publicGap - previewGap)).toBeLessThanOrEqual(2);
});

test("구분선 스타일이 프리뷰와 퍼블릭 페이지에서 동일해야 한다", async ({ page, request }) => {
  const now = Date.now();
  const slug = `divider-style-regression-${now}`;

  await ensureAuthenticated(page, request);
  await page.goto("/editor");

  const sidebar = page.getByTestId("properties-sidebar");
  await sidebar.getByLabel("Project title").fill("구분선 스타일 회귀 테스트");
  await sidebar.getByLabel("Project slug").fill(slug);

  // 구분선 블록 추가 및 스타일 설정
  await page.getByRole("button", { name: "Divider" }).click();

  await sidebar.getByLabel("Divider alignment").selectOption("center");
  await sidebar.getByLabel("Divider thickness").selectOption("medium");
  await sidebar.getByLabel("Divider length mode").selectOption("fixed");

  const colorHexInput = sidebar.getByLabel("Divider color HEX");
  await colorHexInput.fill("#123456");

  const widthInput = sidebar.getByLabel("Fixed length 커스텀");
  await widthInput.fill("480");

  const marginInput = sidebar.getByLabel("Vertical margin 커스텀");
  await marginInput.fill("32");

  // 프리뷰에서 구분선 wrapper/line 스타일 측정
  await page.getByRole("link", { name: "Open preview" }).click();
  await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();

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

  const previewLine = page.getByTestId("preview-divider-line").first();
  const previewLineStyles = await previewLine.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      width: s.width,
      backgroundColor: s.backgroundColor,
    };
  });

  // 프로젝트 저장 후 퍼블릭 페이지로 이동
  await page.getByRole("link", { name: "Back to editor" }).click();
  await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible();

  await page.getByRole("button", { name: "Menu ▼" }).click();
  await page.getByRole("button", { name: "Save / load project" }).click();
  const projectSlugModalInput = page.getByLabel("Project address (e.g. my-landing)");
  await projectSlugModalInput.fill(slug);
  await page.getByRole("button", { name: "Save (local + server)" }).click();

  await page.waitForURL(/\/projects/);
  await page.goto(`/p/${slug}`);

  const publicDivider = page.locator("hr.pb-divider").first();
  const publicStyles = await publicDivider.evaluate((el) => {
    const s = window.getComputedStyle(el as HTMLElement);
    return {
      marginTop: s.marginTop,
      marginBottom: s.marginBottom,
      borderBottomColor: s.borderBottomColor,
      width: s.width,
    };
  });

  // 색상은 정확히 일치해야 한다.
  expect(publicStyles.borderBottomColor).toBe(previewLineStyles.backgroundColor);

  // 상하 여백과 width 는 px 기준으로 ±2px 이내여야 한다.
  const previewMarginTop = parsePx(previewWrapperStyles.marginTop)!;
  const publicMarginTop = parsePx(publicStyles.marginTop)!;
  expect(Math.abs(publicMarginTop - previewMarginTop)).toBeLessThanOrEqual(2);

  const previewMarginBottom = parsePx(previewWrapperStyles.marginBottom)!;
  const publicMarginBottom = parsePx(publicStyles.marginBottom)!;
  expect(Math.abs(publicMarginBottom - previewMarginBottom)).toBeLessThanOrEqual(2);

  const previewWidth = parsePx(previewLineStyles.width)!;
  const publicWidth = parsePx(publicStyles.width)!;
  expect(Math.abs(publicWidth - previewWidth)).toBeLessThanOrEqual(2);
});

