import { expect, test, type Browser, type Locator, type Page, type TestInfo } from "@playwright/test";
import fs from "node:fs";
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 TransferTransaction {
  extTrxId?: string | null;
  status?: string | null;
  transferType?: string | null;
  isCompleted?: boolean;
}

interface TransferItem {
  transaction?: TransferTransaction;
}

interface TransferData {
  rows: TransferItem[];
  requestUrl: string;
}

interface QaState {
  data?: TransferData;
  actionableByType?: Record<TransferType, TransferItem>;
  successfulRows?: TransferItem[];
}

type TransferType = "store" | "manual" | "auto";

const ROLE = "ADMIN";
const transferTypeLabels: Record<TransferType, string> = {
  store: "스토어이체",
  manual: "수동이체",
  auto: "자동이체"
};

export function runTransferDetailsActionButtonSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const pageDefinition = findPageDefinition(checklist.pages, "이체내역");
  const runtimeEnv = getRuntimeQaEnv(options.environment, pageDefinition.application ?? "admin", ROLE);
  const blockReason = getRuntimeBlockReason(runtimeEnv);
  const state: QaState = {};

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

      test(checklist.checklist[0], async ({ browser }, testInfo) => {
        test.setTimeout(90_000);
        await withRolePage(browser, runtimeEnv, async (page) => {
          await openTransferPage(page, runtimeEnv, pageDefinition);
          state.data = await loadTransferData(runtimeEnv);
          state.actionableByType = findActionableRows(state.data.rows);
          state.successfulRows = state.data.rows.filter(isSuccessful);

          await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "current-month-list");
          await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "current-month-list", [
            `조회 기간: ${currentMonthRange().from} ~ ${currentMonthRange().to}`,
            `API 요청: ${redactUrl(state.data.requestUrl)}`,
            `전체 목록: ${state.data.rows.length}건`,
            ...Object.entries(state.actionableByType).map(([type, row]) => `${transferTypeLabels[type as TransferType]} 작업 대상 거래 ID: ${transactionId(row)} / 상태: ${row.transaction?.status ?? "-"}`),
            `성공 상태 거래 수: ${state.successfulRows.length}`
          ]);

          expect(state.data.rows.length, "당월 이체내역 목록이 조회되어야 합니다.").toBeGreaterThan(0);
          for (const type of Object.keys(transferTypeLabels) as TransferType[]) {
            expect(state.actionableByType[type], `${transferTypeLabels[type]}의 성공 이외 작업 대상 데이터가 필요합니다.`).toBeTruthy();
          }
        });
      });

      test(checklist.checklist[1], async ({ browser }, testInfo) => {
        test.setTimeout(90_000);
        await withRolePage(browser, runtimeEnv, async (page) => {
          const actionable = await ensureActionableRows(page, runtimeEnv, pageDefinition, state);
          await openTransferPage(page, runtimeEnv, pageDefinition);
          const evidence: string[] = [];

          for (const type of Object.keys(transferTypeLabels) as TransferType[]) {
            const row = requireRow(actionable[type], `${transferTypeLabels[type]} 작업 대상`);
            const screenRow = await findScreenRow(page, transactionId(row));
            const actionButton = await findActionButton(screenRow);
            evidence.push(`${transferTypeLabels[type]} / 거래 ID ${transactionId(row)} / 상태 ${row.transaction?.status ?? "-"} / 작업 버튼 ${await actionButton.isVisible().catch(() => false) ? "노출" : "미노출"}`);
            await expect(actionButton, `${transferTypeLabels[type]} 항목에는 작업 버튼이 보여야 합니다.`).toBeVisible({ timeout: 10_000 });
          }

          await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "actionable-types");
          await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "actionable-types", evidence);
        });
      });

      test(checklist.checklist[2], async ({ browser }, testInfo) => {
        test.setTimeout(90_000);
        await withRolePage(browser, runtimeEnv, async (page) => {
          const successfulRows = await ensureSuccessfulRows(page, runtimeEnv, pageDefinition, state);
          await openTransferPage(page, runtimeEnv, pageDefinition);
          const verified = successfulRows.slice(0, 10);
          if (verified.length === 0) {
            blocked("당월 성공 상태 이체내역이 없어 작업 버튼 미노출을 검증할 수 없습니다.");
          }

          const evidence: string[] = [];
          for (const row of verified) {
            const screenRow = await findScreenRow(page, transactionId(row));
            const actionButton = await findActionButton(screenRow);
            const visible = await actionButton.isVisible({ timeout: 1_500 }).catch(() => false);
            evidence.push(`성공 / 거래 ID ${transactionId(row)} / 이체구분 ${row.transaction?.transferType ?? "-"} / 작업 버튼 ${visible ? "노출됨" : "미노출"}`);
            expect(visible, `성공 상태 거래 ID ${transactionId(row)}에는 작업 버튼이 노출되면 안 됩니다.`).toBe(false);
          }

          await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "success-no-action");
          await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "success-no-action", evidence);
        });
      });

      test(checklist.checklist[3], async ({ browser }, testInfo) => {
        test.setTimeout(90_000);
        await withRolePage(browser, runtimeEnv, async (page) => {
          const actionable = await ensureActionableRows(page, runtimeEnv, pageDefinition, state);
          await openTransferPage(page, runtimeEnv, pageDefinition);
          const row = requireRow(actionable.store ?? actionable.manual ?? actionable.auto, "작업 창 확인 대상");
          const screenRow = await findScreenRow(page, transactionId(row));
          const actionButton = await findActionButton(screenRow);

          await expect(actionButton, "작업 창 확인 대상의 작업 버튼이 보여야 합니다.").toBeVisible({ timeout: 10_000 });
          await actionButton.dispatchEvent("click");
          const dialog = page.locator("[role='dialog']:visible, [data-radix-popper-content-wrapper]:visible, .swal2-container:visible").last();
          await expect(dialog, "작업 버튼을 클릭하면 이체내역 작업 창이 열려야 합니다.").toBeVisible({ timeout: 10_000 });

          await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "action-dialog");
          await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "action-dialog", [
            `작업 창 대상: ${transactionId(row)}`,
            `이체구분: ${row.transaction?.transferType ?? "-"}`,
            `상태: ${row.transaction?.status ?? "-"}`,
            `작업 창 텍스트: ${snippet(await dialog.innerText().catch(() => ""), 1200)}`
          ]);
        });
      });
    });
  });
}

async function ensureActionableRows(page: Page, runtimeEnv: RuntimeQaEnv, pageDefinition: QaPageDefinition, state: QaState): Promise<Record<TransferType, TransferItem>> {
  if (!state.actionableByType) {
    await openTransferPage(page, runtimeEnv, pageDefinition);
    state.data = await loadTransferData(runtimeEnv);
    state.actionableByType = findActionableRows(state.data.rows);
  }
  return state.actionableByType;
}

async function ensureSuccessfulRows(page: Page, runtimeEnv: RuntimeQaEnv, pageDefinition: QaPageDefinition, state: QaState): Promise<TransferItem[]> {
  if (!state.successfulRows) {
    await openTransferPage(page, runtimeEnv, pageDefinition);
    state.data = await loadTransferData(runtimeEnv);
    state.successfulRows = state.data.rows.filter(isSuccessful);
  }
  return state.successfulRows;
}

async function withRolePage(browser: Browser, runtimeEnv: RuntimeQaEnv, callback: (page: Page) => Promise<void>): Promise<void> {
  const context = await browser.newContext(runtimeEnv.storageState ? { storageState: runtimeEnv.storageState } : {});
  const page = await context.newPage();
  try {
    await callback(page);
  } finally {
    await context.close();
  }
}

async function openTransferPage(page: Page, runtimeEnv: RuntimeQaEnv, pageDefinition: QaPageDefinition): Promise<void> {
  const range = currentMonthRange();
  const targetUrl = new URL(buildPageUrl(runtimeEnv.baseUrl!, pageDefinition, runtimeEnv.tenantId!));
  targetUrl.searchParams.set("from", range.from);
  targetUrl.searchParams.set("to", range.to);
  targetUrl.searchParams.set("exactMatch", "false");

  if (!runtimeEnv.storageState) {
    await loginWithCredentials(page, runtimeEnv);
  }
  await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded" });
  await page.waitForLoadState("networkidle", { timeout: 8_000 }).catch(() => undefined);
  if (await isLoginPage(page, runtimeEnv)) {
    await loginWithCredentials(page, runtimeEnv);
    await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded" });
  }
  expect(await isLoginPage(page, runtimeEnv), "이체내역 화면 진입 전 로그인 화면을 벗어나야 합니다.").toBe(false);
  await expect(page.locator("body"), "이체내역 화면이 표시되어야 합니다.").toContainText(/이체내역|이체일시/, { timeout: 15_000 });
  await page.waitForTimeout(900);
}

async function loadTransferData(runtimeEnv: RuntimeQaEnv): Promise<TransferData> {
  const range = currentMonthRange();
  const requestUrl = new URL(`${apiBaseFromRuntime(runtimeEnv)}/api/v1/transfer/transaction`);
  requestUrl.searchParams.set("from", range.from);
  requestUrl.searchParams.set("to", range.to);
  requestUrl.searchParams.set("exactMatch", "false");
  requestUrl.searchParams.set("size", "500");
  requestUrl.searchParams.set("page", "0");

  const response = await fetch(requestUrl, { headers: nodeApiHeaders(runtimeEnv) });
  const body = await response.json().catch(() => undefined);
  if (!response.ok) {
    throw new Error(`이체내역 목록 조회 실패: ${response.status} ${JSON.stringify(body)}`);
  }
  return { rows: Array.isArray(body?.content) ? body.content : [], requestUrl: requestUrl.toString() };
}

function findActionableRows(rows: TransferItem[]): Record<TransferType, TransferItem> {
  const result = {} as Record<TransferType, TransferItem>;
  for (const type of Object.keys(transferTypeLabels) as TransferType[]) {
    const row = rows.find((candidate) => transferTypeOf(candidate) === type && !isSuccessful(candidate) && transactionId(candidate));
    if (row) result[type] = row;
  }
  return result;
}

function isSuccessful(row: TransferItem): boolean {
  return row.transaction?.isCompleted === true || /COMPLETED|SUCCESS|DONE|성공/i.test(row.transaction?.status ?? "");
}

function transferTypeOf(row: TransferItem): TransferType | undefined {
  const value = row.transaction?.transferType ?? "";
  if (/STORE|스토어/i.test(value)) return "store";
  // Transfer history API uses ADMIN for the UI's manual-transfer category.
  if (/MANUAL|ADMIN|수동/i.test(value)) return "manual";
  if (/AUTO|자동/i.test(value)) return "auto";
  return undefined;
}

async function findScreenRow(page: Page, id: string): Promise<Locator> {
  if (!id) blocked("화면 행을 찾을 거래 ID가 없습니다.");
  const text = page.getByText(id, { exact: true }).first();
  await expect(text, `거래 ID ${id}가 화면 목록에 보여야 합니다.`).toBeVisible({ timeout: 10_000 });
  const row = text.locator("xpath=ancestor::tr[1]");
  if (await row.count()) return row;
  return text.locator("xpath=ancestor::*[self::div or self::li][.//button][1]");
}

async function findActionButton(row: Locator): Promise<Locator> {
  const named = row.getByRole("button", { name: /작업|상세|편집|처리/ }).first();
  if (await named.count()) return named;
  return row.locator("button").last();
}

function transactionId(row: TransferItem): string {
  return String(row.transaction?.extTrxId ?? "").trim();
}

function requireRow(row: TransferItem | undefined, label: string): TransferItem {
  if (!row) blocked(`${label} 데이터를 찾지 못했습니다.`);
  return row;
}

function currentMonthRange(): { from: string; to: string } {
  const now = new Date();
  const formatter = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Seoul", year: "numeric", month: "2-digit", day: "2-digit" });
  const parts = Object.fromEntries(formatter.formatToParts(now).filter((part) => part.type !== "literal").map((part) => [part.type, part.value]));
  const year = parts.year;
  const month = parts.month;
  const day = parts.day;
  return {
    from: process.env.QA_3811_FROM ?? `${year}-${month}-01`,
    to: process.env.QA_3811_TO ?? `${year}-${month}-${day}`
  };
}

function apiBaseFromRuntime(runtimeEnv: RuntimeQaEnv): string {
  const url = new URL(runtimeEnv.baseUrl!);
  url.hostname = url.hostname.replace(/^(admin|store)-/, "api-");
  return url.origin;
}

function nodeApiHeaders(runtimeEnv: RuntimeQaEnv): Record<string, string> {
  const storageState = runtimeEnv.storageState;
  if (!storageState) throw new Error("이체내역 API 확인에는 ADMIN storageState가 필요합니다.");
  const raw = JSON.parse(fs.readFileSync(storageState, "utf8")) as { cookies?: Array<{ name?: string; value?: string; domain?: string }> };
  const apiHost = new URL(apiBaseFromRuntime(runtimeEnv)).hostname;
  const cookies = raw.cookies?.filter((cookie) => cookie.name && cookie.value && domainMatches(apiHost, cookie.domain ?? "")) ?? [];
  if (!cookies.length) throw new Error(`storageState에서 ${apiHost} API 쿠키를 찾지 못했습니다.`);
  return { accept: "application/json", cookie: cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; "), "x-tenant-id": runtimeEnv.tenantId ?? "" };
}

function domainMatches(host: string, domain: string): boolean {
  const normalized = domain.replace(/^\./, "");
  return host === normalized || host.endsWith(`.${normalized}`);
}

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

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

async function attachPageScreenshot(page: Page, testInfo: TestInfo, pageName: string, role: string, suffix: string): Promise<void> {
  const screenshotPath = testInfo.outputPath(`${safeFilename(pageName)}-${safeFilename(role)}-${safeFilename(suffix)}.png`);
  await page.screenshot({ path: screenshotPath, fullPage: false, timeout: 10_000 });
  await testInfo.attach(`${safeFilename(pageName)}-${safeFilename(role)}-${safeFilename(suffix)}.png`, { path: screenshotPath, contentType: "image/png" });
}

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

function snippet(value: string, max: number): string {
  const normalized = value.replace(/\s+/g, " ").trim();
  return normalized.length > max ? `${normalized.slice(0, max)}...` : normalized;
}

function redactUrl(value: string): string {
  const url = new URL(value);
  for (const key of ["token", "authorization", "cookie"]) url.searchParams.delete(key);
  return url.toString();
}

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