import {
  expect,
  test,
  type Browser,
  type BrowserContext,
  type Download,
  type Locator,
  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 { extractDownloadRows } from "../downloadText";
import { getRuntimeQaEnv, type RuntimeQaEnv } from "../env";
import type { QaEnvironment, QaPageDefinition } from "../types";
import { buildPageUrl } from "../url";

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

interface TableSnapshot {
  headers: string[];
  rows: string[][];
  totalCount?: number;
  bodyText: string;
}

interface ParsedExcel {
  fileName: string;
  rows: string[][];
}

const TARGET_ROLES = ["ADMIN", "TA"];
const SEARCH_REQUEST_PATTERN = "/api/v1/deposit-checks";
const REQUIRED_SCREEN_HEADERS = ["입금액", "입금일시", "상태"];
const MATCH_HEADERS = ["업체", "펌뱅킹명", "제목", "입금액", "입금일시", "상태"];

export function runDepositConfirmationExcelSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const pageDefinition = requirePage(checklist.pages, "입금확인");

  const roles = process.env.QA_ROLE
    ? TARGET_ROLES.filter((role) => role === process.env.QA_ROLE?.trim().toUpperCase())
    : TARGET_ROLES;

  for (const role of roles) {
    const runtimeEnv = getRuntimeQaEnv(options.environment, pageDefinition.application ?? "admin", role);
    const blockReason = runtimeBlockReason(runtimeEnv);
    const checklistIndex = role === "ADMIN" ? 2 : 3;
    const checklistItem = checklist.checklist.find((item) =>
      new RegExp(`^${role}\\s+계정으로`).test(item)
    ) ?? checklist.checklist[checklistIndex] ??
      `${role} 계정으로 입금확인 엑셀 버튼과 검색 결과/다운로드 데이터 일치 여부를 확인`;

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

      test(checklistItem, async ({ browser }, testInfo) => {
        test.setTimeout(Number(process.env.QA_TEST_TIMEOUT_MS ?? 180_000));

        await withTracedPage(browser, runtimeEnv, testInfo, pageDefinition.name, role, "excel-search-match", async (page) => {
          await openTargetPage(page, runtimeEnv, pageDefinition);
          await selectPreviousMonth(page);
          await searchDepositChecks(page);

          const screen = await readVisibleTable(page);
          await attachScreenshot(page, testInfo, pageDefinition.name, role, "search-result");

          if (screen.rows.length === 0 && /권한이\s*없습니다/.test(screen.bodyText)) {
            await attachEvidenceLog(testInfo, pageDefinition.name, role, "permission-blocked", [
              `${role} 계정으로 입금확인 화면까지 접근했지만 검색 결과를 조회할 권한이 없습니다.`,
              `현재 URL: ${redactUrl(page.url())}`,
              `화면 일부: ${evidenceSnippet(screen.bodyText, 1800)}`
            ]);
            blocked(`${role} 계정에 입금확인 목록/엑셀 조회 권한 또는 대상 데이터가 없습니다.`);
          }

          const expectedCount = screen.totalCount ?? screen.rows.length;

          const excelButton = getExcelButton(page);
          await expect(excelButton, `${role} 계정에 입금확인 엑셀 버튼이 보여야 합니다.`).toBeVisible({ timeout: 10_000 });
          const excel = await requestExcelDownload(page, excelButton, testInfo, pageDefinition.name, role);
          const excelTable = findExcelTable(excel.rows);

          await attachEvidenceLog(testInfo, pageDefinition.name, role, "excel-search-match", [
            `${role} 계정으로 계좌관리 > 입금확인 화면에 접근했습니다.`,
            "검색 버튼으로 현재 검색 조건의 목록을 확정한 뒤 엑셀을 요청했습니다.",
            `현재 URL: ${redactUrl(page.url())}`,
            `화면 헤더: ${screen.headers.join(" | ") || "감지 안 됨"}`,
            `화면 행 수: ${screen.rows.length}`,
            `화면 총 건수: ${screen.totalCount ?? "감지 안 됨"}`,
            `검색 결과가 0건인 경우 빈 목록과 엑셀 헤더/행 0건의 일치 여부를 확인합니다.`,
            `엑셀 파일: ${excel.fileName}`,
            `엑셀 헤더: ${excelTable.headers.join(" | ") || "감지 안 됨"}`,
            `엑셀 데이터 행 수: ${excelTable.rows.length}`,
            `화면 대표 행: ${formatRow(screen.rows[0])}`,
            `엑셀 대표 행: ${formatRow(excelTable.rows[0])}`,
            `화면 일부: ${evidenceSnippet(screen.bodyText, 1800)}`
          ]);
          await attachScreenshot(page, testInfo, pageDefinition.name, role, "excel-download");

          expect(excel.fileName, `${role} 계정의 다운로드 파일은 엑셀 파일이어야 합니다.`).toMatch(/\.(xlsx|xls|csv)$/i);
          expect(screen.headers, `${role} 계정 화면에 입금확인 목록 헤더가 있어야 합니다.`).toEqual(
            expect.arrayContaining(REQUIRED_SCREEN_HEADERS)
          );
          expect(excelTable.headers, `${role} 계정 엑셀에 화면 목록 헤더가 있어야 합니다.`).toEqual(
            expect.arrayContaining(REQUIRED_SCREEN_HEADERS)
          );

          expect(excelTable.rows.length, `${role} 계정의 엑셀 행 수가 검색 결과 건수와 같아야 합니다.`).toBe(expectedCount);

          const screenKeys = buildComparableKeys(screen.headers, screen.rows);
          const excelKeys = buildComparableKeys(excelTable.headers, excelTable.rows);
          expect(excelKeys, `${role} 계정의 엑셀 데이터가 검색 목록과 동일해야 합니다.`).toEqual(screenKeys);
        });
      });
    });
  }
}

async function openTargetPage(page: Page, runtime: RuntimeQaEnv, definition: QaPageDefinition): Promise<void> {
  if (!runtime.baseUrl) throw new Error("baseUrl이 필요합니다.");
  const url = buildPageUrl(runtime.baseUrl, definition, runtime.tenantId ?? "");
  await page.goto(url, { waitUntil: "domcontentloaded" });
  await settle(page);

  if (await isLoginPage(page, runtime)) {
    await loginWithCredentials(page, runtime);
    await page.goto(url, { waitUntil: "domcontentloaded" });
    await settle(page);
  }

  expect(await isLoginPage(page, runtime), `${runtime.role} 계정으로 입금확인 화면에 로그인되어야 합니다.`).toBe(false);
  expect(page.url(), `${runtime.role} 계정이 입금확인 화면으로 이동해야 합니다.`).toContain(definition.path);
  await expect(page.locator("body"), `${runtime.role} 계정의 입금확인 화면이 표시되어야 합니다.`).toContainText(/입금\s*확인|입금확인|입금\s*내역/, {
    timeout: 20_000
  });
}

async function searchDepositChecks(page: Page): Promise<void> {
  const searchButton = page.getByRole("button", { name: "검색", exact: true }).last();
  await expect(searchButton, "입금확인 검색 버튼이 보여야 합니다.").toBeVisible({ timeout: 10_000 });
  const response = page.waitForResponse(
    (candidate) => candidate.request().method() === "GET" && candidate.url().includes(SEARCH_REQUEST_PATTERN),
    { timeout: 20_000 }
  ).catch(() => undefined);
  await searchButton.click();
  await response;
  await settle(page);
}

async function selectPreviousMonth(page: Page): Promise<void> {
  const candidates = [
    page.locator("button:visible").filter({ hasText: /^전월$/ }).last(),
    page.getByText("전월", { exact: true }).last()
  ];
  for (const candidate of candidates) {
    if (await candidate.isVisible({ timeout: 2_000 }).catch(() => false)) {
      await candidate.click();
      await page.waitForTimeout(300);
      const dateValues = await page.locator("input").evaluateAll((inputs) =>
        inputs.map((input) => (input as HTMLInputElement).value).filter(Boolean)
      );
      expect(
        dateValues.some((value) => /2026-07|07\/01\/2026/.test(value)),
        `전월 선택 후 시작일이 7월로 바뀌어야 합니다. 현재 날짜 입력값=${dateValues.join(", ")}`
      ).toBe(true);
      return;
    }
  }
  throw new Error("입금확인 조회기간 전월 버튼을 찾지 못했습니다.");
}

function getExcelButton(page: Page) {
  return page.locator("button:visible").filter({ hasText: /^엑셀$/ }).last();
}

async function requestExcelDownload(
  page: Page,
  excelButton: ReturnType<typeof getExcelButton>,
  testInfo: TestInfo,
  pageName: string,
  role: string
): Promise<ParsedExcel> {
  const requests: string[] = [];
  const onRequest = (request: { url: () => string; method: () => string }): void => {
    requests.push(`${request.method()} ${redactUrl(request.url())}`);
  };
  page.on("request", onRequest);
  const directDownloadPromise = page.waitForEvent("download", { timeout: 3_000 }).catch(() => undefined);
  try {
    await excelButton.click();
    await page.waitForTimeout(700);
    const postClickBody = normalize(await page.locator("body").innerText({ timeout: 5_000 }).catch(() => ""));
    if (/권한이\s*없습니다/.test(postClickBody)) {
      blocked(`${role} 계정의 입금확인 엑셀 요청이 권한 없음으로 차단되었습니다.`);
    }
    const directDownload = await directDownloadPromise;
    const download = directDownload ?? await clickDownloadMenuAndWait(page);

    if (download) {
      return saveAndParseDownload(download, testInfo, pageName, role);
    }

    const documentNavigationButton = page.getByRole("button", { name: /문서관리\s*페이지로\s*이동/ }).last();
    await expect(documentNavigationButton, "엑셀 요청 완료 모달의 문서관리 페이지 이동 버튼이 보여야 합니다.").toBeVisible({ timeout: 10_000 });
    await documentNavigationButton.click();
    await settle(page);
    await expect(page, "엑셀 요청 완료 후 문서관리 페이지로 이동해야 합니다.").toHaveURL(/\/document/, { timeout: 10_000 });

    const documentEntry = await waitForLatestExcelEntry(page, 90_000);
    const documentDownload = await downloadDocumentEntry(page, documentEntry);
    if (!documentDownload) {
      await testInfo.attach(`${safe(pageName)}-${safe(role)}-excel-menu-body.txt`, {
        body: `현재 URL: ${redactUrl(page.url())}\n\n${await page.locator("body").innerText({ timeout: 10_000 })}\n\n요청:\n${requests.join("\n")}`,
        contentType: "text/plain"
      });
      await attachScreenshot(page, testInfo, pageName, role, "excel-menu");
      throw new Error("입금확인 엑셀 버튼 클릭 후 다운로드 이벤트가 발생하지 않았습니다.");
    }
    return saveAndParseDownload(documentDownload, testInfo, pageName, role);
  } finally {
    page.off("request", onRequest);
  }
}

async function saveAndParseDownload(download: Download, testInfo: TestInfo, pageName: string, role: string): Promise<ParsedExcel> {
  const fileName = download.suggestedFilename();
  const downloadPath = testInfo.outputPath(`${safe(pageName)}-${safe(role)}-${safe(fileName)}`);
  await download.saveAs(downloadPath);
  await testInfo.attach(`${safe(pageName)}-${safe(role)}-${safe(fileName)}`, {
    path: downloadPath,
    contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  });
  const buffer = fs.readFileSync(downloadPath);
  return { fileName, rows: extractDownloadRows(buffer, fileName) };
}

async function clickDownloadMenuAndWait(page: Page): Promise<Download | undefined> {
  const downloadControl = page
    .locator("li:visible, [role='menuitem']:visible, button:visible, a:visible")
    .filter({ hasText: /조회결과\s*다운로드|엑셀\s*다운로드|다운로드/ })
    .last();
  if (!(await downloadControl.isVisible({ timeout: 5_000 }).catch(() => false))) {
    return undefined;
  }

  const downloadPromise = page.waitForEvent("download", { timeout: 30_000 }).catch(() => undefined);
  await downloadControl.click();
  return downloadPromise;
}

async function waitForLatestExcelEntry(page: Page, timeoutMs: number): Promise<Locator> {
  const deadline = Date.now() + timeoutMs;
  let lastBody = "";

  while (Date.now() < deadline) {
    const fileNames = page.getByText(/\.xlsx(?:\b|$)/i);
    const count = await fileNames.count().catch(() => 0);
    for (let index = 0; index < count; index += 1) {
      const candidate = fileNames.nth(index);
      if (await candidate.isVisible({ timeout: 500 }).catch(() => false)) {
        return candidate.locator("xpath=ancestor::*[contains(normalize-space(.), '다운로드')][1]");
      }
    }

    lastBody = evidenceSnippet(await page.locator("body").innerText({ timeout: 1_000 }).catch(() => ""), 800);
    await page.waitForTimeout(2_000);
    await page.reload({ waitUntil: "domcontentloaded", timeout: 20_000 }).catch(() => undefined);
    await settle(page);
  }

  blocked(`문서관리에서 입금확인 엑셀 생성 파일을 ${timeoutMs}ms 안에 찾지 못했습니다. 화면 일부: ${lastBody}`);
}

async function downloadDocumentEntry(page: Page, documentEntry: Locator): Promise<Download | undefined> {
  const control = [
    documentEntry.getByRole("button", { name: /다운로드/ }).first(),
    documentEntry.getByRole("link", { name: /다운로드/ }).first(),
    documentEntry.getByText("다운로드", { exact: true }).first(),
    documentEntry
  ];
  let downloadControl: Locator | undefined;
  for (const candidate of control) {
    if (await candidate.isVisible({ timeout: 1_000 }).catch(() => false)) {
      downloadControl = candidate;
      break;
    }
  }
  if (!downloadControl) return undefined;

  const downloadPromise = page.waitForEvent("download", { timeout: 30_000 }).catch(() => undefined);
  await downloadControl.click();
  return downloadPromise;
}

async function readVisibleTable(page: Page): Promise<TableSnapshot> {
  const activeRoot = page.locator("[data-route-keep-alive-active='true']").first();
  const root = await activeRoot.isVisible({ timeout: 1_000 }).catch(() => false) ? activeRoot : page.locator("body");
  const bodyText = normalize(await root.innerText({ timeout: 10_000 }));
  const totalCount = parseTotalCount(bodyText);
  const tables = root.locator("table:visible");

  for (let index = 0; index < await tables.count(); index += 1) {
    const table = tables.nth(index);
    const headers = (await table.locator("thead tr").first().locator("th").allTextContents()).map(normalize).filter(Boolean);
    const rows = await table.locator("tbody tr").allTextContents();
    const normalizedRows = rows.map((row) => row.split(/\n+/).map(normalize).filter(Boolean)).filter((row) => row.length > 0);
    if (headers.some((header) => REQUIRED_SCREEN_HEADERS.includes(header)) && normalizedRows.length > 0) {
      return { headers, rows: normalizedRows, totalCount, bodyText };
    }
  }

  const rowLocators = root.locator("[role='row']:visible");
  const rowCount = await rowLocators.count();
  const roleRows: string[][] = [];
  let headers: string[] = [];
  for (let index = 0; index < rowCount; index += 1) {
    const row = rowLocators.nth(index);
    let cells = (await row.locator("[role='columnheader'], [role='cell']").allTextContents()).map(normalize);
    if (cells.length === 0) {
      cells = (await row.locator(":scope > *").allTextContents()).map(normalize);
    }
    if (cells.length === 0) {
      cells = (await row.innerText().catch(() => "")).split(/\n+/).map(normalize);
    }
    if (!headers.length && cells.some((cell) => REQUIRED_SCREEN_HEADERS.includes(cell))) {
      headers = cells;
      continue;
    }
    if (cells.some(Boolean)) roleRows.push(cells);
  }
  return { headers, rows: roleRows, totalCount, bodyText };
}

function findExcelTable(rows: string[][]): { headers: string[]; rows: string[][] } {
  const headerIndex = rows.findIndex((row) => row.some((cell) => REQUIRED_SCREEN_HEADERS.includes(normalize(cell))));
  if (headerIndex < 0) {
    throw new Error(`엑셀에서 입금확인 헤더 행을 찾지 못했습니다. rows=${JSON.stringify(rows.slice(0, 4))}`);
  }
  const headers = rows[headerIndex].map(normalize);
  const dataRows = rows.slice(headerIndex + 1).filter((row) => row.some((cell) => normalize(cell)));
  return { headers, rows: dataRows };
}

function buildComparableKeys(headers: string[], rows: string[][]): string[] {
  const amountIndex = findHeaderIndex(headers, "입금액");
  const dateIndex = findHeaderIndex(headers, "입금일시");
  if (amountIndex < 0 || dateIndex < 0) {
    throw new Error(`화면/엑셀 비교에 필요한 헤더를 찾지 못했습니다. headers=${headers.join(" | ")}`);
  }

  // The screen omits empty cells from its virtualized row DOM, while the
  // spreadsheet preserves those columns. Compare stable business fields
  // instead of relying on positional indexes that differ between the two.
  return rows
    .map((row) => {
      const amount = canonicalAmount(
        row.find((value) => /\d[\d,]*\s*원$/.test(normalize(value))) ?? row[amountIndex] ?? ""
      );
      const date = canonicalDate(
        row.find((value) => /\d{4}-\d{2}-\d{2}/.test(normalize(value))) ?? row[dateIndex] ?? ""
      );
      const title = normalize(
        row.filter((value) => /입금|테스트|tid\s*=/i.test(normalize(value))).join(" ")
      );
      const account = normalize(
        row.find((value) => /^\d{10,}$/.test(normalize(value).replace(/\s/g, ""))) ?? ""
      );
      return [title, amount, date, account].join("|");
    })
    .sort();
}

function findHeaderIndex(headers: string[], expected: string): number {
  return headers.findIndex((header) => normalize(header).replace(/\s/g, "") === expected.replace(/\s/g, ""));
}

function parseTotalCount(text: string): number | undefined {
  const matches = Array.from(text.matchAll(/총\s*(\d+)\s*\/\s*(\d+)\s*건/g));
  const match = matches[matches.length - 1];
  return match ? Number(match[2]) : undefined;
}

function canonicalAmount(value: string): string {
  const normalized = normalize(value).replace(/,/g, "");
  if (/^-?\d+\.0$/.test(normalized)) {
    return normalized.slice(0, -2);
  }
  return normalized.replace(/[^\d-]/g, "");
}

function canonicalDate(value: string): string {
  const normalized = normalize(value);
  const numeric = Number(normalized);
  if (Number.isFinite(numeric) && numeric > 30000) {
    return excelSerialDate(numeric);
  }
  return normalized.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? normalized;
}

function excelSerialDate(serial: number): string {
  const date = new Date(Date.UTC(1899, 11, 30) + serial * 86_400_000);
  return date.toISOString().slice(0, 10);
}

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

function runtimeBlockReason(runtime: RuntimeQaEnv): string | undefined {
  if (!runtime.baseUrl) return `${runtime.application}/${runtime.environment}/${runtime.role} baseUrl이 없습니다.`;
  if (!runtime.tenantId) return `${runtime.application}/${runtime.environment}/${runtime.role} tenantId가 없습니다.`;
  if (!runtime.storageState) return `${runtime.application}/${runtime.environment}/${runtime.role} storage state가 없습니다.`;
  return undefined;
}

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

async function withTracedPage(
  browser: Browser,
  runtime: RuntimeQaEnv,
  testInfo: TestInfo,
  pageName: string,
  role: string,
  suffix: string,
  callback: (page: Page) => Promise<void>
): Promise<void> {
  const context = await browser.newContext(runtime.storageState ? { storageState: runtime.storageState, acceptDownloads: true } : { acceptDownloads: true });
  const page = await context.newPage();
  try {
    await callback(page);
  } finally {
    await context.close().catch(() => undefined);
  }
}

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

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

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

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

function formatRow(row?: string[]): string {
  return row?.map(normalize).join(" | ") || "없음";
}

function evidenceSnippet(value: string, maxLength: number): string {
  return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
}

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

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