import fs from "node:fs";
import path from "node:path";
import { expect, test, type Browser, type BrowserContext, 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";

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

interface StoreSnapshot {
  bodyText: string;
  buttonTexts: string[];
  linkText: string[];
  transactions: TransactionBlock[];
}

interface TransactionBlock {
  status: string;
  text: string;
  paymentType: "link" | "manual" | "unknown";
  isCancelled: boolean;
  approvalNo?: string;
}

interface AdminSnapshot {
  bodyText: string;
  buttonTexts: string[];
}

interface SubjectRuntime {
  role: string;
  label: string;
  envRole: string;
  runtimeEnv: RuntimeQaEnv;
}

interface AppPushEvidence {
  status: "pass" | "fail" | "blocked";
  title: string;
  expected: string;
  actual: string;
  paymentUrl?: string;
  paymentResultUrl?: string;
  finalUrl?: string;
  notification?: {
    title?: string;
    body?: string;
  };
  steps?: string[];
  screenshots?: string[];
}

const STORE_PATH = "/transactions";
const ADMIN_PATH = "/trade-history-v2";
const DELIVERY_BUTTON_PATTERN = /배송\s*(이미지|사진)|상품\s*배송|이미지\s*(등록|업로드|보기|확인)|사진\s*(등록|업로드|보기|확인)/;
const DELIVERY_STATUS_PATTERN = /미등록|등록\s*완료|이미지\s*없|배송\s*(이미지|사진)/;
const REQUIRED_LINK_APPROVAL = process.env.QA_3318_REQUIRED_LINK_APPROVAL_NO ?? "15834237";
const OPTIONAL_LINK_APPROVAL = process.env.QA_3318_OPTIONAL_LINK_APPROVAL_NO ?? "47369256";
const REQUIRED_MANUAL_APPROVAL = process.env.QA_3318_REQUIRED_MANUAL_APPROVAL_NO ?? "15850408";

export function runDeliveryImagePushSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const storePage = findPage(checklist.pages, "스토어 거래내역", STORE_PATH);
  const deliveryPage = findPage(checklist.pages, "상품 배송 이미지 등록", STORE_PATH);
  const adminPage = findPage(checklist.pages, "어드민 거래내역", ADMIN_PATH);
  const requiredVe = subjectRuntime(options.environment, "VE", "VE_DELIVERY_REQUIRED", "상품 배송 이미지 필수 가맹점");
  const optionalVe = subjectRuntime(options.environment, "VE", "VE_DELIVERY_OPTIONAL", "상품 배송 이미지 미사용 가맹점");
  const boRequired = subjectRuntime(options.environment, "BO", "BO_DELIVERY_REQUIRED", "필수 설정 연결 영업점");
  const coRequired = subjectRuntime(options.environment, "CO", "CO_DELIVERY_REQUIRED", "필수 설정 연결 법인");
  const adminEnv = getRuntimeQaEnv(options.environment, "admin", "ADMIN");

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    test.describe("[VE] role / 앱 푸시", () => {
      test(checklist.checklist[0], async ({}, testInfo) => {
        const evidence = await attachAppPushEvidence(testInfo, options, "app-push-required-link");
        if (!evidence) {
          blocked("필수 ON 가맹점의 앱 푸시 실기기 증거가 없어 이동 결과를 확인할 수 없습니다.");
        }
        if (evidence.status === "blocked") {
          blocked(evidence.actual);
        }
        expect(evidence.status, evidence.actual).toBe("pass");
      });

      test(checklist.checklist[1], async ({}, testInfo) => {
        const evidence = await attachAppPushEvidence(testInfo, options, "app-push-optional-link");
        if (!evidence) {
          blocked("필수 OFF 가맹점의 앱 푸시 실기기 증거가 없어 기존 거래내역 이동 결과를 확인할 수 없습니다.");
        }
        if (evidence.status === "blocked") {
          blocked(evidence.actual);
        }
        expect(evidence.status, evidence.actual).toBe("pass");
      });

      test(checklist.checklist[2], async ({}, testInfo) => {
        const evidence = await attachAppPushEvidence(testInfo, options, "app-push-required-manual");
        if (!evidence) {
          blocked("필수 ON 가맹점의 수기결제 앱 푸시 실기기 증거가 없어 기존 거래내역 이동 결과를 확인할 수 없습니다.");
        }
        if (evidence.status === "blocked") {
          blocked(evidence.actual);
        }
        expect(evidence.status, evidence.actual).toBe("pass");
      });
    });

    test.describe("[VE] role / 스토어 거래내역", () => {
      test(checklist.checklist[3], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-link-list");
        const target = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
        expect(target, "필수 ON 가맹점의 링크결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
        expect(hasDeliveryButton(target?.text ?? ""), "필수 ON 가맹점의 링크결제 거래에는 배송 이미지 버튼이 보여야 합니다.").toBe(true);
      });

      test(checklist.checklist[4], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, optionalVe, storePage, testInfo, "optional-link-list");
        const target = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: OPTIONAL_LINK_APPROVAL });
        expect(target, "필수 OFF 가맹점의 링크결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
        expect(hasDeliveryButton(target?.text ?? ""), "필수 OFF 가맹점의 링크결제 거래에는 배송 이미지 버튼이 보이면 안 됩니다.").toBe(false);
      });

      test(checklist.checklist[5], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-manual-list");
        const target = findTransaction(snapshot, { paymentType: "manual", cancelled: false, approvalNo: REQUIRED_MANUAL_APPROVAL });
        expect(target, "필수 ON 가맹점의 수기결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
        expect(hasDeliveryButton(target?.text ?? ""), "수기결제 거래에는 배송 이미지 버튼이 보이면 안 됩니다.").toBe(false);
      });

      test(checklist.checklist[6], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-cancel-list");
        const target = findTransaction(snapshot, { paymentType: "link", cancelled: true, approvalNo: REQUIRED_LINK_APPROVAL });
        expect(target, "취소 상태 링크결제 거래가 목록에 보여야 합니다.").toBeTruthy();
        expect(hasDeliveryButton(target?.text ?? ""), "취소 상태 거래에는 배송 이미지 업로드/보기 버튼이 보이면 안 됩니다.").toBe(false);
      });

      test(checklist.checklist[7], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, coRequired, storePage, testInfo, "co-required-list");
        const linkTransactions = snapshot.transactions.filter((transaction) => transaction.paymentType === "link");
        expect(linkTransactions.length, "법인 계정에서 링크결제 거래가 조회되어야 권한 제한을 확인할 수 있습니다.").toBeGreaterThan(0);
        expect(linkTransactions.some((transaction) => hasDeliveryButton(transaction.text)), "법인 계정에는 배송 이미지 등록 버튼이 보이면 안 됩니다.").toBe(false);
      });

      test(checklist.checklist[8], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, boRequired, storePage, testInfo, "bo-required-list");
        const linkTransactions = snapshot.transactions.filter((transaction) => transaction.paymentType === "link");
        expect(linkTransactions.length, "영업점 계정에서 링크결제 거래가 조회되어야 권한 제한을 확인할 수 있습니다.").toBeGreaterThan(0);
        expect(linkTransactions.some((transaction) => hasDeliveryButton(transaction.text)), "영업점 계정에는 배송 이미지 등록 버튼이 보이면 안 됩니다.").toBe(false);
      });

      test(checklist.checklist[9], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-only-register");
        const linkTransaction = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
        expect(linkTransaction, "가맹점 계정의 링크결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
        expect(hasDeliveryButton(linkTransaction?.text ?? ""), "가맹점 계정의 링크결제 거래에는 배송 이미지 등록 버튼이 보여야 합니다.").toBe(true);
      });

      test(checklist.checklist[10], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "delivery-status");
        expect(DELIVERY_STATUS_PATTERN.test(snapshot.bodyText), "스토어 거래내역에서 미등록/등록완료 또는 이미지 상태가 구분되어야 합니다.").toBe(true);
      });
    });

    test.describe("[VE] role / 상품 배송 이미지 등록", () => {
      for (let index = 11; index <= 17; index += 1) {
        test(checklist.checklist[index], async ({ browser }, testInfo) => {
          testInfo.setTimeout(75_000);
          const snapshot = await collectStoreEvidence(browser, requiredVe, deliveryPage, testInfo, `delivery-page-${index + 1}`);
          const linkTransaction = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
          if (!linkTransaction) {
            blocked("가맹점 계정의 링크결제 승인 거래가 목록에 없어 상품 배송 이미지 등록 화면으로 진입할 수 없습니다.");
          }
          expect(hasDeliveryButton(linkTransaction.text), "배송 이미지 등록 화면으로 진입할 버튼이 보여야 합니다.").toBe(true);
        });
      }

      test(checklist.checklist[18], async ({ browser }, testInfo) => {
        testInfo.setTimeout(90_000);
        const [veSnapshot, coSnapshot, boSnapshot] = await Promise.all([
          collectStoreEvidence(browser, requiredVe, storePage, testInfo, "department-user-ve"),
          collectStoreEvidence(browser, coRequired, storePage, testInfo, "department-user-co"),
          collectStoreEvidence(browser, boRequired, storePage, testInfo, "department-user-bo")
        ]);
        const veLink = findTransaction(veSnapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
        const coLinks = coSnapshot.transactions.filter((transaction) => transaction.paymentType === "link");
        const boLinks = boSnapshot.transactions.filter((transaction) => transaction.paymentType === "link");
        expect(veLink, "가맹점 계정에서 링크결제 승인 거래가 보여야 합니다.").toBeTruthy();
        expect(hasDeliveryButton(veLink?.text ?? ""), "해당 거래의 가맹점 사용자에게는 이미지 등록/삭제 진입 버튼이 보여야 합니다.").toBe(true);
        expect(coLinks.some((transaction) => hasDeliveryButton(transaction.text)), "법인 계정에는 이미지 등록/삭제 버튼이 보이면 안 됩니다.").toBe(false);
        expect(boLinks.some((transaction) => hasDeliveryButton(transaction.text)), "영업점 계정에는 이미지 등록/삭제 버튼이 보이면 안 됩니다.").toBe(false);
      });
    });

    test.describe("[ADMIN] role / 어드민 거래내역", () => {
      test(checklist.checklist[19], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectAdminEvidence(browser, adminEnv, adminPage, testInfo, "admin-required-link-image");
        const rowText = rowAround(snapshot.bodyText, REQUIRED_LINK_APPROVAL);
        expect(rowText, "필수 ON 링크결제 거래가 어드민 거래내역에 보여야 합니다.").toContain(REQUIRED_LINK_APPROVAL);
        expect(hasDeliveryButton(rowText) || hasDeliveryButton(snapshot.buttonTexts.join("\n")), "어드민 거래내역에서 필수 ON 링크결제 거래의 배송 이미지 확인이 가능해야 합니다.").toBe(true);
      });

      test(checklist.checklist[20], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectAdminEvidence(browser, adminEnv, adminPage, testInfo, "admin-required-link-empty-image");
        const rowText = rowAround(snapshot.bodyText, REQUIRED_LINK_APPROVAL);
        expect(rowText, "필수 ON 링크결제 거래가 어드민 거래내역에 보여야 합니다.").toContain(REQUIRED_LINK_APPROVAL);
        expect(/이미지가\s*없|이미지\s*없|없습니다/.test(rowText) || hasDeliveryButton(rowText), "등록된 배송 이미지가 없으면 이미지가 없습니다 안내 또는 이미지 확인 진입이 보여야 합니다.").toBe(true);
      });

      test(checklist.checklist[21], async ({ browser }, testInfo) => {
        testInfo.setTimeout(75_000);
        const snapshot = await collectAdminEvidence(browser, adminEnv, adminPage, testInfo, "admin-optional-link-no-image");
        const rowText = rowAround(snapshot.bodyText, OPTIONAL_LINK_APPROVAL);
        expect(rowText, "필수 OFF 링크결제 거래가 어드민 거래내역에 보여야 합니다.").toContain(OPTIONAL_LINK_APPROVAL);
        expect(hasDeliveryButton(rowText), "필수 OFF 링크결제 거래에는 이미지 조회 버튼이 보이면 안 됩니다.").toBe(false);
      });
    });
  });
}

async function attachAppPushEvidence(
  testInfo: TestInfo,
  options: ScenarioOptions,
  slug: string
): Promise<AppPushEvidence | undefined> {
  const evidenceRoot = path.join(process.cwd(), "qa", "issues", options.issueId, options.environment, "artifacts", slug);
  const evidencePath = path.join(evidenceRoot, "evidence.json");
  if (!fs.existsSync(evidencePath)) {
    return undefined;
  }

  const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8")) as AppPushEvidence;
  await testInfo.attach(`${slug}-evidence.md`, {
    body: [
      `# ${evidence.title}`,
      "",
      `- 기대 결과: ${evidence.expected}`,
      `- 실제 결과: ${evidence.actual}`,
      evidence.paymentUrl ? `- 결제 링크: ${evidence.paymentUrl}` : undefined,
      evidence.paymentResultUrl ? `- 결제 완료 URL: ${evidence.paymentResultUrl}` : undefined,
      evidence.finalUrl ? `- 최종 도착 URL: ${evidence.finalUrl}` : undefined,
      evidence.notification ? `- 수신 푸시: ${evidence.notification.title ?? ""} / ${evidence.notification.body ?? ""}` : undefined,
      "",
      "## 실제 진행",
      ...(evidence.steps ?? []).map((step, index) => `${index + 1}. ${step}`)
    ].filter((line): line is string => typeof line === "string").join("\n"),
    contentType: "text/markdown"
  });

  for (const screenshot of evidence.screenshots ?? []) {
    const screenshotPath = path.join(evidenceRoot, screenshot);
    if (!fs.existsSync(screenshotPath)) {
      continue;
    }
    await testInfo.attach(`${slug}-${screenshot}`, {
      body: fs.readFileSync(screenshotPath),
      contentType: "image/png"
    });
  }

  return evidence;
}

async function collectStoreEvidence(
  browser: Browser,
  subject: SubjectRuntime,
  pageDefinition: QaPageDefinition,
  testInfo: TestInfo,
  suffix: string
): Promise<StoreSnapshot> {
  return withRolePage(browser, subject.runtimeEnv, async (page) => {
    await openAuthenticatedPath(page, subject.runtimeEnv, pageDefinition.path || STORE_PATH);
    await settle(page);
    const snapshot = await storeSnapshot(page);
    await attachPageScreenshot(page, testInfo, pageDefinition.name, subject.envRole, suffix);
    await attachEvidenceLog(testInfo, pageDefinition.name, subject.role, suffix, [
      `${subject.label}(${subject.envRole}) 계정으로 ${page.url()} 화면을 조회했습니다.`,
      `거래 블록 수: ${snapshot.transactions.length}`,
      `버튼 텍스트: ${snapshot.buttonTexts.join(" / ") || "없음"}`,
      `링크결제 승인번호 후보: ${snapshot.transactions.filter((transaction) => transaction.paymentType === "link").map((transaction) => transaction.approvalNo ?? "번호 없음").join(", ") || "없음"}`,
      "화면 텍스트 일부:",
      snapshot.bodyText.slice(0, 2500)
    ]);
    return snapshot;
  });
}

async function collectAdminEvidence(
  browser: Browser,
  runtimeEnv: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  testInfo: TestInfo,
  suffix: string
): Promise<AdminSnapshot> {
  return withRolePage(browser, runtimeEnv, async (page) => {
    await openAuthenticatedPath(page, runtimeEnv, pageDefinition.path || ADMIN_PATH);
    await settle(page);
    const snapshot = await adminSnapshot(page);
    await attachPageScreenshot(page, testInfo, pageDefinition.name, runtimeEnv.role, suffix);
    await attachEvidenceLog(testInfo, pageDefinition.name, runtimeEnv.role, suffix, [
      `${runtimeEnv.role} 계정으로 ${page.url()} 화면을 조회했습니다.`,
      `버튼 텍스트: ${snapshot.buttonTexts.join(" / ") || "없음"}`,
      `${REQUIRED_LINK_APPROVAL} 주변 텍스트:`,
      rowAround(snapshot.bodyText, REQUIRED_LINK_APPROVAL),
      `${OPTIONAL_LINK_APPROVAL} 주변 텍스트:`,
      rowAround(snapshot.bodyText, OPTIONAL_LINK_APPROVAL)
    ]);
    return snapshot;
  }, { desktop: true });
}

async function withRolePage<T>(
  browser: Browser,
  runtimeEnv: RuntimeQaEnv,
  callback: (page: Page, context: BrowserContext) => Promise<T>,
  options: { desktop?: boolean } = {}
): Promise<T> {
  const context = await browser.newContext({
    storageState: runtimeEnv.storageState,
    viewport: options.desktop ? { width: 1440, height: 1100 } : { width: 390, height: 844 },
    isMobile: !options.desktop,
    hasTouch: !options.desktop
  });
  const page = await context.newPage();
  try {
    const result = await callback(page, context);
    return result;
  } finally {
    await context.close();
  }
}

async function openAuthenticatedPath(page: Page, runtimeEnv: RuntimeQaEnv, targetPath: string): Promise<void> {
  if (!runtimeEnv.baseUrl) {
    blocked(`${runtimeEnv.application}/${runtimeEnv.environment}/${runtimeEnv.role} baseUrl 설정이 필요합니다.`);
  }
  await page.goto(new URL(targetPath, runtimeEnv.baseUrl).toString(), { waitUntil: "domcontentloaded" });
  await settle(page);
  if (await isLoginPage(page, runtimeEnv)) {
    await loginWithCredentials(page, runtimeEnv);
    await page.goto(new URL(targetPath, runtimeEnv.baseUrl).toString(), { waitUntil: "domcontentloaded" });
    await settle(page);
  }
}

async function storeSnapshot(page: Page): Promise<StoreSnapshot> {
  const bodyText = await page.locator("body").innerText({ timeout: 10_000 });
  const buttonTexts = await visibleTexts(page, "button");
  const linkText = await visibleTexts(page, "a");
  return {
    bodyText,
    buttonTexts,
    linkText,
    transactions: parseStoreTransactions(bodyText)
  };
}

async function adminSnapshot(page: Page): Promise<AdminSnapshot> {
  return {
    bodyText: await page.locator("body").innerText({ timeout: 10_000 }),
    buttonTexts: await visibleTexts(page, "button")
  };
}

function parseStoreTransactions(bodyText: string): TransactionBlock[] {
  const lines = bodyText.split(/\n+/).map((line) => line.trim()).filter(Boolean);
  const blocks: string[][] = [];
  let current: string[] = [];

  for (const line of lines) {
    if (/^결제(승인|취소)/.test(line)) {
      if (current.length > 0) {
        blocks.push(current);
      }
      current = [line];
      continue;
    }
    if (current.length > 0) {
      current.push(line);
    }
  }

  if (current.length > 0) {
    blocks.push(current);
  }

  return blocks.map((block) => {
    const text = block.join("\n");
    const approvalNo = block.find((line) => /^\d{5,}$/.test(line));
    return {
      status: block[0] ?? "",
      text,
      paymentType: /\[링크결제\]/.test(text) ? "link" : /\[수기결제\]/.test(text) ? "manual" : "unknown",
      isCancelled: /결제취소|취소/.test(block[0] ?? ""),
      approvalNo
    };
  });
}

function findTransaction(
  snapshot: StoreSnapshot,
  options: { paymentType: "link" | "manual"; cancelled?: boolean; approvalNo?: string }
): TransactionBlock | undefined {
  return snapshot.transactions.find((transaction) => {
    if (transaction.paymentType !== options.paymentType) {
      return false;
    }
    if (typeof options.cancelled === "boolean" && transaction.isCancelled !== options.cancelled) {
      return false;
    }
    if (options.approvalNo && transaction.approvalNo !== options.approvalNo) {
      return false;
    }
    return true;
  }) ?? snapshot.transactions.find((transaction) => {
    if (transaction.paymentType !== options.paymentType) {
      return false;
    }
    if (typeof options.cancelled === "boolean" && transaction.isCancelled !== options.cancelled) {
      return false;
    }
    return true;
  });
}

function rowAround(bodyText: string, needle: string): string {
  const index = bodyText.indexOf(needle);
  if (index < 0) {
    return "";
  }
  return bodyText.slice(Math.max(0, index - 500), Math.min(bodyText.length, index + 800));
}

function hasDeliveryButton(text: string): boolean {
  return DELIVERY_BUTTON_PATTERN.test(text);
}

async function visibleTexts(page: Page, selector: string): Promise<string[]> {
  return page.locator(selector).evaluateAll((nodes) =>
    nodes
      .map((node) => (node.textContent ?? "").replace(/\s+/g, " ").trim())
      .filter(Boolean)
  ).catch(() => []);
}

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

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

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

async function attachBlockedEvidence(
  testInfo: TestInfo,
  pageName: string,
  role: string,
  suffix: string,
  lines: string[]
): Promise<void> {
  await attachEvidenceLog(testInfo, pageName, role, suffix, lines);
}

function subjectRuntime(environment: QaEnvironment, role: string, envRole: string, label: string): SubjectRuntime {
  return {
    role,
    label,
    envRole,
    runtimeEnv: getRuntimeQaEnv(environment, "store", envRole)
  };
}

function findPage(pages: QaPageDefinition[], name: string, fallbackPath: string): QaPageDefinition {
  return pages.find((page) => page.name === name) ?? {
    name,
    application: name.includes("어드민") ? "admin" : "store",
    path: fallbackPath
  };
}

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

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