import { expect, test, type Browser, type Locator, 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;
const MISSING_MID_ADD_BUTTON_MESSAGE =
  "TA PG MID 관리 화면에서 MID 추가 버튼이 노출되지 않습니다. 단말기 생성 모달 기준 QA를 진행하려면 TA 계정에 MID 추가 버튼/하위 드롭다운 권한이 필요합니다.";
const MISSING_TERMINAL_DROPDOWN_OPTION_MESSAGE =
  "TA PG MID 관리 화면의 MID 추가 하위 드롭다운에서 단말기 항목이 노출되지 않습니다.";

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}`, () => {
    const role = "TA";
    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[0], async ({ browser }, testInfo) => {
          test.setTimeout(60_000);

          await withRolePage(browser, runtimeEnv, async (page) => {
            try {
              await openMidAddDropdown(page, runtimeEnv);
            } catch (error) {
              if (isMissingMidAddPermissionError(error)) {
                await attachEvidenceLog(testInfo, pageDefinition, role, "mid-add-dropdown-missing", [
                  error instanceof Error ? error.message : MISSING_MID_ADD_BUTTON_MESSAGE,
                  "PG MID 관리 화면 진입은 성공했지만 MID 추가 버튼 또는 하위 단말기 항목이 화면에 없습니다.",
                  `현재 URL: ${page.url()}`
                ]);
                await attachPageScreenshot(page, testInfo, pageDefinition, role, "mid-add-dropdown-missing");
              }
              throw error;
            }

            await attachEvidenceLog(testInfo, pageDefinition, role, "create-modal-open", [
              "TA 권한으로 PG MID 관리 화면에 진입했습니다.",
              "관리 화면의 MID 추가 버튼을 클릭해 하위 드롭다운을 열었습니다.",
              "드롭다운 안에 단말기 항목이 표시됩니다.",
              `현재 URL: ${page.url()}`
            ]);
            await attachPageScreenshot(page, testInfo, pageDefinition, role, "mid-add-dropdown-open");
          });
        });

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

          await withRolePage(browser, runtimeEnv, async (page) => {
            await openTerminalCreateModalFromDropdownOrBlock(page, runtimeEnv);
            await expect(page.locator("#mid"), "단말기 MID 생성 모달에 MID 입력창이 보여야 합니다.").toBeVisible();

            await attachEvidenceLog(testInfo, pageDefinition, role, "terminal-modal-open", [
              "MID 추가 하위 드롭다운에서 단말기를 선택했습니다.",
              "단말기 MID 생성 모달이 PG MID 관리 화면 위에 열렸습니다.",
              `현재 URL: ${page.url()}`
            ]);
            await attachPageScreenshot(page, testInfo, pageDefinition, role, "terminal-modal-open");
          });
        });

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

          await withRolePage(browser, runtimeEnv, async (page) => {
            await openTerminalCreateModalFromDropdownOrBlock(page, runtimeEnv);
            await selectContract(page, PAYLETTER_CONTRACT_LABEL);

            await expect(page.locator("body"), "페이레터 단말기 생성 모달에 영수증 조회 키 라벨이 보여야 합니다.").toContainText(
              /영수증\s*조회\s*키/
            );
            await expect(page.locator("#searchKey"), "페이레터 단말기 생성 모달에 영수증 조회 키 입력창이 보여야 합니다.").toBeVisible();

            await attachEvidenceLog(testInfo, pageDefinition, role, "search-key-visible", [
              "단말기 MID 생성 모달에서 PG 계약을 페이레터로 선택했습니다.",
              `PG 계약: ${PAYLETTER_CONTRACT_LABEL}`,
              "페이레터 선택 후 영수증 조회 키 입력 항목이 표시됩니다."
            ]);
            await attachPageScreenshot(page, testInfo, pageDefinition, role, "search-key-visible");
          });
        });

        test(items[3], async ({ browser }, testInfo) => {
          test.setTimeout(90_000);
          test.skip(process.env.QA_3399_ALLOW_MID_MUTATION !== "1", "QA_3399_ALLOW_MID_MUTATION=1일 때만 검증용 MID 생성/삭제를 실행합니다.");
          const createdIds: string[] = [];

          await withRolePage(browser, runtimeEnv, async (page) => {
            try {
              const created = await createPayletterTerminalMidViaModal(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-modal-create", [
                "페이레터 단말기 MID 생성 모달에서 영수증 조회 키를 입력하고 저장했습니다.",
                `생성 MID: ${created.mid}`,
                `생성 ID: ${created.id}`,
                `상세 조회 metadata: searchKey=${created.searchKey}`
              ]);
            } finally {
              await cleanupCreatedMids(page, runtimeEnv, createdIds);
            }
          });
        });

        test(items[4], async ({ browser }, testInfo) => {
          test.setTimeout(90_000);
          test.skip(process.env.QA_3399_ALLOW_MID_MUTATION !== "1", "QA_3399_ALLOW_MID_MUTATION=1일 때만 검증용 MID 생성/삭제를 실행합니다.");
          const createdIds: string[] = [];

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

              const detail = await loadMidDetail(page, runtimeEnv, created.id);
              expect(getMetadataValue(detail, "searchKey"), "상세 API에서 영수증 조회 키가 저장값으로 유지되어야 합니다.").toBe(created.searchKey);

              await openUpdateSurfaceForMid(page, runtimeEnv, created.id, created.mid);
              await expect(page.locator("#searchKey"), "저장 후 다시 연 수정 화면에 영수증 조회 키 값이 유지되어야 합니다.").toHaveValue(
                created.searchKey!
              );

              await attachEvidenceLog(testInfo, pageDefinition, role, "payletter-terminal-modal-persist", [
                "페이레터 단말기 MID 저장 후 상세 API와 수정 화면에서 영수증 조회 키 유지 여부를 확인했습니다.",
                `MID: ${created.mid}`,
                `저장된 영수증 조회 키: ${created.searchKey}`,
                `상세 조회 metadata: searchKey=${getMetadataValue(detail, "searchKey") ?? "-"}`
              ]);
              await attachPageScreenshot(page, testInfo, pageDefinition, role, "payletter-terminal-modal-persist");
            } finally {
              await cleanupCreatedMids(page, runtimeEnv, 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 openManagementPage(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  const url = buildPgMidUrl(runtimeEnv, "/business/pg-mid-manage");
  await openAuthenticatedUrl(page, url.toString(), runtimeEnv, "PG MID 관리");
  await expect(page.locator("body"), "PG MID 관리 화면이 표시되어야 합니다.").toContainText(/PG\s*MID\s*관리|TID\/MID관리/);
}

async function openMidAddDropdown(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  await openManagementPage(page, runtimeEnv);
  await clickFirstVisible(
    [
      page.getByRole("button", { name: /MID\s*추가/ }).first(),
      page.locator("button").filter({ hasText: /MID\s*추가/ }).first()
    ],
    MISSING_MID_ADD_BUTTON_MESSAGE
  );
  await page.waitForTimeout(300);

  if (!await findVisibleTerminalDropdownOption(page)) {
    throw new Error(MISSING_TERMINAL_DROPDOWN_OPTION_MESSAGE);
  }
}

async function openMidAddDropdownOrBlock(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  try {
    await openMidAddDropdown(page, runtimeEnv);
  } catch (error) {
    if (isMissingMidAddPermissionError(error)) {
      test.skip(true, `BLOCKED: ${error instanceof Error ? error.message : MISSING_MID_ADD_BUTTON_MESSAGE}`);
    }
    throw error;
  }
}

async function openTerminalCreateModalFromDropdown(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  await openMidAddDropdown(page, runtimeEnv);
  await clickTerminalOptionFromMidAddDropdown(page);
  await waitForNavigationToSettle(page);

  expect(page.url(), "단말기 MID 생성은 관리 화면의 생성 모달로 열려야 하며 기존 create 경로로 이동하면 안 됩니다.").not.toContain(
    "/business/pg-mid-manage/create"
  );
  expect(page.url(), "단말기 MID 생성 모달을 연 뒤에도 PG MID 관리 화면 URL을 유지해야 합니다.").toContain("/business/pg-mid-manage");

  await expect(page.locator("body"), "단말기 MID 생성 모달 제목 또는 등록 영역이 표시되어야 합니다.").toContainText(
    /PG\s*MID\s*(생성|등록)|MID\s*(생성|등록)/
  );
}

async function openTerminalCreateModalFromDropdownOrBlock(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  try {
    await openTerminalCreateModalFromDropdown(page, runtimeEnv);
  } catch (error) {
    if (isMissingMidAddPermissionError(error)) {
      test.skip(true, `BLOCKED: ${error instanceof Error ? error.message : MISSING_MID_ADD_BUTTON_MESSAGE}`);
    }
    throw error;
  }
}

async function clickTerminalOptionFromMidAddDropdown(page: Page): Promise<void> {
  const terminalOption = await findVisibleTerminalDropdownOption(page);
  if (!terminalOption) {
    throw new Error(MISSING_TERMINAL_DROPDOWN_OPTION_MESSAGE);
  }
  await terminalOption.click({ force: true });
}

async function findVisibleTerminalDropdownOption(page: Page): Promise<Locator | undefined> {
  const candidates = page.locator("li, [role='menuitem'], [role='option'], button, span").filter({ hasText: /^\s*단말기\s*$/ });
  const count = await candidates.count();

  for (let index = count - 1; index >= 0; index -= 1) {
    const candidate = candidates.nth(index);
    if (!await candidate.isVisible({ timeout: 500 }).catch(() => false)) {
      continue;
    }
    const box = await candidate.boundingBox().catch(() => null);
    if (box && isMidAddDropdownRegion(box)) {
      return candidate;
    }
  }

  return undefined;
}

function isMidAddDropdownRegion(box: { x: number; y: number; width: number; height: number }): boolean {
  return box.x >= 1_000 && box.y >= 250 && box.y <= 460 && box.width > 0 && box.height > 0;
}

function isMissingMidAddPermissionError(error: unknown): boolean {
  return (
    error instanceof Error &&
    (error.message.includes(MISSING_MID_ADD_BUTTON_MESSAGE) || error.message.includes(MISSING_TERMINAL_DROPDOWN_OPTION_MESSAGE))
  );
}

async function openCreateModalFromManagementPage(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  await openManagementPage(page, runtimeEnv);
  await clickFirstVisible(
    [
      page.getByRole("button", { name: /MID\s*생성|PG\s*MID\s*생성/ }).first(),
      page.getByRole("button", { name: /생성|등록/ }).filter({ hasText: /MID|PG/ }).first(),
      page.locator("button").filter({ hasText: /MID\s*생성|PG\s*MID\s*생성/ }).first()
    ],
    MISSING_MID_ADD_BUTTON_MESSAGE
  );
  await waitForNavigationToSettle(page);

  expect(page.url(), "MID 생성은 관리 화면의 생성 모달로 열려야 하며 기존 create 경로로 이동하면 안 됩니다.").not.toContain(
    "/business/pg-mid-manage/create"
  );
  expect(page.url(), "MID 생성 모달을 연 뒤에도 PG MID 관리 화면 URL을 유지해야 합니다.").toContain("/business/pg-mid-manage");

  await expect(page.locator("body"), "MID 생성 모달 제목 또는 등록 영역이 표시되어야 합니다.").toContainText(
    /PG\s*MID\s*(생성|등록)|MID\s*(생성|등록)/
  );
}

async function openCreateModalFromManagementPageOrBlock(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  try {
    await openCreateModalFromManagementPage(page, runtimeEnv);
  } catch (error) {
    if (isMissingCreateButtonError(error)) {
      test.skip(true, `BLOCKED: ${MISSING_MID_ADD_BUTTON_MESSAGE}`);
    }
    throw error;
  }
}

function isMissingCreateButtonError(error: unknown): boolean {
  return error instanceof Error && error.message.includes(MISSING_MID_ADD_BUTTON_MESSAGE);
}

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\s*MID\s*(관리\s*)?수정/);
}

async function openUpdateSurfaceForMid(page: Page, runtimeEnv: RuntimeQaEnv, id: string, mid: string): Promise<void> {
  await openManagementPage(page, runtimeEnv);
  await searchMidOnManagementPage(page, mid).catch(() => undefined);

  const midNode = page.getByText(mid, { exact: false }).first();
  const row = page
    .locator(`xpath=//*[contains(normalize-space(), '${mid}')]/ancestor::*[self::tr or contains(@class, 'row') or contains(@class, 'table') or contains(@class, 'Table')][1]`)
    .first();

  if (await midNode.isVisible({ timeout: 5_000 }).catch(() => false)) {
    const updateButton = row.getByRole("button", { name: /수정|상세|보기/ }).last();
    if (await updateButton.isVisible({ timeout: 2_000 }).catch(() => false)) {
      await updateButton.click({ force: true });
      await waitForNavigationToSettle(page);
      await expect(page.locator("#searchKey"), "수정 화면 또는 수정 모달에 영수증 조회 키 입력창이 보여야 합니다.").toBeVisible({
        timeout: 10_000
      });
      return;
    }
  }

  await openUpdatePage(page, runtimeEnv, id);
}

async function searchMidOnManagementPage(page: Page, mid: string): Promise<void> {
  const input = page.locator("input[placeholder*='MID'], input[name='keyword'], input[name='searchKeyword']").first();
  if (await input.isVisible({ timeout: 3_000 }).catch(() => false)) {
    await input.fill(mid);
  }

  const searchButton = page.getByRole("button", { name: /검색|조회/ }).first();
  if (await searchButton.isVisible({ timeout: 2_000 }).catch(() => false)) {
    await searchButton.click({ force: true });
  } else {
    await page.keyboard.press("Enter");
  }
  await waitForNavigationToSettle(page);
}

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 createPayletterTerminalMidViaModal(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  role: string,
  testInfo: TestInfo,
  pageDefinition: QaPageDefinition
): Promise<CreatedMid> {
  await openTerminalCreateModalFromDropdownOrBlock(page, runtimeEnv);
  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} modal ${mid}`);
  await fillTextInput(page, "#searchKey", searchKey);
  await attachPageScreenshot(page, testInfo, pageDefinition, role, "payletter-terminal-modal-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가 없습니다.");
  }

  await attachPageScreenshot(page, testInfo, pageDefinition, role, "payletter-terminal-modal-saved");
  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 selectTerminalMidType(page: Page): Promise<void> {
  await clickFirstVisible(
    [
      page.getByRole("button", { name: /단말기/ }).first(),
      page.locator("button").filter({ hasText: /단말기/ }).first(),
      page.getByText("단말기", { exact: false }).first()
    ],
    "MID 유형 단말기 선택 항목"
  );
  await page.waitForTimeout(300);
}

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 clickFirstVisible(candidates: Locator[], description: string): Promise<void> {
  for (const candidate of candidates) {
    if (await candidate.isVisible({ timeout: 3_000 }).catch(() => false)) {
      await candidate.click({ force: true });
      return;
    }
  }
  throw new Error(description.endsWith("다.") ? description : `${description}을 찾을 수 없습니다.`);
}

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);
}
