import {
  expect,
  test,
  type Browser,
  type BrowserContext,
  type Page,
  type Response,
  type TestInfo
} from "@playwright/test";
import { createHash } from "node:crypto";
import { isLoginPage, loginWithCredentials } from "../auth";
import { loadChecklist, loadIssueMetadata } from "../checklist";
import { getRuntimeQaEnv, type RuntimeQaEnv } from "../env";
import type { QaApplication, QaChecklist, QaEnvironment, QaPageDefinition } from "../types";
import { buildPageUrl } from "../url";

test.describe.configure({ mode: "serial" });
test.use({
  trace: process.env.QA_PUBLISH_TRACE === "1" ? "on" : "retain-on-failure",
  video: "off",
  screenshot: "off"
});

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

interface Fixtures {
  targetRoles: string[];
}

interface AuthMePayload {
  loginId?: string;
  role?: string;
  permissions?: string[];
}

interface CapturedAuthResponse {
  page: Page;
  response: Response;
  payload?: AuthMePayload;
}

interface AuthCollector {
  entries: CapturedAuthResponse[];
  dispose: () => void;
}

interface SurfaceSnapshot {
  origin: string;
  pathname: string;
  title: string;
  role: string;
  loginId: string;
  permissions: string[];
  permissionHash: string;
  menuRoutes: string[];
  menuLabels: string[];
  virtualBannerVisible: boolean;
}

interface VirtualSession {
  context: BrowserContext;
  sourcePage: Page;
  popup: Page;
  targetRowText: string;
  confirmText: string;
  surface: SurfaceSnapshot;
}

const DEFAULT_FIXTURES: Fixtures = {
  targetRoles: ["TA", "CO", "BO", "VE"]
};

const ROLE_LABELS: Record<string, RegExp> = {
  TA: /관리자/,
  CO: /법인/,
  BO: /영업점/,
  VE: /가맹점/
};

export function runAdminVirtualLoginSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const accountPage = findPage(checklist.pages, "계정관리");
  const fixtures = {
    ...DEFAULT_FIXTURES,
    ...((checklist as QaChecklist & { fixtures?: Partial<Fixtures> }).fixtures ?? {})
  };
  const adminRuntime = getRuntimeQaEnv(options.environment, "admin", "ADMIN");
  const adminBlockReason = getRuntimeBlockReason(adminRuntime, true);

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    for (const role of fixtures.targetRoles.map((value) => value.toUpperCase())) {
      const application: QaApplication = role === "VE" ? "store" : "admin";
      const normalRuntime = getRuntimeQaEnv(options.environment, application, role);
      const normalBlockReason = getRuntimeBlockReason(normalRuntime, application === "admin");

      test.describe(`[ADMIN -> ${role}] 가상로그인`, () => {
        test.skip(!!adminBlockReason, adminBlockReason);

        test(`${checklist.checklist[0]} (${role})`, async ({ browser }, testInfo) => {
          test.setTimeout(150_000);
          const virtual = await launchVirtualSession(browser, adminRuntime, normalRuntime, accountPage, role, testInfo);
          try {
            await attachVirtualEvidence(testInfo, virtual, role, normalRuntime, "virtual-login");

            expect(virtual.confirmText, `${role} 가상로그인 확인 모달 문구가 표시되어야 합니다.`)
              .toContain("대상 사용자 권한으로 전환됩니다");
            expect(virtual.surface.role, `${role} 가상로그인 세션의 실제 role이 대상 role과 같아야 합니다.`).toBe(role);
            expect(
              virtual.surface.loginId,
              `${role} 가상로그인 세션의 사용자가 검색한 대상 계정과 같아야 합니다.`
            ).toBe(normalRuntime.username);
            expect(
              virtual.surface.origin,
              `${role} 가상로그인 목적지가 일반 로그인 애플리케이션과 같아야 합니다.`
            ).toBe(new URL(normalRuntime.baseUrl!).origin);
            expect(virtual.surface.permissions.length, `${role} 가상로그인 권한 목록이 로드되어야 합니다.`)
              .toBeGreaterThan(0);
          } finally {
            await virtual.context.close();
          }
        });

        test(`${checklist.checklist[1]} (${role})`, async ({ browser }, testInfo) => {
          test.setTimeout(180_000);
          test.skip(!!normalBlockReason, normalBlockReason);

          const virtual = await launchVirtualSession(browser, adminRuntime, normalRuntime, accountPage, role, testInfo);
          let normal: { context: BrowserContext; page: Page; surface: SurfaceSnapshot } | undefined;
          try {
            normal = await openNormalSession(browser, normalRuntime);
            await attachComparisonEvidence(testInfo, virtual, normal, role);

            expect(virtual.surface.role, `${role} 가상로그인 role이 일반 로그인 role과 같아야 합니다.`)
              .toBe(normal.surface.role);
            expect(virtual.surface.loginId, `${role} 가상로그인 사용자가 일반 로그인 사용자와 같아야 합니다.`)
              .toBe(normal.surface.loginId);
            expect(
              virtual.surface.permissionHash,
              `${role} 가상로그인 권한 집합이 일반 로그인 권한 집합과 같아야 합니다.`
            ).toBe(normal.surface.permissionHash);
            expect(
              virtual.surface.menuRoutes,
              `${role} 가상로그인 메뉴 경로가 일반 로그인 메뉴 경로와 같아야 합니다.`
            ).toEqual(normal.surface.menuRoutes);
            expect(
              virtual.surface.menuLabels,
              `${role} 가상로그인 메뉴 문구가 일반 로그인 메뉴 문구와 같아야 합니다.`
            ).toEqual(normal.surface.menuLabels);
            expect(virtual.surface.pathname, `${role} 가상로그인 첫 화면이 일반 로그인 첫 화면과 같아야 합니다.`)
              .toBe(normal.surface.pathname);
            expect(virtual.surface.title, `${role} 가상로그인 화면 제목이 일반 로그인 화면 제목과 같아야 합니다.`)
              .toBe(normal.surface.title);
          } finally {
            await normal?.context.close();
            await virtual.context.close();
          }
        });
      });
    }
  });
}

async function launchVirtualSession(
  browser: Browser,
  adminRuntime: RuntimeQaEnv,
  targetRuntime: RuntimeQaEnv,
  accountPage: QaPageDefinition,
  role: string,
  testInfo: TestInfo
): Promise<VirtualSession> {
  const context = await browser.newContext({
    ...(adminRuntime.storageState ? { storageState: adminRuntime.storageState } : {}),
    viewport: { width: 1440, height: 900 }
  });
  const collector = createAuthCollector(context);
  const sourcePage = await context.newPage();

  try {
    await openAccountPage(sourcePage, adminRuntime, accountPage);
    const row = await searchTargetAccount(sourcePage, targetRuntime.username!, role);
    const targetRowText = normalizeText(await row.innerText());
    const virtualButton = row.getByRole("button", { name: "가상 로그인", exact: true });
    await expect(virtualButton, `${role} 계정 행에 가상 로그인 버튼이 표시되어야 합니다.`).toBeVisible();
    await expect(virtualButton, `${role} 계정 행의 가상 로그인 버튼이 활성화되어야 합니다.`).toBeEnabled();
    await virtualButton.click();

    const locationDialog = sourcePage.getByRole("dialog", { name: "가상 로그인 위치를 선택해주세요." });
    const needsLocationChoice = await locationDialog
      .waitFor({ state: "visible", timeout: 2_000 })
      .then(() => true)
      .catch(() => false);
    if (needsLocationChoice) {
      await testInfo.attach(`${safeFilename(role)}-virtual-login-location.png`, {
        body: await sourcePage.screenshot({ fullPage: false }),
        contentType: "image/png"
      });
      const destination = role === "VE" ? "스토어 로그인" : "전산 로그인";
      await locationDialog.getByRole("button", { name: destination, exact: true }).click();
    }

    const confirmDialog = sourcePage.getByRole("dialog").filter({ hasText: "가상 로그인을 시작하시겠습니까?" });
    await expect(confirmDialog, `${role} 가상로그인 확인 모달이 표시되어야 합니다.`).toBeVisible({ timeout: 8_000 });
    const confirmText = normalizeText(await confirmDialog.innerText());
    await testInfo.attach(`${safeFilename(role)}-virtual-login-confirm.png`, {
      body: await sourcePage.screenshot({ fullPage: false }),
      contentType: "image/png"
    });
    await confirmDialog.getByRole("button", { name: "시작", exact: true }).click();

    const pinTitle = sourcePage.getByText("PIN 인증", { exact: true });
    await expect(pinTitle, `${role} 가상로그인 시작 시 ADMIN PIN 인증이 표시되어야 합니다.`).toBeVisible({ timeout: 10_000 });
    const pinPanel = pinTitle.locator("..");
    if (!adminRuntime.twoFactorCode) {
      blocked("ADMIN 가상로그인 PIN 인증번호가 설정되지 않았습니다.");
    }
    await pinPanel.locator("input").fill(adminRuntime.twoFactorCode);
    const popupPromise = context.waitForEvent("page", { timeout: 20_000 });
    await pinPanel.getByRole("button", { name: "확인", exact: true }).click();
    const popup = await popupPromise;
    await waitForSettle(popup);

    const me = await waitForAuthMe(collector, popup);
    const surface = await collectSurface(popup, me);
    const expectedRoleLabel = ROLE_LABELS[role];
    if (expectedRoleLabel) {
      expect(targetRowText, `${role} 검색 결과 행의 구분이 대상 role과 일치해야 합니다.`).toMatch(expectedRoleLabel);
    }

    return { context, sourcePage, popup, targetRowText, confirmText, surface };
  } catch (error) {
    collector.dispose();
    await context.close();
    throw error;
  } finally {
    collector.dispose();
  }
}

async function openNormalSession(
  browser: Browser,
  runtime: RuntimeQaEnv
): Promise<{ context: BrowserContext; page: Page; surface: SurfaceSnapshot }> {
  const context = await browser.newContext({
    ...(runtime.storageState ? { storageState: runtime.storageState } : {}),
    viewport: { width: 1440, height: 900 }
  });
  const collector = createAuthCollector(context);
  const page = await context.newPage();
  try {
    const targetUrl = new URL("/", runtime.baseUrl!);
    if (runtime.tenantId && runtime.application === "admin") {
      targetUrl.searchParams.set("tenantId", runtime.tenantId);
    }
    await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded", timeout: 60_000 });
    await waitForSettle(page);
    if (await isLoginPage(page, runtime)) {
      await loginWithCredentials(page, runtime);
      await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded", timeout: 60_000 });
      await waitForSettle(page);
    }
    expect(await isLoginPage(page, runtime), `${runtime.role} 일반 로그인 세션이 유지되어야 합니다.`).toBe(false);
    const me = await waitForAuthMe(collector, page);
    const surface = await collectSurface(page, me);
    return { context, page, surface };
  } catch (error) {
    await context.close();
    throw error;
  } finally {
    collector.dispose();
  }
}

async function openAccountPage(
  page: Page,
  runtime: RuntimeQaEnv,
  pageDefinition: QaPageDefinition
): Promise<void> {
  if (!runtime.storageState) {
    await loginWithCredentials(page, runtime);
  }
  const targetUrl = buildPageUrl(runtime.baseUrl!, pageDefinition, runtime.tenantId!);
  await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
  await waitForSettle(page);
  if (await isLoginPage(page, runtime)) {
    await loginWithCredentials(page, runtime);
    await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
    await waitForSettle(page);
  }
  expect(await isLoginPage(page, runtime), "ADMIN 계정관리 로그인 세션이 유지되어야 합니다.").toBe(false);
  await expect(page.locator("body"), "계정관리 화면이 표시되어야 합니다.").toContainText("가상로그인", {
    timeout: 20_000
  });
}

async function searchTargetAccount(page: Page, loginId: string, role: string) {
  const input = page.getByRole("textbox", { name: "아이디", exact: true });
  await expect(input, "계정관리 아이디 검색 필드가 표시되어야 합니다.").toBeVisible();
  await input.fill(loginId);

  const responsePromise = page.waitForResponse((response) => {
    if (response.request().method() !== "GET" || !response.url().includes("/api/v2/users")) {
      return false;
    }
    return new URL(response.url()).searchParams.get("loginId") === loginId;
  }, { timeout: 30_000 });
  await page.getByRole("button", { name: "검색", exact: true }).filter({ visible: true }).click();
  const response = await responsePromise;
  expect(response.status(), `${role} 계정 검색 API가 성공해야 합니다.`).toBeLessThan(400);
  await waitForSettle(page);

  const grids = page.getByRole("grid");
  let selectedRows = grids.first().locator('[role="row"][data-id]');
  let smallestCount = Number.POSITIVE_INFINITY;
  for (let index = 0; index < await grids.count(); index += 1) {
    const candidateRows = grids.nth(index).locator('[role="row"][data-id]');
    const count = await candidateRows.count();
    if (count > 0 && count < smallestCount) {
      selectedRows = candidateRows;
      smallestCount = count;
    }
  }
  expect(smallestCount, `${role} 로그인아이디 검색 결과가 1건이어야 합니다.`).toBe(1);
  return selectedRows.first();
}

function createAuthCollector(context: BrowserContext): AuthCollector {
  const entries: CapturedAuthResponse[] = [];
  const listener = async (response: Response) => {
    const pathname = new URL(response.url()).pathname;
    const isAdminAuth = pathname === "/api/v1/auth/me";
    const isStoreAuth = pathname === "/api/v1/store/users/auth";
    const isStoreUserInfo = /^\/api\/v1\/store\/users\/[^/]+\/info$/.test(pathname);
    if (!isAdminAuth && !isStoreAuth && !isStoreUserInfo) {
      return;
    }
    const body = await response.json().catch(() => undefined) as {
      payload?: AuthMePayload & {
        user?: AuthMePayload | { user?: AuthMePayload };
      };
    } | undefined;
    const storeUser = isStoreUserInfo && body?.payload?.user
      ? ("user" in body.payload.user ? body.payload.user.user : body.payload.user)
      : undefined;
    entries.push({
      page: response.frame().page(),
      response,
      payload: (storeUser ?? body?.payload) as AuthMePayload | undefined
    });
  };
  context.on("response", listener);
  return {
    entries,
    dispose: () => context.off("response", listener)
  };
}

async function waitForAuthMe(collector: AuthCollector, page: Page): Promise<AuthMePayload> {
  const deadline = Date.now() + 20_000;
  while (Date.now() < deadline) {
    const combined = collector.entries
      .filter((entry) => entry.page === page && entry.response.status() === 200 && entry.payload)
      .reduce<AuthMePayload>((result, entry) => ({ ...result, ...entry.payload }), {});
    if (combined.role && combined.loginId && combined.permissions) {
      return combined;
    }
    await page.waitForTimeout(250);
  }
  blocked(`가상/일반 로그인 화면의 사용자·권한 응답을 확인하지 못했습니다: ${page.url()}`);
}

async function collectSurface(page: Page, me: AuthMePayload): Promise<SurfaceSnapshot> {
  const url = new URL(page.url());
  const links = await page.locator("a:visible").evaluateAll((elements) => elements.map((element) => ({
    text: (element.textContent ?? "").replace(/\s+/g, " ").trim(),
    href: element.getAttribute("href") ?? ""
  })));
  const menuRoutes = uniqueSorted(links
    .map((link) => normalizeRoute(link.href, url.origin))
    .filter((route): route is string => !!route));
  const menuLabels = uniqueSorted(links
    .map((link) => normalizeText(link.text))
    .filter((text) => text.length > 0 && text.length <= 80));
  const permissions = uniqueSorted(me.permissions ?? []);
  const body = normalizeText(await page.locator("body").innerText());

  return {
    origin: url.origin,
    pathname: url.pathname,
    title: await page.title(),
    role: (me.role ?? "").toUpperCase(),
    loginId: me.loginId ?? "",
    permissions,
    permissionHash: sha256(permissions.join("\n")),
    menuRoutes,
    menuLabels,
    virtualBannerVisible: body.includes("가상 로그인 중")
  };
}

async function attachVirtualEvidence(
  testInfo: TestInfo,
  virtual: VirtualSession,
  role: string,
  targetRuntime: RuntimeQaEnv,
  action: string
): Promise<void> {
  const lines = [
    `대상 role: ${role}`,
    `대상 계정 일치: ${virtual.surface.loginId === targetRuntime.username ? "예" : "아니오"}`,
    `검색 결과 행: ${snippet(virtual.targetRowText, 700)}`,
    `확인 모달: ${virtual.confirmText}`,
    `가상로그인 URL: ${redactUrl(virtual.popup.url())}`,
    `실제 role: ${virtual.surface.role}`,
    `가상 로그인 중 안내: ${virtual.surface.virtualBannerVisible ? "노출" : "미노출"}`,
    `권한 수/해시: ${virtual.surface.permissions.length} / ${virtual.surface.permissionHash}`,
    `메뉴 경로: ${virtual.surface.menuRoutes.join(" | ")}`,
    "결론: ADMIN 계정관리에서 대상 계정 가상로그인을 시작해 대상 role의 애플리케이션으로 전환되었습니다."
  ];
  await testInfo.attach(`${safeFilename(role)}-${safeFilename(action)}.md`, {
    body: `${lines.join("\n")}\n`,
    contentType: "text/markdown"
  });
  await testInfo.attach(`${safeFilename(role)}-${safeFilename(action)}.png`, {
    body: await virtual.popup.screenshot({ fullPage: false }),
    contentType: "image/png"
  });
}

async function attachComparisonEvidence(
  testInfo: TestInfo,
  virtual: VirtualSession,
  normal: { page: Page; surface: SurfaceSnapshot },
  role: string
): Promise<void> {
  const lines = [
    `대상 role: ${role}`,
    `가상/일반 사용자 일치: ${virtual.surface.loginId === normal.surface.loginId ? "예" : "아니오"}`,
    `가상/일반 role: ${virtual.surface.role} / ${normal.surface.role}`,
    `가상/일반 URL: ${redactUrl(virtual.popup.url())} / ${redactUrl(normal.page.url())}`,
    `가상/일반 화면 제목: ${virtual.surface.title} / ${normal.surface.title}`,
    `가상/일반 권한 수: ${virtual.surface.permissions.length} / ${normal.surface.permissions.length}`,
    `가상 권한 해시: ${virtual.surface.permissionHash}`,
    `일반 권한 해시: ${normal.surface.permissionHash}`,
    `가상 메뉴 경로: ${virtual.surface.menuRoutes.join(" | ")}`,
    `일반 메뉴 경로: ${normal.surface.menuRoutes.join(" | ")}`,
    `가상 메뉴 문구: ${virtual.surface.menuLabels.join(" | ")}`,
    `일반 메뉴 문구: ${normal.surface.menuLabels.join(" | ")}`,
    "결론: 가상로그인과 일반 로그인에서 사용자, role, 권한 집합, 첫 화면, 메뉴 경로와 문구가 일치했습니다."
  ];
  await testInfo.attach(`${safeFilename(role)}-virtual-normal-comparison.md`, {
    body: `${lines.join("\n")}\n`,
    contentType: "text/markdown"
  });
  await testInfo.attach(`${safeFilename(role)}-virtual-screen.png`, {
    body: await virtual.popup.screenshot({ fullPage: false }),
    contentType: "image/png"
  });
  await testInfo.attach(`${safeFilename(role)}-normal-screen.png`, {
    body: await normal.page.screenshot({ fullPage: false }),
    contentType: "image/png"
  });
}

function getRuntimeBlockReason(runtime: RuntimeQaEnv, requireTenant: boolean): string | undefined {
  if (!runtime.baseUrl || !runtime.loginUrl) {
    return `BLOCKED: ${runtime.application}/${runtime.environment}/${runtime.role} base/login URL이 필요합니다.`;
  }
  if (requireTenant && !runtime.tenantId) {
    return `BLOCKED: ${runtime.application}/${runtime.environment}/${runtime.role} tenantId가 필요합니다.`;
  }
  if (!runtime.storageState && (!runtime.username || !runtime.password)) {
    return `BLOCKED: ${runtime.application}/${runtime.environment}/${runtime.role} 로그인 정보가 필요합니다.`;
  }
  if (!runtime.username) {
    return `BLOCKED: ${runtime.application}/${runtime.environment}/${runtime.role} 대상 로그인아이디가 필요합니다.`;
  }
  return undefined;
}

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

function normalizeRoute(rawHref: string, origin: string): string | undefined {
  if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("javascript:")) {
    return undefined;
  }
  try {
    const url = new URL(rawHref, origin);
    if (url.origin !== origin || /\/(auth|login)(\/|$)/i.test(url.pathname)) {
      return undefined;
    }
    return url.pathname.replace(/\/$/, "") || "/";
  } catch {
    return undefined;
  }
}

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

function uniqueSorted(values: string[]): string[] {
  return [...new Set(values)].sort((left, right) => left.localeCompare(right, "ko"));
}

function sha256(value: string): string {
  return createHash("sha256").update(value).digest("hex");
}

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

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

function redactUrl(rawUrl: string): string {
  try {
    const url = new URL(rawUrl);
    for (const key of ["tenantId", "token", "code", "launchId"]) {
      if (url.searchParams.has(key)) {
        url.searchParams.set(key, "[redacted]");
      }
    }
    return url.toString();
  } catch {
    return rawUrl;
  }
}

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

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