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

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

interface AccountLookupConfig {
  validBankName?: string;
  validAccountNumber?: string;
  expectedHolderName?: string;
  invalidBankName?: string;
  invalidAccountNumber?: string;
  linkShopUrl?: string;
  cubeLinkShopUrl?: string;
}

interface LookupAttempt {
  pageName: string;
  role: string;
  bankSelected: boolean;
  accountFilled: boolean;
  lookupClicked: boolean;
  responseStatus?: number;
  responseUrl?: string;
  holderName?: string;
  failureMessage?: string;
  bodySample: string;
}

const STORE_ROLE = "VE";
const ADMIN_ROLES = ["ADMIN", "TA"];
const SUCCESS_TEXT_PATTERN = /예금주|정상|확인되었습니다|조회되었습니다|성공/i;
const FAILURE_TEXT_PATTERN = /실패|오류|유효하지|잘못|확인할 수|조회할 수|존재하지|필수|불가|제한/i;
const NEXT_BUTTON_PATTERN = /다음|진행|저장|이체|등록|수정|결제/i;

export function runAccountHolderLookupSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const config = getAccountLookupConfig();

  const storeEnv = getRuntimeQaEnv(options.environment, "store", STORE_ROLE);
  const adminEnvs = ADMIN_ROLES.map((role) => getRuntimeQaEnv(options.environment, "admin", role));
  const storePages = checklist.pages.filter((page) => (page.application ?? "admin") === "store");
  const adminPages = checklist.pages.filter((page) => (page.application ?? "admin") === "admin");

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    test.describe("[VE] role", () => {
      test(checklist.checklist[0] ?? "스토어 예금주 조회 성공 확인", async ({ browser }, testInfo) => {
        requireValidLookupConfig(config);
        await withRolePage(browser, storeEnv, async (page) => {
          const targetPages = storePages.filter((item) => !item.name.includes("링크샵"));
          if (targetPages.length === 0) {
            blocked("스토어 송금 예금주 확인 대상 화면이 checklist.json에 없습니다.");
          }

          const attempts: LookupAttempt[] = [];
          for (const pageDefinition of targetPages) {
            const attempt = await verifyLookupSuccess(page, storeEnv, pageDefinition, config);
            attempts.push(attempt);
          }

          await attachEvidenceLog(testInfo, "스토어 예금주 조회", STORE_ROLE, "store-success", formatAttempts(attempts));
          await attachPageScreenshot(page, testInfo, "스토어 예금주 조회", STORE_ROLE, "store-success");
        });
      });

      test(checklist.checklist[1] ?? "링크샵 예금주 조회 후 다음 단계 진행 확인", async ({ browser }, testInfo) => {
        requireValidLookupConfig(config);
        const linkTargets = buildLinkShopTargets(storePages, config);
        if (linkTargets.length === 0) {
          blocked("링크샵/큐브 링크샵 예금주 조회 화면 URL이 필요합니다. QA_3154_LINK_SHOP_URL 또는 QA_3154_CUBE_LINK_SHOP_URL을 .env.local에 설정하세요.");
        }

        await withRolePage(browser, storeEnv, async (page) => {
          const attempts: LookupAttempt[] = [];
          for (const target of linkTargets) {
            const attempt = await verifyLookupSuccess(page, storeEnv, target.page, config, target.url);
            const nextButtonState = await inspectNextButtonState(page);
            attempts.push({
              ...attempt,
              failureMessage: attempt.failureMessage ?? `다음/진행 버튼 상태: ${nextButtonState}`
            });
            expect(nextButtonState, `${target.page.name}에서 예금주 조회 성공 후 다음 단계 버튼이 진행 가능해야 합니다.`).not.toBe("disabled");
          }

          await attachEvidenceLog(testInfo, "링크샵 예금주 조회", STORE_ROLE, "link-shop-next", formatAttempts(attempts));
          await attachPageScreenshot(page, testInfo, "링크샵 예금주 조회", STORE_ROLE, "link-shop-next");
        });
      });
    });

    for (const runtimeEnv of adminEnvs) {
      const role = runtimeEnv.role;

      test.describe(`[${role}] role`, () => {
        test(checklist.checklist[2] ?? "전산 계좌 관련 예금주 조회 성공 확인", async ({ browser }, testInfo) => {
          requireValidLookupConfig(config);
          await withRolePage(browser, runtimeEnv, async (page) => {
            const attempts = await verifyPageGroupLookup(page, runtimeEnv, accountLookupPages(adminPages), config);
            await attachEvidenceLog(testInfo, "전산 계좌 예금주 조회", role, "admin-account-success", formatAttempts(attempts));
            await attachPageScreenshot(page, testInfo, "전산 계좌 예금주 조회", role, "admin-account-success");
          });
        });

        test(checklist.checklist[3] ?? "이체하기 수취 계좌 예금주 확인", async ({ browser }, testInfo) => {
          requireValidLookupConfig(config);
          await withRolePage(browser, runtimeEnv, async (page) => {
            const transferPages = adminPages.filter((item) => item.name.includes("이체하기"));
            const attempts = await verifyPageGroupLookup(page, runtimeEnv, transferPages, config);
            const nextButtonState = await inspectNextButtonState(page);
            await attachEvidenceLog(testInfo, "이체하기 예금주 확인", role, "transfer-holder", [
              ...formatAttempts(attempts),
              `이체 진행 버튼 상태: ${nextButtonState}`
            ]);
            await attachPageScreenshot(page, testInfo, "이체하기 예금주 확인", role, "transfer-holder");
            expect(nextButtonState, "예금주 확인 후 이체 진행 버튼이 접근 가능해야 합니다.").not.toBe("missing");
          });
        });

        test(checklist.checklist[4] ?? "업체 등록/수정 화면 예금주 조회와 저장 흐름 확인", async ({ browser }, testInfo) => {
          requireValidLookupConfig(config);
          await withRolePage(browser, runtimeEnv, async (page) => {
            const attempts = await verifyPageGroupLookup(page, runtimeEnv, businessAccountPages(adminPages), config);
            const nextButtonState = await inspectNextButtonState(page);
            await attachEvidenceLog(testInfo, "업체 등록 수정 예금주 조회", role, "business-account-holder", [
              ...formatAttempts(attempts),
              `저장/등록 버튼 상태: ${nextButtonState}`
            ]);
            await attachPageScreenshot(page, testInfo, "업체 등록 수정 예금주 조회", role, "business-account-holder");
            expect(nextButtonState, "예금주 조회 후 저장/등록 흐름을 확인할 수 있는 버튼이 있어야 합니다.").not.toBe("missing");
          });
        });
      });
    }

    test(checklist.checklist[5] ?? "잘못된 계좌번호 실패 안내와 진행 제한 확인", async ({ browser }, testInfo) => {
      requireInvalidLookupConfig(config);
      await withRolePage(browser, storeEnv, async (page) => {
        const targetPage = storePages.find((item) => !item.name.includes("링크샵")) ?? storePages[0];
        if (!targetPage) {
          blocked("실패 안내 검증에 사용할 스토어 대상 화면이 없습니다.");
        }

        const attempt = await verifyLookupFailure(page, storeEnv, targetPage, config);
        const nextButtonState = await inspectNextButtonState(page);
        await attachEvidenceLog(testInfo, "예금주 조회 실패 안내", STORE_ROLE, "invalid-account", [
          ...formatAttempts([attempt]),
          `저장/다음 버튼 상태: ${nextButtonState}`
        ]);
        await attachPageScreenshot(page, testInfo, "예금주 조회 실패 안내", STORE_ROLE, "invalid-account");
        expect(nextButtonState, "실패 조회 후 저장 또는 다음 단계 진행은 제한되어야 합니다.").not.toBe("enabled");
      });
    });

    test(checklist.checklist[6] ?? "계좌번호 변경 후 재조회 시 이전 결과 초기화 확인", async ({ browser }, testInfo) => {
      requireValidLookupConfig(config);
      requireInvalidLookupConfig(config);
      const runtimeEnv = adminEnvs[0];
      await withRolePage(browser, runtimeEnv, async (page) => {
        const targetPage = accountLookupPages(adminPages)[0] ?? adminPages[0];
        if (!targetPage) {
          blocked("재조회 검증에 사용할 전산 계좌 화면이 없습니다.");
        }

        const success = await verifyLookupSuccess(page, runtimeEnv, targetPage, config);
        const firstHolder = success.holderName;
        const failure = await verifyLookupFailure(page, runtimeEnv, targetPage, config, { keepPage: true });
        const bodyAfterFailure = await visibleBodyText(page);
        if (firstHolder) {
          expect(bodyAfterFailure, "계좌번호 변경 후 실패 재조회 시 이전 예금주명이 남아 있으면 안 됩니다.").not.toContain(firstHolder);
        }

        await attachEvidenceLog(testInfo, "예금주 재조회 초기화", runtimeEnv.role, "repeat-relookup", [
          "1차 정상 조회 후 잘못된 계좌번호로 재조회",
          ...formatAttempts([success, failure])
        ]);
        await attachPageScreenshot(page, testInfo, "예금주 재조회 초기화", runtimeEnv.role, "repeat-relookup");
      });
    });

    test(checklist.checklist[7] ?? "권한별 접근 가능한 화면과 예금주 조회 버튼 노출 확인", async ({ browser }, testInfo) => {
      test.setTimeout(90_000);
      const evidence: string[] = [];

      await withRolePage(browser, storeEnv, async (page) => {
        const attempts = await inspectRolePages(page, storeEnv, storePages);
        evidence.push(...formatAttempts(attempts));
        await attachPageScreenshot(page, testInfo, "권한별 접근 확인", STORE_ROLE, "store-access");
      });

      for (const runtimeEnv of adminEnvs) {
        await withRolePage(browser, runtimeEnv, async (page) => {
          const attempts = await inspectRolePages(page, runtimeEnv, adminPages);
          evidence.push(...formatAttempts(attempts));
          await attachPageScreenshot(page, testInfo, "권한별 접근 확인", runtimeEnv.role, "admin-access");
        });
      }

      await attachEvidenceLog(testInfo, "권한별 접근 확인", "ALL", "role-access", evidence);
      expect(evidence.length, "권한별 화면 접근 증거가 있어야 합니다.").toBeGreaterThan(0);
      const unresolved = evidence.filter((line) => /접근이 제한|화면 접근|404|찾을 수 없습니다|버튼=미확인/.test(line));
      if (unresolved.length > 0) {
        blocked(`권한별 접근/버튼 확인이 끝나지 않은 화면이 있습니다: ${unresolved.slice(0, 5).join(" | ")}`);
      }
    });
  });
}

async function verifyPageGroupLookup(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  pageDefinitions: QaPageDefinition[],
  config: AccountLookupConfig
): Promise<LookupAttempt[]> {
  if (pageDefinitions.length === 0) {
    blocked(`${runtimeEnv.role} 계정으로 확인할 대상 화면이 checklist.json에 없습니다.`);
  }

  const attempts: LookupAttempt[] = [];
  for (const pageDefinition of pageDefinitions) {
    attempts.push(await verifyLookupSuccess(page, runtimeEnv, pageDefinition, config));
  }
  return attempts;
}

async function verifyLookupSuccess(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  config: AccountLookupConfig,
  overrideUrl?: string
): Promise<LookupAttempt> {
  await openTargetPage(page, runtimeEnv, pageDefinition, overrideUrl);
  await openEntryFormIfNeeded(page);

  const attempt = await performLookup(page, runtimeEnv, pageDefinition, {
    bankName: config.validBankName,
    accountNumber: config.validAccountNumber
  });

  const bodyText = await visibleBodyText(page);
  const holderName = config.expectedHolderName ?? attempt.holderName;
  if (!holderName) {
    blocked(`${pageDefinition.name} 예금주 조회 응답 또는 화면에서 예금주명을 확인하지 못했습니다.`);
  }

  expect(
    bodyText.includes(holderName) || (attempt.holderName ? holderName.includes(attempt.holderName) : false),
    `${pageDefinition.name} 화면에 예금주명이 표시되어야 합니다.`
  ).toBe(true);

  expect(
    attempt.responseStatus === undefined || attempt.responseStatus < 400,
    `${pageDefinition.name} 예금주 조회 응답은 성공이어야 합니다.`
  ).toBe(true);

  return {
    ...attempt,
    holderName,
    bodySample: sampleText(bodyText)
  };
}

async function verifyLookupFailure(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  config: AccountLookupConfig,
  options: { keepPage?: boolean } = {}
): Promise<LookupAttempt> {
  if (!options.keepPage) {
    await openTargetPage(page, runtimeEnv, pageDefinition);
    await openEntryFormIfNeeded(page);
  }

  const attempt = await performLookup(page, runtimeEnv, pageDefinition, {
    bankName: config.invalidBankName ?? config.validBankName,
    accountNumber: config.invalidAccountNumber
  });
  const bodyText = await visibleBodyText(page);
  const hasFailureMessage = FAILURE_TEXT_PATTERN.test(bodyText) || !!attempt.failureMessage || (attempt.responseStatus ?? 200) >= 400;

  expect(hasFailureMessage, `${pageDefinition.name} 잘못된 계좌 조회 시 실패 안내가 보여야 합니다.`).toBe(true);

  return {
    ...attempt,
    failureMessage: attempt.failureMessage ?? findFailureLine(bodyText),
    bodySample: sampleText(bodyText)
  };
}

async function inspectRolePages(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  pageDefinitions: QaPageDefinition[]
): Promise<LookupAttempt[]> {
  const attempts: LookupAttempt[] = [];
  for (const pageDefinition of pageDefinitions) {
    if (isUnconfirmedRoute(pageDefinition)) {
      attempts.push({
        pageName: pageDefinition.name,
        role: runtimeEnv.role,
        bankSelected: false,
        accountFilled: false,
        lookupClicked: false,
        bodySample: "자동 경로 후보 미확정",
        failureMessage: "BLOCKED: route/selector 확정 필요"
      });
      continue;
    }

    try {
      await openTargetPage(page, runtimeEnv, pageDefinition);
      await openEntryFormIfNeeded(page);
      const bodyText = await visibleBodyText(page);
      const lookupButton = await findLookupButton(page, pageDefinition);
      const accountInput = await findAccountInput(page);
      attempts.push({
        pageName: pageDefinition.name,
        role: runtimeEnv.role,
        bankSelected: await hasBankControl(page),
        accountFilled: await accountInput.count().then((count) => count > 0),
        lookupClicked: await lookupButton.count().then((count) => count > 0),
        bodySample: sampleText(bodyText),
        failureMessage: accessDeniedText(bodyText)
      });
    } catch (error) {
      attempts.push({
        pageName: pageDefinition.name,
        role: runtimeEnv.role,
        bankSelected: false,
        accountFilled: false,
        lookupClicked: false,
        bodySample: sampleText(await visibleBodyText(page)),
        failureMessage: error instanceof Error ? error.message : String(error)
      });
    }
  }
  return attempts;
}

function isUnconfirmedRoute(pageDefinition: QaPageDefinition): boolean {
  return /계좌 관리|통합 계좌 관리|모계좌관리|시스템 지급계좌 관리|법인계좌관리V2|영업점관리/.test(pageDefinition.name);
}

async function performLookup(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  input: { bankName?: string; accountNumber?: string }
): Promise<LookupAttempt> {
  const bankSelected = input.bankName ? await selectBank(page, input.bankName) : false;
  const accountInput = await findAccountInput(page);
  if ((await accountInput.count()) === 0) {
    blocked(`${pageDefinition.name} 화면에서 계좌번호 입력 영역을 찾지 못했습니다.`);
  }

  await accountInput.first().fill(input.accountNumber ?? "");
  const lookupButton = await findLookupButton(page, pageDefinition);
  if ((await lookupButton.count()) === 0) {
    blocked(`${pageDefinition.name} 화면에서 예금주 조회 버튼을 찾지 못했습니다.`);
  }

  const responsePromise = waitForLookupResponse(page, pageDefinition);
  await lookupButton.first().click();
  const response = await responsePromise;
  await page.waitForTimeout(800);

  const bodyText = await visibleBodyText(page);
  const responseJson = response ? await response.json().catch(() => undefined) : undefined;
  const holderName = findHolderName(responseJson) ?? findHolderNameFromText(bodyText);
  const failureMessage = findFailureMessage(responseJson) ?? findFailureLine(bodyText);

  return {
    pageName: pageDefinition.name,
    role: runtimeEnv.role,
    bankSelected,
    accountFilled: true,
    lookupClicked: true,
    responseStatus: response?.status(),
    responseUrl: response ? sanitizeUrl(response.url()) : undefined,
    holderName,
    failureMessage,
    bodySample: sampleText(bodyText)
  };
}

async function openTargetPage(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  overrideUrl?: string
): Promise<void> {
  if (!runtimeEnv.baseUrl) {
    blocked(`${runtimeEnv.application}/${runtimeEnv.role} baseUrl 설정이 필요합니다.`);
  }

  const targetUrl = overrideUrl
    ? new URL(overrideUrl).toString()
    : buildPageUrl(runtimeEnv.baseUrl, pageDefinition, runtimeEnv.tenantId ?? "");

  if (!runtimeEnv.storageState) {
    await loginWithCredentials(page, runtimeEnv);
  }

  await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
  await waitForNavigationToSettle(page);

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

  const bodyText = await visibleBodyText(page);
  const denied = accessDeniedText(bodyText);
  if (denied) {
    blocked(`${runtimeEnv.role} 계정으로 ${pageDefinition.name} 화면 접근이 제한되었습니다: ${denied}`);
  }
}

async function openEntryFormIfNeeded(page: Page): Promise<void> {
  if ((await (await findAccountInput(page)).count()) > 0 && (await (await findLookupButton(page)).count()) > 0) {
    return;
  }

  const formButton = page
    .locator("button:visible, [role='button']:visible")
    .filter({ hasText: /등록|추가|생성|수정|상세|관리|계좌/i })
    .first();
  if ((await formButton.count()) === 0) {
    return;
  }

  await formButton.click().catch(() => undefined);
  await page.waitForTimeout(700);
}

async function findAccountInput(page: Page): Promise<Locator> {
  const inputs = page.locator("input:visible");
  const count = await inputs.count();
  for (let index = 0; index < count; index += 1) {
    const input = inputs.nth(index);
    const meta = await input.evaluate((element) => {
      const inputElement = element as HTMLInputElement;
      const label = inputElement.labels ? Array.from(inputElement.labels).map((item) => item.textContent ?? "").join(" ") : "";
      return [
        inputElement.name,
        inputElement.id,
        inputElement.placeholder,
        inputElement.getAttribute("aria-label"),
        label
      ].join(" ");
    });
    if (/계좌|account|acct/i.test(meta) && !/예금주|holder|name|bank/i.test(meta)) {
      const disabled = await input.isDisabled().catch(() => true);
      if (!disabled) {
        return input;
      }
    }
  }
  return page.locator("input:visible").filter({ hasNotText: /예금주/ }).first();
}

async function findLookupButton(page: Page, pageDefinition?: QaPageDefinition): Promise<Locator> {
  if (pageDefinition?.searchButtonSelector) {
    const configured = page.locator(pageDefinition.searchButtonSelector).filter({ hasText: /예금주|계좌|확인|조회|수취/ }).first();
    if ((await configured.count()) > 0) {
      return configured;
    }
  }

  return page
    .locator("button:visible, [role='button']:visible")
    .filter({ hasText: /예금주|계좌\s*조회|계좌조회|수취.*확인|확인/i })
    .first();
}

async function selectBank(page: Page, bankName: string): Promise<boolean> {
  const selectElements = page.locator("select:visible");
  for (let index = 0; index < await selectElements.count(); index += 1) {
    const select = selectElements.nth(index);
    const optionLabel = await select
      .locator("option")
      .evaluateAll((options, expected) => {
        const normalizedExpected = String(expected).replace(/\s+/g, "");
        const match = options
          .map((option) => (option.textContent ?? "").trim())
          .find((label) => label.replace(/\s+/g, "").includes(normalizedExpected));
        return match ?? "";
      }, bankName)
      .catch(() => "");
    if (!optionLabel) {
      continue;
    }
    const matched = await select
      .selectOption({ label: optionLabel })
      .then(() => true)
      .catch(() => false);
    if (matched) {
      return true;
    }
  }

  const bankControls = page
    .locator("[role='combobox']:visible, button:visible, [aria-haspopup='listbox']:visible")
    .filter({ hasText: /은행|선택|bank/i });
  for (let index = 0; index < await bankControls.count(); index += 1) {
    const control = bankControls.nth(index);
    await control.click().catch(() => undefined);
    const option = page.getByText(new RegExp(escapeRegExp(bankName))).last();
    if ((await option.count()) > 0) {
      await option.click().catch(() => undefined);
      return true;
    }
  }

  return false;
}

async function hasBankControl(page: Page): Promise<boolean> {
  if ((await page.locator("select:visible").count()) > 0) {
    return true;
  }
  return (await page.locator("[role='combobox']:visible, button:visible").filter({ hasText: /은행|bank/i }).count()) > 0;
}

async function waitForLookupResponse(page: Page, pageDefinition: QaPageDefinition) {
  const pattern = new RegExp(pageDefinition.dataRequestUrlPattern ?? "account|holder|owner|depositor|hecto|dozn|bank", "i");
  return page
    .waitForResponse((response) => {
      const request = response.request();
      return request.method() !== "GET" && pattern.test(response.url());
    }, { timeout: 8_000 })
    .catch(() => undefined);
}

async function inspectNextButtonState(page: Page): Promise<"enabled" | "disabled" | "missing"> {
  const button = page.locator("button:visible, [role='button']:visible").filter({ hasText: NEXT_BUTTON_PATTERN }).last();
  if ((await button.count()) === 0) {
    return "missing";
  }
  return (await button.isDisabled().catch(() => false)) ? "disabled" : "enabled";
}

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

function accountLookupPages(pages: QaPageDefinition[]): QaPageDefinition[] {
  return pages.filter((page) => /계좌조회|법인계좌|지급계좌|통합 계좌|모계좌|시스템 지급계좌|블랙리스트/.test(page.name));
}

function businessAccountPages(pages: QaPageDefinition[]): QaPageDefinition[] {
  return pages.filter((page) => /법인관리|영업점관리|가맹점관리/.test(page.name));
}

function buildLinkShopTargets(
  storePages: QaPageDefinition[],
  config: AccountLookupConfig
): Array<{ page: QaPageDefinition; url?: string }> {
  const linkPages = storePages.filter((item) => item.name.includes("링크샵"));
  return linkPages
    .map((page) => ({
      page,
      url: page.name.includes("큐브") ? config.cubeLinkShopUrl : config.linkShopUrl
    }))
    .filter((item) => item.url);
}

function getAccountLookupConfig(): AccountLookupConfig {
  return {
    validBankName: process.env.QA_3154_VALID_BANK_NAME,
    validAccountNumber: process.env.QA_3154_VALID_ACCOUNT_NUMBER,
    expectedHolderName: process.env.QA_3154_EXPECTED_HOLDER_NAME,
    invalidBankName: process.env.QA_3154_INVALID_BANK_NAME,
    invalidAccountNumber: process.env.QA_3154_INVALID_ACCOUNT_NUMBER,
    linkShopUrl: process.env.QA_3154_LINK_SHOP_URL,
    cubeLinkShopUrl: process.env.QA_3154_CUBE_LINK_SHOP_URL
  };
}

function requireValidLookupConfig(config: AccountLookupConfig): void {
  const missing = [
    ["QA_3154_VALID_BANK_NAME", config.validBankName],
    ["QA_3154_VALID_ACCOUNT_NUMBER", config.validAccountNumber]
  ].filter(([, value]) => !value);
  if (missing.length > 0) {
    blocked(`정상 예금주 조회 테스트 데이터가 필요합니다: ${missing.map(([key]) => key).join(", ")}`);
  }
}

function requireInvalidLookupConfig(config: AccountLookupConfig): void {
  if (!config.invalidAccountNumber) {
    blocked("실패 예금주 조회 테스트 데이터가 필요합니다: QA_3154_INVALID_ACCOUNT_NUMBER");
  }
}

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

async function visibleBodyText(page: Page): Promise<string> {
  return (await page.locator("body").innerText({ timeout: 5_000 }).catch(() => "")).replace(/\s+/g, " ").trim();
}

function findHolderName(value: unknown): string | undefined {
  return findStringByKey(value, /(account)?holder|owner|depositor|depositorName|accountName|holderName|예금주/i);
}

function findFailureMessage(value: unknown): string | undefined {
  return findStringByKey(value, /message|error|resultMessage|reason/i);
}

function findStringByKey(value: unknown, pattern: RegExp): string | undefined {
  if (!value || typeof value !== "object") {
    return undefined;
  }
  for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
    if (pattern.test(key) && typeof item === "string" && item.trim()) {
      return item.trim();
    }
    const nested = findStringByKey(item, pattern);
    if (nested) {
      return nested;
    }
  }
  return undefined;
}

function findHolderNameFromText(text: string): string | undefined {
  const match = text.match(/예금주\s*[:：]?\s*([가-힣A-Za-z0-9()._-]{2,30})/);
  return match?.[1];
}

function findFailureLine(text: string): string | undefined {
  return text
    .split(/(?<=[.!?。])\s+|\n+/)
    .map((line) => line.trim())
    .find((line) => FAILURE_TEXT_PATTERN.test(line));
}

function accessDeniedText(text: string): string | undefined {
  if (/권한이 없습니다|접근.*제한|접근.*불가|Not Found|404|Forbidden|로그인/.test(text)) {
    return sampleText(text);
  }
  return undefined;
}

function formatAttempts(attempts: LookupAttempt[]): string[] {
  return attempts.map((attempt, index) =>
    [
      `${index + 1}. ${attempt.role} / ${attempt.pageName}`,
      `은행 선택=${attempt.bankSelected ? "성공" : "미확인"}`,
      `계좌 입력=${attempt.accountFilled ? "성공" : "미확인"}`,
      `조회 버튼=${attempt.lookupClicked ? "확인" : "미확인"}`,
      `응답=${attempt.responseStatus ?? "-"} ${attempt.responseUrl ?? ""}`.trim(),
      `예금주=${attempt.holderName ? maskHolder(attempt.holderName) : "-"}`,
      `안내=${attempt.failureMessage ?? "-"}`,
      `화면 일부=${attempt.bodySample}`
    ].join(" / ")
  );
}

async function attachEvidenceLog(
  testInfo: TestInfo,
  pageName: string,
  role: string,
  suffix: string,
  lines: string[]
): Promise<void> {
  await testInfo.attach(`${safeFilename(pageName)}-${role}-${suffix}.md`, {
    body: `${lines.join("\n")}\n`,
    contentType: "text/markdown"
  });
}

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

function sanitizeUrl(rawUrl: string): string {
  const url = new URL(rawUrl);
  for (const key of Array.from(url.searchParams.keys())) {
    if (/account|bank|holder|name|number/i.test(key)) {
      url.searchParams.set(key, "<redacted>");
    }
  }
  return url.toString().replace(/accountNo=[^&]+/gi, "accountNo=<redacted>");
}

function sampleText(value: string): string {
  return value.replace(/\s+/g, " ").slice(0, 260);
}

function maskHolder(value: string): string {
  if (value.length <= 2) {
    return "*".repeat(value.length);
  }
  return `${value.slice(0, 1)}${"*".repeat(Math.max(1, value.length - 2))}${value.slice(-1)}`;
}

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

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function blocked(message: string): never {
  throw new Error(`BLOCKED: ${message}`);
}
