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

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

type TwoFactorMethod = "email" | "sms" | "pin";
type LoginStage = "primary" | "duplicate";

interface Account {
  label: string;
  username: string;
  method: TwoFactorMethod;
}

interface DuplicateState {
  firstContext: BrowserContext;
  secondContext: BrowserContext;
  firstPage: Page;
  secondPage: Page;
}

const ACCOUNTS: Record<TwoFactorMethod, Account> = {
  email: { label: "이메일 2차 인증", username: "jjm_email", method: "email" },
  sms: { label: "SMS 2차 인증", username: "jjm_sms", method: "sms" },
  pin: { label: "PIN 2차 인증", username: "jjm_pin", method: "pin" }
};

const FORCE_MODAL = /중복\s*로그인|강제\s*(로그인|접속)|기존\s*(접속|로그인).*(종료|해제)/;
const modalCopyByMethod = new Map<TwoFactorMethod, string>();

export function runDuplicateLoginSecondFactorSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const runtime = getRuntimeQaEnv(options.environment, "admin", "ADMIN");

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    for (const [index, account] of Object.values(ACCOUNTS).entries()) {
      test(checklist.checklist[index], async ({ browser }, testInfo) => {
        test.setTimeout(account.method === "pin" ? 180_000 : 600_000);
        const state = await startDuplicateLogin(browser, runtime, account);
        try {
          const surface = await inspectSurface(state.secondPage, account);
          await attachEvidence(testInfo, state.secondPage, `${account.method}-duplicate-result`, [
            `${account.label}로 일반 브라우저와 독립 브라우저에서 각각 2차 인증을 완료했습니다.`,
            ...describeLoginSurface(surface)
          ]);
          await expectForceModalOnly(state.secondPage, account);
          const modalText = await forceModalText(state.secondPage);
          modalCopyByMethod.set(account.method, modalText);
          await testInfo.attach(`${account.method}-force-modal-copy.txt`, {
            body: modalText,
            contentType: "text/plain"
          });

          const previousUrl = state.firstPage.url();
          await clickForceButton(state.secondPage, /강제\s*로그인|확인|계속/);
          await expect(state.secondPage).not.toHaveURL(/\/auth\/login/, { timeout: 20_000 });
          const stayedBeforeActivity = state.firstPage.url() === previousUrl;
          await state.firstPage.reload({ waitUntil: "domcontentloaded" });
          await expect(state.firstPage).toHaveURL(/\/auth\/login/, { timeout: 20_000 });
          await attachEvidence(testInfo, state.secondPage, `${account.method}-force-login-complete`, [
            `${account.label} 강제로그인 후 현재 브라우저 로그인 완료: 예`,
            `기존 브라우저 자동 이동 없음: ${stayedBeforeActivity ? "예" : "아니오"}`,
            "기존 브라우저 재요청 후 로그인 화면 전환: 예"
          ]);
          expect(stayedBeforeActivity, "기존 브라우저는 재요청 전까지 현재 화면에 머물러야 합니다.").toBe(true);
        } finally {
          await closeDuplicateState(state);
        }
      });
    }

    test(checklist.checklist[3], async ({}, testInfo) => {
      const email = modalCopyByMethod.get("email");
      const sms = modalCopyByMethod.get("sms");
      const pin = modalCopyByMethod.get("pin");
      await testInfo.attach("force-modal-copy-comparison.md", {
        body: `Email: ${email ?? "-"}\nSMS: ${sms ?? "-"}\nPIN: ${pin ?? "-"}\n`,
        contentType: "text/markdown"
      });
      expect(email, "Email 강제로그인 모달 문구가 기록되어야 합니다.").toBe(pin);
      expect(sms, "SMS 강제로그인 모달 문구가 PIN과 동일해야 합니다.").toBe(pin);
    });
  });
}

async function startDuplicateLogin(browser: Browser, runtime: RuntimeQaEnv, account: Account): Promise<DuplicateState> {
  const firstContext = await browser.newContext({
    userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.1 Safari/605.1.15"
  });
  const secondContext = await browser.newContext({
    userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/139.0.0.0 Safari/537.36"
  });
  const firstPage = await firstContext.newPage();
  const secondPage = await secondContext.newPage();

  try {
    await loginFully(firstPage, runtime, account, "primary");
    await submitCredentials(secondPage, runtime, account);
    await completeSecondFactor(secondPage, account, "duplicate");
    await secondPage.waitForTimeout(800);
    return { firstContext, secondContext, firstPage, secondPage };
  } catch (error) {
    await Promise.allSettled([firstContext.close(), secondContext.close()]);
    throw error;
  }
}

async function loginFully(page: Page, runtime: RuntimeQaEnv, account: Account, stage: LoginStage): Promise<void> {
  await submitCredentials(page, runtime, account);
  await completeSecondFactor(page, account, stage);
  await expect(page).not.toHaveURL(/\/auth\/login/, { timeout: 20_000 });
}

async function submitCredentials(page: Page, runtime: RuntimeQaEnv, account: Account): Promise<void> {
  const password = requiredEnv("QA_4104_ACCOUNT_PASSWORD");
  if (!runtime.loginUrl) {
    blocked("개발계 어드민 loginUrl이 필요합니다.");
  }
  await page.goto(runtime.loginUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
  await page.locator(runtime.usernameSelector).first().fill(account.username);
  await page.locator(runtime.passwordSelector).first().fill(password);
  await clickFirstVisible(page.locator(runtime.submitSelector));
}

async function completeSecondFactor(page: Page, account: Account, stage: LoginStage): Promise<void> {
  await expectSecondFactorPrompt(page, account);
  if (account.method !== "pin") {
    if (account.method === "sms") {
      await page.getByPlaceholder("회원정보에 등록된 전화번호 입력").fill(requiredEnv("QA_4104_SMS_PHONE"));
    }
    const send = page.getByRole("button", { name: /인증번호\s*(전송|재전송)/ }).last();
    await expect(send).toBeVisible({ timeout: 10_000 });
    await send.click();
    const sentNotice = page.getByText("인증번호를 보냈습니다", { exact: true });
    if (await sentNotice.waitFor({ state: "visible", timeout: 5_000 }).then(() => true).catch(() => false)) {
      await clickExactButton(page, "OK");
      await expect(sentNotice).toBeHidden({ timeout: 10_000 });
    }
  }
  const code = await secondFactorCode(account, stage);
  await secondFactorInput(page, account).fill(code);
  await clickExactButton(page, "확인");
  await page.waitForTimeout(800);
}

async function expectSecondFactorPrompt(page: Page, account: Account): Promise<void> {
  const input = secondFactorInput(page, account);
  await expect(input, `${account.label} 인증 입력 모달이 표시되어야 합니다.`).toBeVisible({ timeout: 30_000 });
}

function secondFactorInput(page: Page, account: Account) {
  const placeholder = account.method === "pin" ? "PIN 번호 입력" : account.method === "email" ? "이메일 인증 번호 입력" : "SMS 인증 번호 입력";
  return page.getByPlaceholder(placeholder);
}

async function secondFactorCode(account: Account, stage: LoginStage): Promise<string> {
  if (account.method === "pin") {
    return requiredEnv("QA_4104_PIN");
  }
  const key = `QA_4104_${account.method.toUpperCase()}_OTP_${stage.toUpperCase()}`;
  const value = process.env[key];
  if (value) {
    return validateOtp(value, key);
  }
  const directory = process.env.QA_4104_OTP_DIR;
  if (!directory) {
    blocked(`${account.label} 완료 검증에는 ${key} 또는 QA_4104_OTP_DIR이 필요합니다.`);
  }
  const otpPath = path.join(directory, `${account.method}-${stage}.txt`);
  console.log(`[qa:4104] ${account.method}-${stage} 인증번호 대기: ${otpPath}`);
  const waitMs = positiveNumber(process.env.QA_4104_OTP_WAIT_MS, 240_000);
  const deadline = Date.now() + waitMs;
  while (Date.now() < deadline) {
    if (fs.existsSync(otpPath)) {
      const otp = fs.readFileSync(otpPath, "utf8").trim();
      fs.rmSync(otpPath, { force: true });
      return validateOtp(otp, otpPath);
    }
    await new Promise((resolve) => setTimeout(resolve, 500));
  }
  blocked(`${account.label} ${stage} 인증번호를 ${Math.round(waitMs / 1000)}초 안에 받지 못했습니다.`);
}

function validateOtp(value: string, source: string): string {
  if (!/^\d{6}$/.test(value)) {
    blocked(`${source} 인증번호는 6자리 숫자여야 합니다.`);
  }
  return value;
}

async function expectForceModalOnly(page: Page, account: Account): Promise<void> {
  const surface = await inspectSurface(page, account);
  expect(surface.forceVisible, `${account.label} 중복 로그인 시 강제로그인 확인 모달이 단독으로 표시되어야 합니다.`).toBe(true);
  expect(surface.factorVisible, `${account.label} 강제로그인 확인 모달과 2차 인증 모달이 겹치면 안 됩니다.`).toBe(false);
}

async function clickForceButton(page: Page, name: RegExp): Promise<void> {
  const modal = forceModal(page);
  const button = modal.getByRole("button", { name }).last();
  await expect(button).toBeVisible({ timeout: 5_000 });
  await button.click();
}

async function forceModalText(page: Page): Promise<string> {
  const modal = forceModal(page);
  await expect(modal).toBeVisible({ timeout: 10_000 });
  return normalize(await modal.innerText());
}

function forceModal(page: Page) {
  return page.locator("[role='dialog'], .fixed.inset-0").filter({ hasText: FORCE_MODAL }).last();
}

interface LoginSurface {
  forceVisible: boolean;
  factorVisible: boolean;
  path: string;
  body: string;
}

async function inspectSurface(page: Page, account: Account): Promise<LoginSurface> {
  const body = normalize(await page.locator("body").innerText().catch(() => ""));
  const factorVisible = await secondFactorInput(page, account).isVisible().catch(() => false);
  return {
    forceVisible: await forceModal(page).isVisible().catch(() => false),
    factorVisible,
    path: new URL(page.url()).pathname,
    body: body.slice(-700)
  };
}

function describeLoginSurface(surface: LoginSurface): string[] {
  return [
    `강제로그인 확인 모달: ${surface.forceVisible ? "노출" : "미노출"}`,
    `2차 인증 입력 모달: ${surface.factorVisible ? "노출" : "미노출"}`,
    `현재 경로: ${surface.path}`,
    `화면 일부: ${surface.body || "-"}`
  ];
}

async function clickExactButton(page: Page, name: string): Promise<void> {
  const buttons = page.getByRole("button", { name, exact: true });
  const count = await buttons.count();
  for (let index = count - 1; index >= 0; index -= 1) {
    const button = buttons.nth(index);
    if (await button.isVisible().catch(() => false)) {
      await button.click();
      return;
    }
  }
  await buttons.last().click();
}

async function clickFirstVisible(locator: ReturnType<Page["locator"]>): Promise<void> {
  const count = await locator.count();
  for (let index = 0; index < count; index += 1) {
    const candidate = locator.nth(index);
    if (await candidate.isVisible().catch(() => false)) {
      await candidate.click();
      return;
    }
  }
  await locator.first().click();
}

async function attachEvidence(testInfo: TestInfo, page: Page, slug: string, lines: string[]): Promise<void> {
  await testInfo.attach(`${slug}.md`, { body: `${lines.join("\n")}\n`, contentType: "text/markdown" });
  const screenshot = testInfo.outputPath(`${slug}.png`);
  await page.screenshot({ path: screenshot, fullPage: true });
  await testInfo.attach(`${slug}.png`, { path: screenshot, contentType: "image/png" });
}

async function closeDuplicateState(state: DuplicateState): Promise<void> {
  await state.firstContext.close();
  await state.secondContext.close();
}

function requiredEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    blocked(`${key} 런타임 환경변수가 필요합니다. 실제 비밀번호와 인증번호는 저장소 및 QA 산출물에 기록하지 않습니다.`);
  }
  return value;
}

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

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

function positiveNumber(value: string | undefined, fallback: number): number {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
