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

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

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

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

interface PagePayload<T> {
  content?: T[];
  totalElements?: number;
}

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

interface Provider {
  id?: string;
  code?: string;
  name?: string;
}

interface PgContract {
  id?: string;
  name?: string;
  provider?: Provider;
  corporation?: {
    id?: string;
    code?: string;
    name?: string;
  };
}

interface PgMid {
  id?: string;
  mid?: string;
  midType?: OptionValue | string;
  contract?: PgContract;
}

interface KpdTarget {
  contractId: string;
  contractName: string;
  providerCode: string;
  providerName: string;
  midId: string;
  mid: string;
  midType: string;
}

interface HiddenFieldDefinition {
  label: string;
  aliases: string[];
  labelPatterns: RegExp[];
}

interface ControlSnapshot {
  tag: string;
  id: string;
  name: string;
  type: string;
  placeholder: string;
  ariaLabel: string;
  title: string;
  labelText: string;
  labelledByText: string;
  parentText: string;
}

const ADMIN_ROLE = "ADMIN";
const KPD_PROVIDER_CODE = "KPD";
const KPD_PROVIDER_NAME = "한국결제데이터";

const HIDDEN_FIELDS: Record<"pgMid" | "publicKey" | "appId", HiddenFieldDefinition> = {
  pgMid: {
    label: "PG MID",
    aliases: ["mid"],
    labelPatterns: [
      /(^|\n|\s)PG\s*MID(\s|$|입력|번호)/i
    ]
  },
  publicKey: {
    label: "Public Key",
    aliases: ["publickey", "midinfopublickey", "metadata-publickey"],
    labelPatterns: [
      /public\s*key/i,
      /퍼블릭\s*키/i
    ]
  },
  appId: {
    label: "App ID",
    aliases: ["appid", "app-id", "midinfoappid", "metadata-appid"],
    labelPatterns: [
      /\bapp\s*id\b/i,
      /앱\s*(아이디|ID)/i
    ]
  }
};

export function runKpdMidHiddenFieldsSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const pageDefinition = findPageDefinition(checklist.pages, "PG MID 관리");
  const runtimeEnv = getRuntimeQaEnv(options.environment, pageDefinition.application ?? "admin", ADMIN_ROLE);
  const blockReason = getRuntimeBlockReason(runtimeEnv);
  const items = checklist.checklist;

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    test.describe(`[${ADMIN_ROLE}] role`, () => {
      test.describe(pageDefinition.name, () => {
        test.skip(!!blockReason, blockReason);

        test(items[0], async ({ browser }, testInfo) => {
          test.setTimeout(75_000);
          await verifyHiddenFieldOnCreate(browser, runtimeEnv, pageDefinition, testInfo, HIDDEN_FIELDS.pgMid);
        });

        test(items[1], async ({ browser }, testInfo) => {
          test.setTimeout(75_000);
          await verifyHiddenFieldOnCreate(browser, runtimeEnv, pageDefinition, testInfo, HIDDEN_FIELDS.publicKey);
        });

        test(items[2], async ({ browser }, testInfo) => {
          test.setTimeout(75_000);
          await verifyHiddenFieldOnCreate(browser, runtimeEnv, pageDefinition, testInfo, HIDDEN_FIELDS.appId);
        });

        test(items[3], async ({ browser }, testInfo) => {
          test.setTimeout(75_000);
          await verifyHiddenFieldOnUpdate(browser, runtimeEnv, pageDefinition, testInfo, HIDDEN_FIELDS.pgMid);
        });

        test(items[4], async ({ browser }, testInfo) => {
          test.setTimeout(75_000);
          await verifyHiddenFieldOnUpdate(browser, runtimeEnv, pageDefinition, testInfo, HIDDEN_FIELDS.publicKey);
        });

        test(items[5], async ({ browser }, testInfo) => {
          test.setTimeout(75_000);
          await verifyHiddenFieldOnUpdate(browser, runtimeEnv, pageDefinition, testInfo, HIDDEN_FIELDS.appId);
        });
      });
    });
  });
}

async function verifyHiddenFieldOnCreate(
  browser: Browser,
  runtimeEnv: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  testInfo: TestInfo,
  field: HiddenFieldDefinition
): Promise<void> {
  await withRolePage(browser, runtimeEnv, async (page) => {
    const target = await openKpdCreatePage(page, runtimeEnv);
    await verifyHiddenField(page, pageDefinition, testInfo, field, target, "create");
  });
}

async function verifyHiddenFieldOnUpdate(
  browser: Browser,
  runtimeEnv: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  testInfo: TestInfo,
  field: HiddenFieldDefinition
): Promise<void> {
  await withRolePage(browser, runtimeEnv, async (page) => {
    const target = await openKpdUpdatePage(page, runtimeEnv);
    await verifyHiddenField(page, pageDefinition, testInfo, field, target, "update");
  });
}

async function verifyHiddenField(
  page: Page,
  pageDefinition: QaPageDefinition,
  testInfo: TestInfo,
  field: HiddenFieldDefinition,
  target: KpdTarget,
  surface: "create" | "update"
): Promise<void> {
  const controls = await collectVisibleControls(page);
  const matches = controls.filter((control) => matchesHiddenField(control, field));
  const surfaceLabel = surface === "create" ? "생성" : "수정";
  const slug = `${surface}-${safeFilename(field.label).toLowerCase()}`;

  await attachEvidenceLog(testInfo, pageDefinition, ADMIN_ROLE, slug, [
    `화면: 한국결제데이터 MID ${surfaceLabel} 화면`,
    `PG 계약: ${target.contractName} (${target.contractId})`,
    `PG: ${target.providerName} (${target.providerCode})`,
    `MID 타입: ${target.midType}`,
    `수정 대상 MID: ${surface === "update" ? `${target.mid} (${target.midId})` : "생성 화면 검증"}`,
    `검증 입력박스: ${field.label}`,
    `매칭된 visible 입력박스 수: ${matches.length}`,
    ...matches.map((control, index) => `${index + 1}. ${formatControl(control)}`)
  ]);
  await attachPageScreenshot(page, testInfo, pageDefinition, ADMIN_ROLE, slug);

  expect(
    matches.map(formatControl),
    `한국결제데이터 MID ${surfaceLabel} 화면에 ${field.label} 입력박스가 보이면 안 됩니다.`
  ).toEqual([]);
}

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 ensureAuthenticated(page: Page, runtimeEnv: RuntimeQaEnv): Promise<void> {
  const url = buildAdminUrl(runtimeEnv, "/");
  await openAuthenticatedUrl(page, url.toString(), runtimeEnv, "전산 홈");
}

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

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

  if (await isLoginPage(page, runtimeEnv)) {
    await loginWithCredentials(page, runtimeEnv);
    await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
    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 openKpdCreatePage(page: Page, runtimeEnv: RuntimeQaEnv): Promise<KpdTarget> {
  await ensureAuthenticated(page, runtimeEnv);
  const target = await resolveKpdTarget(page, runtimeEnv);
  const url = buildAdminUrl(runtimeEnv, "/business/pg-mid-manage/create");
  url.searchParams.set("midType", target.midType);
  await openAuthenticatedUrl(page, url.toString(), runtimeEnv, "PG 한국결제데이터 MID 생성");
  await expect(page.locator("body"), "PG MID 생성/등록 화면이 표시되어야 합니다.").toContainText(/PG\s*MID\s*관리|PG\s*MID.*(생성|등록)|MID\s*타입/);
  await selectContract(page, target.contractName);
  await expect(page.locator("body"), "한국결제데이터 계약명이 선택된 상태로 보여야 합니다.").toContainText(target.contractName);
  await page.waitForTimeout(800);
  return target;
}

async function openKpdUpdatePage(page: Page, runtimeEnv: RuntimeQaEnv): Promise<KpdTarget> {
  await ensureAuthenticated(page, runtimeEnv);
  const target = await resolveKpdTarget(page, runtimeEnv);
  const url = buildAdminUrl(runtimeEnv, `/business/pg-mid-manage/update/${target.midId}`);
  await openAuthenticatedUrl(page, url.toString(), runtimeEnv, "PG 한국결제데이터 MID 수정");
  await expect(page.locator("body"), "PG MID 수정 화면이 표시되어야 합니다.").toContainText(/PG\s*MID\s*(관리\s*)?수정|MID\s*수정/);
  await expect(page.locator("body"), "수정 화면에서 한국결제데이터 계약명이 보여야 합니다.").toContainText(target.contractName);
  await page.waitForTimeout(800);
  return target;
}

async function resolveKpdTarget(page: Page, runtimeEnv: RuntimeQaEnv): Promise<KpdTarget> {
  const mids = await loadMidRows(page, runtimeEnv);
  const kpdMids = mids.filter((mid) => mid.id && mid.mid && isKpdProvider(mid.contract?.provider));
  const preferredMid =
    kpdMids.find((mid) => getOptionValue(mid.midType) === "KEYIN" && /수기/.test(mid.contract?.name ?? "")) ??
    kpdMids.find((mid) => getOptionValue(mid.midType) === "KEYIN") ??
    kpdMids[0];

  if (!preferredMid?.id || !preferredMid.mid || !preferredMid.contract?.id || !preferredMid.contract.name) {
    blocked("한국결제데이터 MID 수정 검증에 사용할 기존 PG MID 데이터가 없습니다.");
  }

  const provider = preferredMid.contract.provider ?? {};
  return {
    contractId: preferredMid.contract.id,
    contractName: preferredMid.contract.name,
    providerCode: provider.code ?? "",
    providerName: provider.name ?? "",
    midId: preferredMid.id,
    mid: preferredMid.mid,
    midType: getOptionValue(preferredMid.midType) ?? "KEYIN"
  };
}

async function loadMidRows(page: Page, runtimeEnv: RuntimeQaEnv): Promise<PgMid[]> {
  const result = await callApi<PayloadResponse<PagePayload<PgMid>>>(
    page,
    runtimeEnv,
    "/v1/pgmids/manage?page=0&size=300"
  );
  expect(result.ok, formatApiFailure("PG MID 관리 목록 조회", result)).toBe(true);
  return result.json.payload?.content ?? [];
}

async function selectContract(page: Page, contractName: string): Promise<void> {
  if (await page.getByText(contractName, { exact: false }).first().isVisible({ timeout: 1_000 }).catch(() => false)) {
    return;
  }

  const control = await findMainContractControl(page);
  if (!control) {
    blocked("PG 계약 선택 컨트롤을 찾지 못했습니다.");
  }

  await control.click({ position: { x: 300, y: 20 }, force: true });
  await page.waitForTimeout(300);
  const clickedText = await clickContractOptionByDom(page, contractName);
  if (!clickedText) {
    blocked(`한국결제데이터 계약 옵션을 드롭다운에서 찾지 못했습니다: ${contractName}`);
  }
  await waitForNavigationToSettle(page);
  await expect(page.locator("body"), `한국결제데이터 계약이 선택되어야 합니다: ${contractName}`).toContainText(contractName);
}

async function findMainContractControl(page: Page) {
  const controls = page.locator("div.relative, [role='combobox']").filter({ hasText: /계약을 선택하세요|선택된 계약|PG\s*계약/ });
  const count = await controls.count();
  let fallback;

  for (let index = 0; index < count; index += 1) {
    const control = controls.nth(index);
    const box = await control.boundingBox().catch(() => null);
    if (!box || box.width < 200 || box.height < 20) {
      continue;
    }
    if (box.x > 240 && box.y > 250) {
      return control;
    }
    fallback ??= control;
  }

  return fallback;
}

async function clickContractOptionByDom(page: Page, contractName: string): Promise<string | undefined> {
  return page.evaluate((name) => {
    const isClickableCandidate = (element: HTMLElement) => {
      const text = element.innerText?.trim() ?? "";
      if (!text.includes(name)) {
        return false;
      }
      const box = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return (
        box.width > 0 &&
        box.height > 0 &&
        style.display !== "none" &&
        style.visibility !== "hidden" &&
        !/fixed inset-0.*opacity-0/.test(String(element.className ?? ""))
      );
    };
    const candidates = Array.from(document.querySelectorAll<HTMLElement>("div, li, button, span, p"))
      .filter(isClickableCandidate)
      .sort((left, right) => (left.innerText ?? "").length - (right.innerText ?? "").length);
    const target = candidates[0];
    if (!target) {
      return undefined;
    }
    target.scrollIntoView({ block: "center", inline: "nearest" });
    target.click();
    return target.innerText?.trim().slice(0, 200);
  }, contractName);
}

async function collectVisibleControls(page: Page): Promise<ControlSnapshot[]> {
  return page.locator("body").evaluate((body) => {
    const isVisible = (element: HTMLElement) => {
      const style = window.getComputedStyle(element);
      const box = element.getBoundingClientRect();
      return (
        style.display !== "none" &&
        style.visibility !== "hidden" &&
        Number(style.opacity) !== 0 &&
        box.width > 0 &&
        box.height > 0
      );
    };
    const readLabelledBy = (element: HTMLElement) => {
      const ids = (element.getAttribute("aria-labelledby") ?? "").split(/\s+/).filter(Boolean);
      return ids
        .map((id) => document.getElementById(id)?.textContent?.trim() ?? "")
        .filter(Boolean)
        .join(" ");
    };
    const readLabelFor = (element: HTMLElement) => {
      const id = element.id;
      if (!id) {
        return "";
      }
      return Array.from(document.querySelectorAll<HTMLLabelElement>(`label[for="${CSS.escape(id)}"]`))
        .map((label) => label.innerText.trim())
        .filter(Boolean)
        .join(" ");
    };
    return Array.from(body.querySelectorAll<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>("input, textarea, select"))
      .filter((element) => isVisible(element))
      .filter((element) => (element.getAttribute("type") ?? "").toLowerCase() !== "hidden")
      .map((element) => ({
        tag: element.tagName.toLowerCase(),
        id: element.id ?? "",
        name: element.getAttribute("name") ?? "",
        type: element.getAttribute("type") ?? "",
        placeholder: element.getAttribute("placeholder") ?? "",
        ariaLabel: element.getAttribute("aria-label") ?? "",
        title: element.getAttribute("title") ?? "",
        labelText: [readLabelFor(element), element.closest("label")?.textContent?.trim() ?? ""].filter(Boolean).join(" "),
        labelledByText: readLabelledBy(element),
        parentText: element.parentElement?.innerText?.trim().slice(0, 180) ?? ""
      }));
  });
}

function matchesHiddenField(control: ControlSnapshot, field: HiddenFieldDefinition): boolean {
  const idOrName = [control.id, control.name]
    .map(normalizeKey)
    .filter(Boolean);
  if (idOrName.some((value) => field.aliases.map(normalizeKey).includes(value))) {
    return true;
  }

  const searchableText = [
    control.placeholder,
    control.ariaLabel,
    control.title,
    control.labelText,
    control.labelledByText,
    control.parentText
  ].filter(Boolean).join("\n");
  return field.labelPatterns.some((pattern) => pattern.test(searchableText));
}

function formatControl(control: ControlSnapshot): string {
  return [
    `${control.tag}${control.type ? `[type=${control.type}]` : ""}`,
    control.id ? `id=${control.id}` : "",
    control.name ? `name=${control.name}` : "",
    control.placeholder ? `placeholder=${control.placeholder}` : "",
    control.labelText ? `label=${control.labelText}` : "",
    control.parentText ? `parent=${control.parentText}` : ""
  ].filter(Boolean).join(" / ");
}

function normalizeKey(value: string): string {
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
}

function isKpdProvider(provider: Provider | undefined): boolean {
  return (
    provider?.code === KPD_PROVIDER_CODE ||
    provider?.name === KPD_PROVIDER_NAME ||
    /한국\s*결제\s*데이터/.test(provider?.name ?? "")
  );
}

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

async function callApi<T>(
  page: Page,
  runtimeEnv: RuntimeQaEnv,
  apiPath: string
): Promise<ApiResult<T>> {
  const response = await page.context().request.get(`${resolveApiBase(runtimeEnv)}${apiPath}`, {
    headers: runtimeEnv.tenantId ? { "X-TENANT-ID": runtimeEnv.tenantId } : undefined
  });
  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>
    : {};
  return {
    status: response.status(),
    ok: response.ok(),
    json: json as T,
    message: String(result.message ?? asRecord.message ?? "")
  };
}

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 buildAdminUrl(runtimeEnv: RuntimeQaEnv, pathname: string): URL {
  const url = new URL(pathname, ensureTrailingSlash(runtimeEnv.baseUrl!));
  if (runtimeEnv.tenantId) {
    url.searchParams.set("tenantId", runtimeEnv.tenantId);
  }
  return url;
}

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

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

function blocked(message: string): never {
  test.skip(true, `BLOCKED: ${message}`);
  throw new Error(`BLOCKED: ${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 safeFilename(value: string): string {
  return value
    .replace(/[^a-zA-Z0-9가-힣_.-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80);
}
