import fs from "node:fs";
import path from "node:path";
import { expect, test, type Browser, type Locator, type Page, type Response, 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 DisbursementAccountCandidate {
  id?: number;
  name?: string;
  firmBankName?: string;
  balance?: number;
  isMonify?: boolean;
}

interface DisbursementAccountPayload {
  disbursementAccounts?: DisbursementAccountCandidate[];
  corporationAccount?: {
    id?: number;
    name?: string;
    balance?: number;
  };
  tenantLimit?: {
    limit?: number;
    used?: number;
  };
  transferAmount?: number;
}

interface RoleInspectionSnapshot {
  role: string;
  currentUrl: string;
  pageText: string;
  waitingRows: string[];
  waitingRowButtons: string[];
  modalOpened: boolean;
  modalText: string;
  visibleControlTexts: string[];
  visibleAccountControlNames: string[];
  selectedAccountName?: string;
  executeButtonVisible: boolean;
  executeButtonEnabled: boolean;
  accountPayload?: DisbursementAccountPayload;
  historyPayload?: unknown;
  allAccountNames: string[];
  tenantAccountNames: string[];
  corporationAccountNames: string[];
  hasHistoryArea: boolean;
  hasHistoryTitle: boolean;
  forbiddenAccountNames: string[];
  forbiddenVisibleAccountNames: string[];
}

const ROLES = ["ADMIN", "TA"] as const;
const SETTLEMENT_WAITING_BUTTON_TEXT = /^정산대기$/;
const HISTORY_AREA_PATTERN = /이체\s*실행\s*이력|거래후잔액|발신처|수신처/;
const HISTORY_TITLE_PATTERN = /(이체\s*실행\s*)?이력/;
const SETTLEMENT_PROCESS_GUARD = "QA_3590_ALLOW_SETTLEMENT_PROCESS";

test.use({ trace: "on" });

export function runSettlementWaitingAccountScopeSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const settlementPage = findPageDefinition(checklist.pages, "정산내역");
  const items = checklist.checklist;

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    test.setTimeout(Number(process.env.QA_TEST_TIMEOUT_MS ?? 120_000));

    test(items[0], async ({ browser }, testInfo) => {
      const snapshot = await inspectRole(browser, options, settlementPage, "ADMIN", testInfo, "admin-candidates");
      await attachRoleInspection(testInfo, settlementPage, snapshot, "admin-candidates");

      expect(snapshot.modalOpened, "ADMIN 정산대기 처리 모달이 열려야 합니다.").toBe(true);
      expect(snapshot.allAccountNames.length, "ADMIN 지급계좌 후보 API에 운영자 허용 범위 후보가 있어야 합니다.").toBeGreaterThan(0);
      for (const accountName of snapshot.allAccountNames) {
        expect(snapshot.modalText, `ADMIN 모달에 지급계좌 후보 '${accountName}'이 보여야 합니다.`).toContain(accountName);
      }
    });

    test(items[1], async ({ browser }, testInfo) => {
      const snapshot = await inspectRole(browser, options, settlementPage, "ADMIN", testInfo, "admin-forbidden-scope");
      await attachRoleInspection(testInfo, settlementPage, snapshot, "admin-forbidden-scope");

      expect(snapshot.modalOpened, "ADMIN 정산대기 처리 모달이 열려야 합니다.").toBe(true);
      expect(
        snapshot.visibleAccountControlNames,
        "ADMIN 화면의 지급계좌 선택 후보는 API가 내려준 후보 목록 안에만 있어야 합니다."
      ).toEqual(expect.arrayContaining(snapshot.visibleAccountControlNames.filter((name) => snapshot.allAccountNames.includes(name))));
      expect(
        snapshot.visibleAccountControlNames.filter((name) => !snapshot.allAccountNames.includes(name)),
        "ADMIN 화면에 API 후보 밖의 지급계좌가 노출되면 안 됩니다."
      ).toEqual([]);
      expect(
        snapshot.forbiddenVisibleAccountNames,
        `ADMIN 화면/API 후보에 허용되지 않은 지급계좌가 보이면 안 됩니다: ${snapshot.forbiddenAccountNames.join(", ")}`
      ).toEqual([]);
    });

    test(items[2], async ({ browser }, testInfo) => {
      const snapshot = await inspectRole(browser, options, settlementPage, "ADMIN", testInfo, "admin-history");
      await attachRoleInspection(testInfo, settlementPage, snapshot, "admin-history");

      expect(snapshot.modalOpened, "ADMIN 정산대기 처리 모달이 열려야 합니다.").toBe(true);
      expect(snapshot.hasHistoryArea, "ADMIN 계정에서 지급계좌 이력 영역이 보여야 합니다.").toBe(true);
      expect(snapshot.hasHistoryTitle, "ADMIN 이력 영역 제목에 '이력' 문구가 보여야 합니다.").toBe(true);
    });

    test(items[3], async ({ browser }, testInfo) => {
      const snapshot = await inspectRole(browser, options, settlementPage, "TA", testInfo, "ta-candidates");
      await attachRoleInspection(testInfo, settlementPage, snapshot, "ta-candidates");

      expect(snapshot.modalOpened, "TA 정산대기 처리 모달이 열려야 합니다.").toBe(true);
      expect(snapshot.tenantAccountNames.length, "TA 지급계좌 후보에 테넌트 지급계좌(isMonify=true)가 포함되어야 합니다.").toBeGreaterThan(0);
      expect(snapshot.corporationAccountNames.length, "TA 지급계좌 후보에 법인 지급계좌(isMonify=false)가 포함되어야 합니다.").toBeGreaterThan(0);
      for (const accountName of [...snapshot.tenantAccountNames, ...snapshot.corporationAccountNames]) {
        expect(snapshot.modalText, `TA 모달에 지급계좌 후보 '${accountName}'이 보여야 합니다.`).toContain(accountName);
      }
    });

    test(items[4], async ({ browser }, testInfo) => {
      const snapshot = await inspectRole(browser, options, settlementPage, "TA", testInfo, "ta-history-hidden");
      await attachRoleInspection(testInfo, settlementPage, snapshot, "ta-history-hidden");

      expect(snapshot.modalOpened, "TA 정산대기 처리 모달이 열려야 합니다.").toBe(true);
      expect(snapshot.hasHistoryArea, "TA 계정에서는 지급계좌 이력 영역이 보이면 안 됩니다.").toBe(false);
      expect(snapshot.hasHistoryTitle, "TA 계정에서는 이력 제목이 보이면 안 됩니다.").toBe(false);
    });

    test(items[5], async ({ browser }, testInfo) => {
      const snapshots: RoleInspectionSnapshot[] = [];
      for (const role of ROLES) {
        const snapshot = await inspectRole(browser, options, settlementPage, role, testInfo, `${role.toLowerCase()}-selectable`);
        snapshots.push(snapshot);
      }
      await attachHistoryMatrix(testInfo, settlementPage, snapshots, "candidate-selectable");

      for (const snapshot of snapshots) {
        expect(snapshot.modalOpened, `${snapshot.role} 정산대기 처리 모달이 열려야 합니다.`).toBe(true);
        expect(snapshot.visibleAccountControlNames.length, `${snapshot.role} 화면에 선택 가능한 지급계좌 후보가 보여야 합니다.`).toBeGreaterThan(0);
        expect(snapshot.selectedAccountName, `${snapshot.role} 화면에 보이는 지급계좌 후보를 선택할 수 있어야 합니다.`).toBeTruthy();
        expect(snapshot.executeButtonVisible, `${snapshot.role} 지급계좌 선택 후 이체 실행 버튼이 보여야 합니다.`).toBe(true);
        expect(snapshot.executeButtonEnabled, `${snapshot.role} 지급계좌 선택 후 이체 실행 버튼이 활성화되어야 합니다.`).toBe(true);
      }
    });
  });
}

async function inspectRole(
  browser: Browser,
  options: ScenarioOptions,
  pageDefinition: QaPageDefinition,
  role: string,
  testInfo: TestInfo,
  slug: string
): Promise<RoleInspectionSnapshot> {
  const runtimeEnv = getRuntimeQaEnv(options.environment, pageDefinition.application ?? "admin", role);
  const skipReason = getRuntimeBlockReason(runtimeEnv) ?? getRoleSkipReason(role);
  test.skip(!!skipReason, skipReason);

  return withTracedPage(browser, runtimeEnv, testInfo, pageDefinition.name, role, slug, async (page) => {
    const apiResponses = captureSettlementModalResponses(page);
    await openSettlementPage(page, pageDefinition, runtimeEnv);
    const waitingRows = await collectWaitingRows(page);
    const waitingRowButtons = await collectWaitingRowButtons(page);
    const waitingButton = await findSettlementWaitingButton(page);

    if (!waitingButton) {
      const pageText = await bodyTextOf(page);
      const snapshot: RoleInspectionSnapshot = {
        role,
        currentUrl: page.url(),
        pageText,
        waitingRows,
        waitingRowButtons,
        modalOpened: false,
        modalText: "",
        visibleControlTexts: [],
        visibleAccountControlNames: [],
        selectedAccountName: undefined,
        executeButtonVisible: false,
        executeButtonEnabled: false,
        accountPayload: undefined,
        historyPayload: undefined,
        allAccountNames: [],
        tenantAccountNames: [],
        corporationAccountNames: [],
        hasHistoryArea: false,
        hasHistoryTitle: false,
        forbiddenAccountNames: getForbiddenAccountNames(),
        forbiddenVisibleAccountNames: []
      };
      await attachRoleInspection(testInfo, pageDefinition, snapshot, `${slug}-no-action`);
      await attachPageScreenshot(page, testInfo, pageDefinition.name, role, `${slug}-no-action`);
      return snapshot;
    }

    await waitingButton.scrollIntoViewIfNeeded({ timeout: 3_000 }).catch(() => undefined);
    await waitingButton.click({ timeout: 5_000 });
    const modal = await waitForSettlementWaitingModal(page);
    await page.waitForTimeout(1_500);
    const payloads = await apiResponses.flush();
    const modalText = await modalTextOf(modal);
    const accountPayload = extractAccountPayload(payloads);
    const historyPayload = extractHistoryPayload(payloads);
    const accounts = accountPayload?.disbursementAccounts ?? [];
    const allAccountNames = accounts.map((account) => account.name).filter(isNonEmptyString);
    const tenantAccountNames = accounts.filter((account) => account.isMonify === true).map((account) => account.name).filter(isNonEmptyString);
    const corporationAccountNames = accounts.filter((account) => account.isMonify !== true).map((account) => account.name).filter(isNonEmptyString);
    const visibleControlTexts = await collectVisibleControlTexts(modal);
    const visibleAccountControlNames = allAccountNames.filter((accountName) =>
      visibleControlTexts.some((text) => text.includes(accountName))
    );
    const selectedAccountName = await selectFirstVisibleAccountCandidate(modal, visibleAccountControlNames);
    const executeButton = modal.locator("button, [role='button']").filter({ hasText: /이체\s*실행|처리|확인|저장/ }).last();
    const executeButtonVisible = await executeButton.isVisible({ timeout: 500 }).catch(() => false);
    const executeButtonEnabled = executeButtonVisible ? await executeButton.isEnabled({ timeout: 500 }).catch(() => false) : false;
    if (process.env[SETTLEMENT_PROCESS_GUARD] === "1" && selectedAccountName && executeButtonVisible && executeButtonEnabled) {
      await executeButton.click({ timeout: 3_000 });
      await page.waitForTimeout(1_000);
    }
    const forbiddenAccountNames = getForbiddenAccountNames();
    const forbiddenVisibleAccountNames = forbiddenAccountNames.filter((name) =>
      modalText.includes(name) || allAccountNames.includes(name) || visibleAccountControlNames.includes(name)
    );
    const snapshot: RoleInspectionSnapshot = {
      role,
      currentUrl: page.url(),
      pageText: await bodyTextOf(page),
      waitingRows,
      waitingRowButtons,
      modalOpened: true,
      modalText,
      visibleControlTexts,
      visibleAccountControlNames,
      selectedAccountName,
      executeButtonVisible,
      executeButtonEnabled,
      accountPayload,
      historyPayload,
      allAccountNames,
      tenantAccountNames,
      corporationAccountNames,
      hasHistoryArea: HISTORY_AREA_PATTERN.test(modalText),
      hasHistoryTitle: HISTORY_TITLE_PATTERN.test(modalText),
      forbiddenAccountNames,
      forbiddenVisibleAccountNames
    };

    await attachPageScreenshot(page, testInfo, pageDefinition.name, role, slug);
    return snapshot;
  });
}

async function openSettlementPage(
  page: Page,
  pageDefinition: QaPageDefinition,
  runtimeEnv: RuntimeQaEnv
): Promise<void> {
  if (!runtimeEnv.storageState) {
    await loginWithCredentials(page, runtimeEnv);
  }

  const targetUrl = buildSettlementTargetUrl(runtimeEnv, pageDefinition);
  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), `${pageDefinition.name} 진입 전 로그인 화면을 벗어나야 합니다.`).toBe(false);
  await expect(page.locator("body"), "정산내역 화면 본문이 표시되어야 합니다.").toContainText(/정산\s*내역|정산내역/, {
    timeout: 15_000
  });
  await applySettlementMonthlySearch(page);
  await expect(page.locator("body"), "정산대기 상태 정산 건이 조회되어야 합니다.").toContainText("정산대기", { timeout: 15_000 });
}

function buildSettlementTargetUrl(runtimeEnv: RuntimeQaEnv, pageDefinition: QaPageDefinition): string {
  const url = new URL(buildPageUrl(runtimeEnv.baseUrl!, pageDefinition, runtimeEnv.tenantId!));
  const { from, to } = getSettlementDateRange();
  url.searchParams.set("from", from);
  url.searchParams.set("to", to);
  return url.toString();
}

function getSettlementDateRange(): { from: string; to: string } {
  const now = new Date();
  const year = now.getFullYear();
  const month = now.getMonth();
  const firstDay = new Date(year, month, 1);
  const lastDay = new Date(year, month + 1, 0);
  return {
    from: process.env.QA_3590_SETTLEMENT_FROM ?? formatDate(firstDay),
    to: process.env.QA_3590_SETTLEMENT_TO ?? formatDate(lastDay)
  };
}

function formatDate(value: Date): string {
  return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
}

async function applySettlementMonthlySearch(page: Page): Promise<void> {
  const root = await activeRouteRoot(page);
  const monthButton = root.locator("button, [role='button']").filter({ hasText: /^당월$/ }).first();
  if (await monthButton.isVisible({ timeout: 1_000 }).catch(() => false)) {
    await monthButton.click({ timeout: 2_000 }).catch(() => undefined);
  }

  const searchButton = root.locator("button, [role='button']").filter({ hasText: /^검색$/ }).first();
  if (await searchButton.isVisible({ timeout: 1_000 }).catch(() => false)) {
    await Promise.all([
      page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined),
      searchButton.click({ timeout: 2_000 }).catch(() => undefined)
    ]);
    await page.waitForTimeout(700);
  }
}

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

async function findSettlementWaitingButton(page: Page): Promise<Locator | undefined> {
  const root = await activeRouteRoot(page);
  const rows = root.locator("tr, [role='row']");
  const rowCount = Math.min(await rows.count().catch(() => 0), 160);

  for (let index = 0; index < rowCount; index += 1) {
    const row = rows.nth(index);
    if (!(await row.isVisible({ timeout: 150 }).catch(() => false))) {
      continue;
    }
    const rowText = await locatorText(row);
    if (!/선정산/.test(rowText)) {
      continue;
    }
    const button = row.locator("button, [role='button'], a").filter({ hasText: SETTLEMENT_WAITING_BUTTON_TEXT }).first();
    if (await button.isVisible({ timeout: 250 }).catch(() => false)) {
      return button;
    }
  }

  return undefined;
}

async function waitForSettlementWaitingModal(page: Page): Promise<Locator> {
  const modal = page
    .locator("[role='dialog'], .modal.show, .modal, .MuiDialog-root, .ant-modal, [class*='Modal']")
    .filter({ hasText: /정산대기|자금\s*이동|지급계좌|이체\s*실행/ })
    .last();
  await expect(modal, "정산대기 자금 이동 처리 모달이 표시되어야 합니다.").toBeVisible({ timeout: 15_000 });
  return modal;
}

async function activeRouteRoot(page: Page): Promise<Locator> {
  const routeRoot = page.locator("[data-route-keep-alive-active='true']").last();
  return (await routeRoot.count().catch(() => 0)) > 0 ? routeRoot : page.locator("body");
}

async function collectWaitingRows(page: Page): Promise<string[]> {
  const root = await activeRouteRoot(page);
  const rows = root.locator("tr, [role='row']");
  const output: string[] = [];
  const count = Math.min(await rows.count().catch(() => 0), 120);

  for (let index = 0; index < count; index += 1) {
    const row = rows.nth(index);
    if (!(await row.isVisible({ timeout: 150 }).catch(() => false))) {
      continue;
    }
    const text = await locatorText(row);
    if (/정산대기/.test(text)) {
      output.push(text);
    }
  }

  return unique(output);
}

async function collectWaitingRowButtons(page: Page): Promise<string[]> {
  const root = await activeRouteRoot(page);
  const rows = root.locator("tr, [role='row']");
  const output: string[] = [];
  const count = Math.min(await rows.count().catch(() => 0), 120);

  for (let index = 0; index < count; index += 1) {
    const row = rows.nth(index);
    if (!(await row.isVisible({ timeout: 150 }).catch(() => false))) {
      continue;
    }
    const text = await locatorText(row);
    if (!/정산대기/.test(text)) {
      continue;
    }
    output.push(...await collectVisibleControlTexts(row));
  }

  return unique(output);
}

function captureSettlementModalResponses(page: Page): { flush: () => Promise<CapturedResponse[]> } {
  const responses: CapturedResponse[] = [];
  const pending: Promise<void>[] = [];

  const listener = (response: Response) => {
    if (!/\/api\/v1\/settlement\/[^/]+\/(disbursement-accounts|transfer-histories)/.test(response.url())) {
      return;
    }
    const promise = collectResponseJson(response)
      .then((captured) => {
        if (captured) {
          responses.push(captured);
        }
      })
      .catch(() => undefined);
    pending.push(promise);
  };

  page.on("response", listener);

  return {
    flush: async () => {
      await Promise.race([Promise.allSettled(pending), page.waitForTimeout(1_500)]);
      page.off("response", listener);
      return responses;
    }
  };
}

interface CapturedResponse {
  url: string;
  status: number;
  json: unknown;
}

async function collectResponseJson(response: Response): Promise<CapturedResponse | undefined> {
  const contentType = await response.headerValue("content-type").catch(() => "");
  if (!contentType?.includes("application/json")) {
    return undefined;
  }

  const json = await response.json().catch(() => undefined);
  if (json === undefined) {
    return undefined;
  }

  return {
    url: response.url(),
    status: response.status(),
    json
  };
}

function extractAccountPayload(responses: CapturedResponse[]): DisbursementAccountPayload | undefined {
  const response = responses.find((item) => item.url.includes("/disbursement-accounts"));
  return unwrapPayload(response?.json) as DisbursementAccountPayload | undefined;
}

function extractHistoryPayload(responses: CapturedResponse[]): unknown {
  const response = responses.find((item) => item.url.includes("/transfer-histories"));
  return unwrapPayload(response?.json);
}

function unwrapPayload(value: unknown): unknown {
  if (!value || typeof value !== "object") {
    return value;
  }
  const object = value as Record<string, unknown>;
  return object.payload ?? object.data ?? value;
}

async function withTracedPage<T>(
  browser: Browser,
  runtimeEnv: RuntimeQaEnv,
  testInfo: TestInfo,
  pageName: string,
  role: string,
  slug: string,
  callback: (page: Page) => Promise<T>
): Promise<T> {
  const context = await browser.newContext(
    runtimeEnv.storageState
      ? { storageState: runtimeEnv.storageState, acceptDownloads: true }
      : { acceptDownloads: true }
  );
  const page = await context.newPage();

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

async function attachRoleInspection(
  testInfo: TestInfo,
  pageDefinition: QaPageDefinition,
  snapshot: RoleInspectionSnapshot,
  slug: string
): Promise<void> {
  await attachEvidenceLog(testInfo, pageDefinition.name, snapshot.role, slug, [
    `role: ${snapshot.role}`,
    `URL: ${redactUrl(snapshot.currentUrl)}`,
    `정산대기 행: ${snapshot.waitingRows.join(" / ") || "-"}`,
    `정산대기 행 버튼: ${snapshot.waitingRowButtons.join(" / ") || "-"}`,
    `모달 열림: ${snapshot.modalOpened ? "yes" : "no"}`,
    `지급계좌 후보(API): ${snapshot.allAccountNames.join(" / ") || "-"}`,
    `테넌트 지급계좌(isMonify=true): ${snapshot.tenantAccountNames.join(" / ") || "-"}`,
    `법인 지급계좌(isMonify=false): ${snapshot.corporationAccountNames.join(" / ") || "-"}`,
    `화면 선택 후보: ${snapshot.visibleAccountControlNames.join(" / ") || "-"}`,
    `선택한 후보: ${snapshot.selectedAccountName ?? "-"}`,
    `이체 실행 버튼: ${snapshot.executeButtonVisible ? "visible" : "hidden"} / ${snapshot.executeButtonEnabled ? "enabled" : "disabled"}`,
    `실제 처리 guard(${SETTLEMENT_PROCESS_GUARD}): ${process.env[SETTLEMENT_PROCESS_GUARD] === "1" ? "enabled" : "disabled"}`,
    `허용되지 않은 후보 검사값: ${snapshot.forbiddenAccountNames.join(" / ") || "별도 미제공"}`,
    `허용되지 않은 후보 노출: ${snapshot.forbiddenVisibleAccountNames.join(" / ") || "-"}`,
    `모달 버튼/선택 컨트롤: ${snapshot.visibleControlTexts.join(" / ") || "-"}`,
    `이력 영역: ${snapshot.hasHistoryArea ? "visible" : "hidden"} / title=${snapshot.hasHistoryTitle ? "visible" : "hidden"}`,
    `계좌 API 요약: ${formatJson(snapshot.accountPayload)}`,
    `이력 API 요약: ${formatJson(snapshot.historyPayload)}`,
    `모달 텍스트 일부: ${snippet(snapshot.modalText || snapshot.pageText, 1800)}`
  ]);
}

async function attachHistoryMatrix(
  testInfo: TestInfo,
  pageDefinition: QaPageDefinition,
  snapshots: RoleInspectionSnapshot[],
  slug: string
): Promise<void> {
  await attachEvidenceLog(testInfo, pageDefinition.name, "ALL", slug, [
    "| role | 모달 | 테넌트 지급계좌 | 법인 지급계좌 | 선택 후보 | 선택값 | 실행 버튼 | 이력 영역 |",
    "|---|---|---|---|---|---|---|---|",
    ...snapshots.map((snapshot) => [
      snapshot.role,
      snapshot.modalOpened ? "열림" : "미열림",
      snapshot.tenantAccountNames.join(", ") || "-",
      snapshot.corporationAccountNames.join(", ") || "-",
      snapshot.visibleAccountControlNames.join(", ") || "-",
      snapshot.selectedAccountName ?? "-",
      `${snapshot.executeButtonVisible ? "노출" : "미노출"}/${snapshot.executeButtonEnabled ? "활성" : "비활성"}`,
      snapshot.hasHistoryArea ? "노출" : "미노출"
    ].join(" | "))
  ]);
}

async function attachEvidenceLog(
  testInfo: TestInfo,
  pageName: string,
  role: string,
  slug: string,
  lines: string[]
): Promise<void> {
  const filePath = testInfo.outputPath(`${safeName(pageName)}-${role}-${slug}.md`);
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf8");
  await testInfo.attach(`${pageName} / ${role} / ${slug} log`, {
    path: filePath,
    contentType: "text/markdown"
  });
}

async function attachPageScreenshot(
  page: Page,
  testInfo: TestInfo,
  pageName: string,
  role: string,
  slug: string
): Promise<void> {
  const filePath = testInfo.outputPath(`${safeName(pageName)}-${role}-${slug}.png`);
  await page.screenshot({ path: filePath, fullPage: true }).catch(() => undefined);
  if (fs.existsSync(filePath)) {
    await testInfo.attach(`${pageName} / ${role} / ${slug} screenshot`, {
      path: filePath,
      contentType: "image/png"
    });
  }
}

async function modalTextOf(modal: Locator): Promise<string> {
  return normalizeText(await modal.innerText({ timeout: 5_000 }).catch(() => ""));
}

async function locatorText(locator: Locator): Promise<string> {
  return normalizeText(await locator.innerText({ timeout: 1_000 }).catch(() => ""));
}

async function bodyTextOf(page: Page): Promise<string> {
  return normalizeText(await page.locator("body").innerText({ timeout: 5_000 }).catch(() => ""));
}

async function collectVisibleControlTexts(locator: Locator): Promise<string[]> {
  const texts = await locator.locator("button, a, [role='button']").evaluateAll((nodes) => {
    function normalize(value: string): string {
      return value.replace(/\s+/g, " ").trim();
    }

    function visible(element: Element): boolean {
      const htmlElement = element as HTMLElement;
      const style = window.getComputedStyle(htmlElement);
      const rect = htmlElement.getBoundingClientRect();
      return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
    }

    return nodes
      .filter(visible)
      .map((node) => normalize((node as HTMLElement).innerText || node.getAttribute("aria-label") || node.textContent || ""))
      .filter(Boolean);
  }).catch(() => []);

  return unique(texts);
}

async function selectFirstVisibleAccountCandidate(modal: Locator, accountNames: string[]): Promise<string | undefined> {
  for (const accountName of accountNames) {
    const candidate = modal
      .locator("button, [role='button'], label, [class*='card'], [class*='Card']")
      .filter({ hasText: new RegExp(escapeRegExp(accountName)) })
      .first();
    if (!(await candidate.isVisible({ timeout: 300 }).catch(() => false))) {
      continue;
    }
    await candidate.click({ timeout: 2_000 }).catch(async () => {
      await candidate.dispatchEvent("click").catch(() => undefined);
    });
    return accountName;
  }

  return undefined;
}

function getForbiddenAccountNames(): string[] {
  return unique([
    ...splitEnvList(process.env.QA_3590_FORBIDDEN_DISBURSEMENT_ACCOUNT_NAMES),
    ...splitEnvList(process.env.QA_3590_FORBIDDEN_ACCOUNT_NAMES)
  ]);
}

function splitEnvList(value: string | undefined): string[] {
  return (value ?? "")
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
}

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

function getRoleSkipReason(role: string): string | undefined {
  const skippedRoles = (process.env.QA_SKIP_ROLES ?? "")
    .split(",")
    .map((value) => value.trim().toUpperCase())
    .filter(Boolean);
  return skippedRoles.includes(role.toUpperCase()) ? `${role} role auth refresh 실패로 QA_SKIP_ROLES에 포함되었습니다.` : undefined;
}

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

function normalizeText(value: string): string {
  return value.replace(/\s+/g, " ").trim();
}

function unique(values: string[]): string[] {
  return [...new Set(values.filter(Boolean))];
}

function isNonEmptyString(value: unknown): value is string {
  return typeof value === "string" && value.trim().length > 0;
}

function snippet(value: string, maxLength = 800): string {
  return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
}

function formatJson(value: unknown): string {
  if (value === undefined) {
    return "-";
  }
  return snippet(JSON.stringify(value), 1600);
}

function redactUrl(rawUrl: string): string {
  try {
    const url = new URL(rawUrl);
    if (url.searchParams.has("tenantId")) {
      url.searchParams.set("tenantId", "<redacted>");
    }
    return url.toString();
  } catch {
    return rawUrl.replace(/tenantId=redacted&]+/g, "tenantId=<redacted>");
  }
}

function safeName(value: string): string {
  return value.replace(/[^\p{L}\p{N}._-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 90);
}

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