import { expect, test, type Browser, type Page, type TestInfo } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
import { isLoginPage, loginWithCredentials } from "../auth";
import { loadChecklist, loadIssueMetadata } from "../checklist";
import { getRuntimeQaEnv, type RuntimeQaEnv } from "../env";
import type { QaEnvironment, QaPageDefinition } from "../types";

interface ScenarioOptions {
  issueId: string;
  environment: QaEnvironment;
}

interface ApiOptions {
  method?: string;
  body?: unknown;
}

interface ApiResult<T = unknown> {
  method: string;
  path: string;
  status: number;
  ok: boolean;
  json: T;
  message?: string;
}

interface PayloadResponse<T> {
  payload?: T;
  result?: {
    code?: number;
    message?: string;
  };
  message?: string;
}

interface OptionValue {
  value?: string;
  code?: string;
  label?: string;
  name?: string;
}

interface MetadataSetting {
  id: number;
  name: string;
  field: string;
  required?: boolean;
}

interface MetadataSettingsPage {
  totalElements?: number;
  content?: MetadataSetting[];
}

interface MetadataValue {
  id?: number;
  settingId?: number;
  field?: string;
  name?: string;
  required?: boolean;
  value?: string;
  midType?: string;
}

interface PgMid {
  id?: string;
  mid?: string;
  rootMid?: string;
  memo?: string;
  midType?: OptionValue | string;
  status?: OptionValue | string;
  isBtecaMid?: boolean;
  contract?: {
    id?: string;
    name?: string;
    provider?: {
      id?: string;
      code?: string;
      name?: string;
    };
    corporation?: {
      id?: string;
      code?: string;
      name?: string;
    };
  };
  corporationId?: string;
  terminalVendor?: {
    id?: string | null;
    code?: string;
    name?: string;
    status?: OptionValue | string;
  };
  keyinMetadata?: MetadataValue[];
  keyinUsers?: Array<{ id?: string }>;
  keyinInputType?: OptionValue | string;
}

interface CreatedMid {
  id: string;
  mid: string;
  searchKey?: string;
  apiKey?: string;
}

type PgMidType = "KEYIN" | "ISP" | "TERMINAL";

const PAYLETTER_CONTRACT_ID = "tenant-id-redacted";
const PAYLETTER_CONTRACT_LABEL = "A_주홍석_법인_페이레터";
const PAYLETTER_SEARCH_KEY_SETTING_ID = 34;
const PAYLETTER_VENDOR_ID = "tenant-id-redacted";
const PAYLETTER_VENDOR_LABEL = "A_주홍석_가맹5_페이레터";

const NON_PAYLETTER_TERMINAL_CONTRACT_LABEL = "반길현_계약_다날";
const NON_PAYLETTER_PROVIDER_ID = "11f1-6d4n-a1pg0vdr-4nal-000000000001";

const COMMON_METADATA_CONTRACT_ID = "tenant-id-redacted";
const COMMON_METADATA_CONTRACT_LABEL = "주홍석_루시페이먼츠";
const COMMON_METADATA_SETTING_ID = 33;

export function runPayletterMidReceiptKeySuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const pageDefinition = findPageDefinition(checklist.pages, "PG MID 관리");
  const items = checklist.checklist;

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    for (const [role, itemIndex] of [["ADMIN", 0], ["TA", 1]] as const) {
      const runtimeEnv = getRuntimeQaEnv(options.environment, pageDefinition.application ?? "admin", role);
      const blockReason = getRuntimeBlockReason(runtimeEnv);

      test.describe(`[${role}] role`, () => {
        test.describe(pageDefinition.name, () => {
          test.skip(!!blockReason, blockReason);

          test(items[itemIndex], async ({ browser }, testInfo) => {
            test.setTimeout(90_000);
            const createdIds: string[] = [];

            await withRolePage(browser, runtimeEnv, async (page) => {
              try {
                const created = await createPayletterTerminalMidViaUi(page, runtimeEnv, role, testInfo, pageDefinition);
                createdIds.push(created.id);

                const detail = await loadMidDetail(page, runtimeEnv, created.id);
                expect(getMetadataValue(detail, "searchKey"), "등록 후 상세 조회의 영수증 조회 키가 입력값과 일치해야 합니다.").toBe(created.searchKey);
                expect(getOptionValue(detail.midType), "등록한 MID 타입은 단말기여야 합니다.").toBe("TERMINAL");

                await attachEvidenceLog(testInfo, pageDefinition, role, "payletter-terminal-create", [
                  `${role} 권한으로 페이레터 단말기 MID 등록 화면에서 영수증 조회 키를 입력하고 저장했습니다.`,
                  `생성 MID: ${created.mid}`,
                  `생성 ID: ${created.id}`,
                  `상세 조회 metadata: searchKey=${created.searchKey}`
                ]);
                await attachPageScreenshot(page, testInfo, pageDefinition, role, "payletter-terminal-create");
              } finally {
                await cleanupCreatedMids(page, runtimeEnv, createdIds);
              }
            });
          });
        });
      });
    }

    const adminEnv = getRuntimeQaEnv(options.environment, pageDefinition.application ?? "admin", "ADMIN");
    const adminBlockReason = getRuntimeBlockReason(adminEnv);

    test.describe("[ADMIN] role", () => {
      test.describe(pageDefinition.name, () => {
        test.skip(!!adminBlockReason, adminBlockReason);

        test(items[2], async ({ browser }, testInfo) => {
          test.setTimeout(90_000);
          const createdIds: string[] = [];

          await withRolePage(browser, adminEnv, async (page) => {
            try {
              const initialSearchKey = `OLD-${makeQaMid("P")}`;
              const created = await createPayletterTerminalMidViaApi(page, adminEnv, {
                searchKey: initialSearchKey,
                memo: `QA #3399 update ${initialSearchKey}`
              });
              createdIds.push(created.id);

              await openUpdatePage(page, adminEnv, created.id);
              await expect(page.locator("#searchKey"), "수정 화면에 기존 영수증 조회 키가 표시되어야 합니다.").toHaveValue(initialSearchKey);
              await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", "payletter-update-before");

              const updatedSearchKey = `NEW-${created.mid}`;
              await updateSearchKeyViaUi(page, adminEnv, created.id, updatedSearchKey);
              const detail = await loadMidDetail(page, adminEnv, created.id);
              expect(getMetadataValue(detail, "searchKey"), "수정 후 상세 조회의 영수증 조회 키가 변경값과 일치해야 합니다.").toBe(updatedSearchKey);

              await openUpdatePage(page, adminEnv, created.id);
              await expect(page.locator("#searchKey"), "수정 후 다시 연 화면에 변경한 영수증 조회 키가 유지되어야 합니다.").toHaveValue(updatedSearchKey);

              await attachEvidenceLog(testInfo, pageDefinition, "ADMIN", "payletter-update-persist", [
                "페이레터 단말기 MID 수정 화면에서 영수증 조회 키를 변경하고 저장했습니다.",
                `MID: ${created.mid}`,
                `변경 전: ${initialSearchKey}`,
                `변경 후: ${updatedSearchKey}`,
                `상세 조회 metadata: searchKey=${getMetadataValue(detail, "searchKey") ?? "-"}`
              ]);
              await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", "payletter-update-after");
            } finally {
              await cleanupCreatedMids(page, adminEnv, createdIds);
            }
          });
        });

        test(items[3], async ({ browser }, testInfo) => {
          test.setTimeout(60_000);

          await withRolePage(browser, adminEnv, async (page) => {
            const settings = await loadMetadataSettings(page, adminEnv, NON_PAYLETTER_PROVIDER_ID, "TERMINAL");
            expect(settings.content ?? [], "페이레터 외 단말기 metadata 설정에는 영수증 조회 키가 없어야 합니다.").not.toEqual(
              expect.arrayContaining([expect.objectContaining({ field: "searchKey" })])
            );

            await openCreatePage(page, adminEnv, "TERMINAL");
            await selectContract(page, NON_PAYLETTER_TERMINAL_CONTRACT_LABEL);
            await expect(page.locator("body"), "다른 PG 단말기 화면에는 영수증 조회 키 라벨이 보이면 안 됩니다.").not.toContainText(
              /영수증\s*조회\s*키/,
              { timeout: 3_000 }
            );
            await expect(page.locator("#searchKey"), "다른 PG 단말기 화면에는 searchKey 입력이 생성되면 안 됩니다.").toHaveCount(0);

            await attachEvidenceLog(testInfo, pageDefinition, "ADMIN", "non-payletter-terminal-no-search-key", [
              "다날 단말기 MID 등록 화면에서 영수증 조회 키 입력 항목이 노출되지 않는지 확인했습니다.",
              `계약: ${NON_PAYLETTER_TERMINAL_CONTRACT_LABEL}`,
              `metadata 설정 건수: ${settings.totalElements ?? settings.content?.length ?? 0}`
            ]);
            await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", "non-payletter-terminal-no-search-key");
          });
        });

        test(items[4], async ({ browser }, testInfo) => {
          test.setTimeout(120_000);
          const createdIds: string[] = [];

          await withRolePage(browser, adminEnv, async (page) => {
            try {
              const keyin = await createCommonMetadataMidViaUi(page, adminEnv, "KEYIN", testInfo, pageDefinition);
              createdIds.push(keyin.id);
              const keyinDetail = await loadMidDetail(page, adminEnv, keyin.id);
              expect(getMetadataValue(keyinDetail, "apiKey"), "수기 MID 상세 조회의 API KEY metadata가 입력값과 일치해야 합니다.").toBe(keyin.apiKey);

              const isp = await createCommonMetadataMidViaUi(page, adminEnv, "ISP", testInfo, pageDefinition);
              createdIds.push(isp.id);
              const ispDetail = await loadMidDetail(page, adminEnv, isp.id);
              expect(getMetadataValue(ispDetail, "apiKey"), "ISP MID 상세 조회의 API KEY metadata가 입력값과 일치해야 합니다.").toBe(isp.apiKey);

              await attachEvidenceLog(testInfo, pageDefinition, "ADMIN", "common-metadata-keyin-isp", [
                "수기결제와 ISP MID 등록 화면에서 기존 결제 공통 metadata(API KEY)를 입력하고 저장했습니다.",
                `수기 MID: ${keyin.mid} / apiKey=${keyin.apiKey}`,
                `ISP MID: ${isp.mid} / apiKey=${isp.apiKey}`
              ]);
            } finally {
              await cleanupCreatedMids(page, adminEnv, createdIds);
            }
          });
        });

        test(items[5], async ({ browser }, testInfo) => {
          test.setTimeout(90_000);
          const createdIds: string[] = [];

          await withRolePage(browser, adminEnv, async (page) => {
            try {
              const originalSearchKey = `KEEP-${makeQaMid("V")}`;
              const originalMemo = `QA #3399 preserve ${originalSearchKey}`;
              const created = await createPayletterTerminalMidViaApi(page, adminEnv, {
                searchKey: originalSearchKey,
                memo: originalMemo,
                terminalVendorId: PAYLETTER_VENDOR_ID
              });
              createdIds.push(created.id);

              const before = await loadMidDetail(page, adminEnv, created.id);
              expect(before.terminalVendor?.id, "검증용 MID에는 가맹점 연결이 저장되어야 합니다.").toBe(PAYLETTER_VENDOR_ID);
              expect(getOptionValue(before.status), "가맹점 연결 MID는 정상 상태여야 합니다.").toBe("ACTIVE");
              expect(before.memo, "검증용 MID 메모가 저장되어야 합니다.").toBe(originalMemo);

              await openUpdatePage(page, adminEnv, created.id);
              await expect(page.locator("body"), "수정 화면에 연결 가맹점명이 보여야 합니다.").toContainText(PAYLETTER_VENDOR_LABEL);
              await expect(page.locator("#memo"), "수정 화면에 기존 메모가 유지되어야 합니다.").toHaveValue(originalMemo);

              const updatedSearchKey = `KEEP-UPDATED-${created.mid}`;
              await updateSearchKeyViaUi(page, adminEnv, created.id, updatedSearchKey);
              const after = await loadMidDetail(page, adminEnv, created.id);

              expect(after.terminalVendor?.id, "영수증 조회 키 수정 후 가맹점 연결이 유지되어야 합니다.").toBe(PAYLETTER_VENDOR_ID);
              expect(getOptionValue(after.status), "영수증 조회 키 수정 후 상태가 유지되어야 합니다.").toBe(getOptionValue(before.status));
              expect(after.memo, "영수증 조회 키 수정 후 메모가 유지되어야 합니다.").toBe(originalMemo);
              expect(getMetadataValue(after, "searchKey"), "영수증 조회 키만 변경되어야 합니다.").toBe(updatedSearchKey);

              await attachEvidenceLog(testInfo, pageDefinition, "ADMIN", "preserve-existing-values", [
                "가맹점 연결이 있는 페이레터 단말기 MID에서 영수증 조회 키만 수정하고 기존 입력값 유지 여부를 확인했습니다.",
                `MID: ${created.mid}`,
                `가맹점: ${after.terminalVendor?.name ?? "-"} (${after.terminalVendor?.id ?? "-"})`,
                `상태: ${getOptionValue(before.status) ?? "-"} -> ${getOptionValue(after.status) ?? "-"}`,
                `메모: ${after.memo ?? "-"}`,
                `영수증 조회 키: ${originalSearchKey} -> ${updatedSearchKey}`
              ]);
              await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", "preserve-existing-values");
            } finally {
              await cleanupCreatedMids(page, adminEnv, createdIds);
            }
          });
        });
      });
    });
  });
}

function findPageDefinition(pages: QaPageDefinition[], name: string): QaPageDefinition {
  const exact = pages.find((pageDefinition) => pageDefinition.name === name);
  if (exact) {
    return exact;
  }
  const partial = pages.find((pageDefinition) => pageDefinition.name.includes(name));
  if (partial) {
    return partial;
  }
  throw new Error(`checklist.json pages에 '${name}' 페이지 정의가 필요합니다.`);
}

function getRuntimeBlockReason(runtimeEnv: RuntimeQaEnv): string | undefined {
  const appKey = runtimeEnv.application.toUpperCase();
  const envKey = runtimeEnv.environment.toUpperCase();

  if (!runtimeEnv.baseUrl) {
    return `BIX_${appKey}_${envKey}_BASE_URL 또는 BIX_${appKey}_${envKey}_LOGIN_URL이 필요합니다.`;
  }
  if (!runtimeEnv.tenantId) {
    return `BIX_${appKey}_${envKey}_TENANT_ID가 필요합니다.`;
  }
  if (!runtimeEnv.storageState && (!runtimeEnv.username || !runtimeEnv.password || !runtimeEnv.loginUrl)) {
    return `${runtimeEnv.application}/${runtimeEnv.environment}/${runtimeEnv.role} role의 storageState 또는 로그인 계정 정보가 필요합니다.`;
  }

  return undefined;
}

async function withRolePage(
  browser: Browser,
  runtimeEnv: RuntimeQaEnv,
  callback: (page: Page) => Promise<void>
): Promise<void> {
  const context = await browser.newContext(
    runtimeEnv.storageState
      ? { storageState: runtimeEnv.storageState, acceptDownloads: true }
      : { acceptDownloads: true }
  );
  const page = await context.newPage();

  try {
    await callback(page);
  } finally {
    await context.close();
  }
}

async function openAuthenticatedUrl(
  page: Page,
  targetUrl: string,
  runtimeEnv: RuntimeQaEnv,
  pageName: string
): Promise<void> {
  if (!runtimeEnv.storageState) {
    await loginWithCredentials(page, runtimeEnv);
  }

  await page.goto(targetUrl);
  await waitForNavigationToSettle(page);

  if (await isLoginPage(page, runtimeEnv)) {
    await loginWithCredentials(page, runtimeEnv);
    await page.goto(targetUrl);
    await waitForNavigationToSettle(page);
  }

  expect(await isLoginPage(page, runtimeEnv), `${pageName} 업무 화면 진입 전 로그인 화면을 벗어나야 합니다.`).toBe(false);
}

async function waitForNavigationToSettle(page: Page): Promise<void> {
  await page.waitForLoadState("domcontentloaded");
  await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
  await page.waitForTimeout(500);
}

async function openCreatePage(page: Page, runtimeEnv: RuntimeQaEnv, midType: PgMidType): Promise<void> {
  const url = buildPgMidUrl(runtimeEnv, "/business/pg-mid-manage/create");
  url.searchParams.set("midType", midType);
  await openAuthenticatedUrl(page, url.toString(), runtimeEnv, `PG MID 생성 ${midType}`);
  await expect(page.locator("body"), "PG MID 생성 화면이 표시되어야 합니다.").toContainText(/PG MID\s*생성/);
}

async function openUpdatePage(page: Page, runtimeEnv: RuntimeQaEnv, id: string): Promise<void> {
  const url = buildPgMidUrl(runtimeEnv, `/business/pg-mid-manage/update/${id}`);
  await openAuthenticatedUrl(page, url.toString(), runtimeEnv, "PG MID 수정");
  await expect(page.locator("body"), "PG MID 수정 화면이 표시되어야 합니다.").toContainText(/PG MID\s*수정/);
}

async function createPayletterTerminalMidViaUi(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  role: string,
  testInfo: TestInfo,
  pageDefinition: QaPageDefinition
): Promise<CreatedMid> {
  await openCreatePage(page, runtimeEnv, "TERMINAL");
  await selectContract(page, PAYLETTER_CONTRACT_LABEL);
  await expect(page.locator("#searchKey"), "페이레터 단말기 등록 화면에 영수증 조회 키 입력이 보여야 합니다.").toBeVisible();

  const mid = makeQaMid(role === "TA" ? "TA" : "AD");
  const searchKey = `SEARCH-${mid}`;
  await fillTextInput(page, "#mid", mid);
  await fillTextInput(page, "#rootMid", mid);
  await fillTextInput(page, "#memo", `QA #3399 ${role} ${mid}`);
  await fillTextInput(page, "#searchKey", searchKey);
  await attachPageScreenshot(page, testInfo, pageDefinition, role, "payletter-terminal-create-filled");

  const { responseJson, requestBody } = await submitCreateForm(page);
  expect(requestBody.contractId, "등록 요청 계약은 페이레터 계약이어야 합니다.").toBe(PAYLETTER_CONTRACT_ID);
  expect(requestBody.midType, "등록 요청 MID 타입은 단말기여야 합니다.").toBe("TERMINAL");
  expect(getRequestMetadataValue(requestBody, "searchKey"), "등록 요청 payload에 영수증 조회 키가 포함되어야 합니다.").toBe(searchKey);

  if (!responseJson.payload) {
    throw new Error("페이레터 단말기 MID 생성 응답에 ID가 없습니다.");
  }

  return {
    id: responseJson.payload,
    mid,
    searchKey
  };
}

async function createCommonMetadataMidViaUi(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  midType: "KEYIN" | "ISP",
  testInfo: TestInfo,
  pageDefinition: QaPageDefinition
): Promise<CreatedMid> {
  await openCreatePage(page, runtimeEnv, midType);
  await selectContract(page, COMMON_METADATA_CONTRACT_LABEL);
  await expect(page.locator("#apiKey"), `${midType} 등록 화면에 결제 공통 metadata API KEY 입력이 보여야 합니다.`).toBeVisible();

  const mid = makeQaMid(midType === "KEYIN" ? "K" : "I");
  const apiKey = `API-${mid}`;
  await fillTextInput(page, "#mid", mid);
  await fillTextInput(page, "#rootMid", mid);
  await fillTextInput(page, "#memo", `QA #3399 ${midType} ${mid}`);
  await fillTextInput(page, "#apiKey", apiKey);
  await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", `common-metadata-${midType.toLowerCase()}-filled`);

  const { responseJson, requestBody } = await submitCreateForm(page);
  expect(requestBody.contractId, `${midType} 등록 요청 계약은 기존 루시페이먼츠 계약이어야 합니다.`).toBe(COMMON_METADATA_CONTRACT_ID);
  expect(requestBody.midType, `${midType} 등록 요청 MID 타입이 유지되어야 합니다.`).toBe(midType);
  expect(getRequestMetadataValue(requestBody, "apiKey"), `${midType} 등록 요청 payload에 API KEY metadata가 포함되어야 합니다.`,).toBe(apiKey);

  if (!responseJson.payload) {
    throw new Error(`${midType} MID 생성 응답에 ID가 없습니다.`);
  }

  await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", `common-metadata-${midType.toLowerCase()}-saved`);
  return {
    id: responseJson.payload,
    mid,
    apiKey
  };
}

async function submitCreateForm(page: Page): Promise<{
  responseJson: PayloadResponse<string>;
  requestBody: Record<string, unknown>;
}> {
  const submitButton = page.getByRole("button", { name: "등록" }).last();
  await expect(submitButton, "PG MID 등록 버튼이 보여야 합니다.").toBeVisible({ timeout: 5_000 });
  if (!await submitButton.isEnabled()) {
    throw new Error("PG MID 등록 버튼이 비활성화되어 저장 요청이 발생하지 않았습니다.");
  }

  const requestPromise = page.waitForRequest(
    (request) => request.method() === "POST" && /\/api\/v1\/pgmids$/.test(request.url()),
    { timeout: 15_000 }
  );
  const responsePromise = page.waitForResponse(
    (response) => response.request().method() === "POST" && /\/api\/v1\/pgmids$/.test(response.url()),
    { timeout: 15_000 }
  );

  await submitButton.click({ force: true });
  const [request, response] = await Promise.all([requestPromise, responsePromise]);
  const responseJson = await parseResponseJson<PayloadResponse<string>>(response);
  expect(response.ok(), formatHttpFailure("PG MID 등록", response.status(), responseJson)).toBe(true);

  return {
    responseJson,
    requestBody: parseRequestBody(request.postData())
  };
}

async function updateSearchKeyViaUi(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  id: string,
  updatedSearchKey: string
): Promise<Record<string, unknown>> {
  if (!page.url().includes(`/business/pg-mid-manage/update/${id}`)) {
    await openUpdatePage(page, runtimeEnv, id);
  }

  await fillTextInput(page, "#searchKey", updatedSearchKey);
  await page.getByRole("button", { name: "수정" }).click({ force: true });
  const reasonInput = page.locator("#swal2-textarea");
  await expect(reasonInput, "수정 저장 전 변경사유 입력창이 표시되어야 합니다.").toBeVisible({ timeout: 5_000 });
  await reasonInput.fill("QA #3399 영수증 조회 키 수정 검증");

  const requestPromise = page.waitForRequest(
    (request) => request.method() === "PATCH" && request.url().includes(`/api/v1/pgmids/${id}`),
    { timeout: 15_000 }
  );
  const responsePromise = page.waitForResponse(
    (response) => response.request().method() === "PATCH" && response.url().includes(`/api/v1/pgmids/${id}`),
    { timeout: 15_000 }
  );
  await page.getByRole("button", { name: "확인" }).last().click({ force: true });
  const [request, response] = await Promise.all([requestPromise, responsePromise]);
  const responseJson = await parseResponseJson<PayloadResponse<unknown>>(response);
  expect(response.ok(), formatHttpFailure("PG MID 수정", response.status(), responseJson)).toBe(true);

  const requestBody = parseRequestBody(request.postData());
  expect(getRequestMetadataValue(requestBody, "searchKey"), "수정 요청 payload에 변경한 영수증 조회 키가 포함되어야 합니다.").toBe(updatedSearchKey);
  await expect(page.locator("body"), "수정 성공 안내가 표시되어야 합니다.").toContainText(/성공적으로 MID가 수정되었습니다|성공/);
  return requestBody;
}

async function selectContract(page: Page, label: string): Promise<void> {
  const control = page
    .locator("div.relative")
    .filter({ hasText: /계약을 선택하세요|선택된 계약|PG계약/ })
    .first();
  await expect(control, "PG 계약 선택 컨트롤이 보여야 합니다.").toBeVisible({ timeout: 10_000 });
  await control.click({ position: { x: 300, y: 20 } });
  await expect(page.getByText(label, { exact: false }).first(), `테스트 PG 계약 옵션이 보여야 합니다: ${label}`).toBeVisible({
    timeout: 10_000
  });
  await page.getByText(label, { exact: false }).first().click({ force: true });
  await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
  await page.waitForTimeout(500);
}

async function fillTextInput(page: Page, selector: string, value: string): Promise<void> {
  const input = page.locator(selector).first();
  await expect(input, `${selector} 입력창이 보여야 합니다.`).toBeVisible({ timeout: 5_000 });
  await input.fill(value);
}

async function loadMetadataSettings(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  providerId: string,
  midType: PgMidType
): Promise<MetadataSettingsPage> {
  const result = await callApi<PayloadResponse<MetadataSettingsPage>>(
    page,
    runtimeEnv,
    `/v1/pgproviders/${providerId}/metadata-settings?midType=${midType}`
  );
  expect(result.ok, formatApiFailure("PG provider metadata 설정 조회", result)).toBe(true);
  return result.json.payload ?? {};
}

async function loadMidDetail(page: Page, runtimeEnv: RuntimeQaEnv, id: string): Promise<PgMid> {
  const result = await callApi<PayloadResponse<PgMid>>(page, runtimeEnv, `/v1/pgmids/${id}`);
  expect(result.ok, formatApiFailure("PG MID 상세 조회", result)).toBe(true);
  if (!result.json.payload) {
    throw new Error(`PG MID 상세 응답에 payload가 없습니다: ${id}`);
  }
  return result.json.payload;
}

async function createPayletterTerminalMidViaApi(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  options: {
    searchKey: string;
    memo: string;
    terminalVendorId?: string | null;
  }
): Promise<CreatedMid> {
  const mid = makeQaMid("P");
  const result = await callApi<PayloadResponse<string>>(page, runtimeEnv, "/v1/pgmids", {
    method: "POST",
    body: {
      contractId: PAYLETTER_CONTRACT_ID,
      mid,
      midType: "TERMINAL",
      rootMid: mid,
      memo: options.memo,
      terminalVendorId: options.terminalVendorId ?? null,
      keyinMetadata: [
        {
          settingId: PAYLETTER_SEARCH_KEY_SETTING_ID,
          value: options.searchKey,
          midType: "TERMINAL"
        }
      ]
    }
  });
  expect(result.ok, formatApiFailure("페이레터 단말기 PG MID 생성", result)).toBe(true);
  if (!result.json.payload) {
    throw new Error("페이레터 단말기 PG MID 생성 응답에 ID가 없습니다.");
  }
  return {
    id: result.json.payload,
    mid,
    searchKey: options.searchKey
  };
}

async function cleanupCreatedMids(page: Page, runtimeEnv: RuntimeQaEnv, ids: string[]): Promise<void> {
  for (const id of ids) {
    await callApi<PayloadResponse<unknown>>(page, runtimeEnv, `/v1/pgmids/${id}/safe-delete`, {
      method: "DELETE"
    }).catch(() => undefined);
  }
}

async function callApi<T>(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  apiPath: string,
  options: ApiOptions = {}
): Promise<ApiResult<T>> {
  const method = options.method ?? "GET";
  const apiBase = resolveApiBase(runtimeEnv);
  let lastResult: ApiResult<T> | undefined;

  for (let attempt = 1; attempt <= 3; attempt += 1) {
    const response = await page.context().request.fetch(`${apiBase}${apiPath}`, {
      method,
      headers: {
        "Content-Type": "application/json",
        ...(runtimeEnv.tenantId ? { "X-TENANT-ID": runtimeEnv.tenantId } : {})
      },
      data: options.body === undefined ? undefined : options.body
    });
    const text = await response.text();
    let json: unknown = null;
    try {
      json = text ? JSON.parse(text) : null;
    } catch {
      json = text;
    }
    const asRecord = json && typeof json === "object" ? json as Record<string, unknown> : {};
    const result = asRecord.result && typeof asRecord.result === "object"
      ? asRecord.result as Record<string, unknown>
      : {};
    lastResult = {
      method,
      path: apiPath,
      status: response.status(),
      ok: response.ok(),
      json: json as T,
      message: String(result.message ?? asRecord.message ?? "")
    };

    if (lastResult.ok || ![502, 503, 504].includes(lastResult.status) || attempt === 3) {
      return lastResult;
    }
    await page.waitForTimeout(1_000 * attempt);
  }

  return lastResult!;
}

function resolveApiBase(runtimeEnv: RuntimeQaEnv): string {
  const baseUrl = new URL(runtimeEnv.baseUrl!);
  baseUrl.hostname = baseUrl.hostname
    .replace(/^admin-/, "api-")
    .replace(/^store-/, "api-");
  baseUrl.pathname = "/api";
  baseUrl.search = "";
  baseUrl.hash = "";
  return baseUrl.toString().replace(/\/$/, "");
}

function buildPgMidUrl(runtimeEnv: RuntimeQaEnv, pathname: string): URL {
  const url = new URL(pathname, ensureTrailingSlash(runtimeEnv.baseUrl!));
  url.searchParams.set("tenantId", runtimeEnv.tenantId!);
  return url;
}

function ensureTrailingSlash(value: string): string {
  return value.endsWith("/") ? value : `${value}/`;
}

function parseRequestBody(value: string | null): Record<string, unknown> {
  if (!value) {
    return {};
  }
  try {
    return JSON.parse(value) as Record<string, unknown>;
  } catch {
    return {};
  }
}

async function parseResponseJson<T>(response: { text: () => Promise<string> }): Promise<T> {
  const text = await response.text();
  return (text ? JSON.parse(text) : {}) as T;
}

function getRequestMetadataValue(requestBody: Record<string, unknown>, field: string): string | undefined {
  const entries = Array.isArray(requestBody.keyinMetadata) ? requestBody.keyinMetadata as MetadataValue[] : [];
  const expectedSettingId = field === "searchKey" ? PAYLETTER_SEARCH_KEY_SETTING_ID : COMMON_METADATA_SETTING_ID;
  const entry = entries.find((item) => item.field === field || item.settingId === expectedSettingId);
  return entry?.value;
}

function getMetadataValue(mid: PgMid, field: string): string | undefined {
  return (mid.keyinMetadata ?? []).find((metadata) => metadata.field === field)?.value;
}

function getOptionValue(value: OptionValue | string | undefined): string | undefined {
  if (!value) {
    return undefined;
  }
  if (typeof value === "string") {
    return value;
  }
  return value.value ?? value.code;
}

function formatApiFailure(label: string, result: ApiResult<unknown>): string {
  return `${label} 요청이 실패했습니다. status=${result.status} message=${result.message ?? "-"}`;
}

function formatHttpFailure(label: string, status: number, response: PayloadResponse<unknown>): string {
  return `${label} 요청이 실패했습니다. status=${status} message=${response.result?.message ?? response.message ?? "-"}`;
}

async function attachEvidenceLog(
  testInfo: TestInfo,
  pageDefinition: QaPageDefinition,
  role: string,
  slug: string,
  lines: string[]
): Promise<void> {
  const evidencePath = testInfo.outputPath(`${safeFilename(pageDefinition.name)}-${role}-${slug}.md`);
  fs.writeFileSync(evidencePath, `${lines.join("\n")}\n`, "utf8");
  await testInfo.attach(path.basename(evidencePath), {
    path: evidencePath,
    contentType: "text/markdown"
  });
}

async function attachPageScreenshot(
  page: Page,
  testInfo: TestInfo,
  pageDefinition: QaPageDefinition,
  role: string,
  slug: string
): Promise<void> {
  const screenshotPath = testInfo.outputPath(`${safeFilename(pageDefinition.name)}-${role}-${slug}.png`);
  await page.screenshot({ path: screenshotPath, fullPage: true });
  await testInfo.attach(`${safeFilename(pageDefinition.name)}-${role}-${slug}.png`, {
    path: screenshotPath,
    contentType: "image/png"
  });
}

function makeQaMid(prefix: string): string {
  return `QA3399${prefix}${Date.now().toString(36).toUpperCase()}${Math.floor(Math.random() * 1000)}`;
}

function safeFilename(value: string): string {
  return value
    .replace(/[^a-zA-Z0-9가-힣_.-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80);
}
