import { expect, test, type Browser, type Locator, type Page, type TestInfo } from "@playwright/test";
import { isLoginPage, loginWithCredentials } from "../auth";
import { loadChecklist, loadIssueMetadata } from "../checklist";
import { getRuntimeQaEnv, type RuntimeQaEnv } from "../env";
import { pageRunsForRole, resolveChecklistExecutionRoles } from "../roles";
import type { 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 CancelModalState {
  modalText: string;
  fullCancelVisible: boolean;
  partialCancelVisible: boolean;
  amountValue: string;
  submitVisible: boolean;
  submitDisabled: boolean;
  rowText: string;
}

const REQUIRED_ROLES = ["ADMIN", "TA", "CO", "BO"];
const SEARCH_FROM = "2026-03-01";
const SEARCH_TO = "2026-09-10";

export function runTransactionManualCancelModalSuite(options: ScenarioOptions): void {
  const metadata = loadIssueMetadata(options.issueId);
  const checklist = loadChecklist(options.issueId, options.environment);
  const transactionPage = findPageDefinition(checklist.pages, "거래내역");

  test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
    for (const role of resolveChecklistExecutionRoles(checklist)) {
      if (!REQUIRED_ROLES.includes(role) || !pageRunsForRole(checklist, transactionPage, role)) {
        continue;
      }

      const runtimeRole = resolveRuntimeRole(checklist, role);
      const accountLabel = resolveAccountLabel(checklist, role);
      const runtime = getRuntimeQaEnv(options.environment, transactionPage.application ?? "admin", runtimeRole);
      const blockReason = getRuntimeBlockReason(runtime, role);

      test.describe(`[${role}] ${transactionPage.name}`, () => {
        test.skip(!!blockReason, blockReason);

        test(`[${role}] role / ${transactionPage.name} / ${checklist.checklist[0]}`, async ({ browser }, testInfo) => {
          test.setTimeout(120_000);
          await withRolePage(browser, runtime, transactionPage, async (page) => {
            const { modal, rowText } = await openCancelModalForApprovedTransaction(page);
            const state = await inspectModal(modal, rowText);
            await attachEvidence(testInfo, page, role, "cancel-type-options", accountLabel, state, [
              "결론: 전산취소 버튼으로 연 모달에 전체취소와 부분취소 선택 항목이 모두 표시되었습니다."
            ]);

            expect(state.fullCancelVisible, `${role} 전산취소 모달에 전체취소가 표시되어야 합니다.`).toBe(true);
            expect(state.partialCancelVisible, `${role} 전산취소 모달에 부분취소가 표시되어야 합니다.`).toBe(true);
          });
        });

        test(`[${role}] role / ${transactionPage.name} / ${checklist.checklist[1]}`, async ({ browser }, testInfo) => {
          test.setTimeout(120_000);
          await withRolePage(browser, runtime, transactionPage, async (page) => {
            const { modal, rowText } = await openCancelModalForApprovedTransaction(page);
            await selectCancelType(modal, "전체취소");
            const state = await inspectModal(modal, rowText);
            await attachEvidence(testInfo, page, role, "full-cancel-autofill", accountLabel, state, [
              `자동 입력 금액: ${state.amountValue || "공란"}`,
              "결론: 전체취소 선택 시 취소 금액이 자동 입력되었습니다. 취소신청은 제출하지 않았습니다."
            ]);

            expect(parseMoney(state.amountValue), `${role} 전체취소 선택 시 취소 금액이 자동 입력되어야 합니다.`).toBeGreaterThan(0);
          });
        });

        test(`[${role}] role / ${transactionPage.name} / ${checklist.checklist[2]}`, async ({ browser }, testInfo) => {
          test.setTimeout(120_000);
          await withRolePage(browser, runtime, transactionPage, async (page) => {
            const { modal, rowText } = await openCancelModalForApprovedTransaction(page);
            await selectCancelType(modal, "부분취소");
            const before = await inspectModal(modal, rowText);
            await attachEvidence(testInfo, page, role, "partial-cancel-empty-disabled", accountLabel, before, [
              "단계: 부분취소 선택 직후",
              "결론: 취소 금액은 공란이고 취소신청 버튼은 비활성화 상태입니다."
            ]);

            expect(parseMoney(before.amountValue), `${role} 부분취소 선택 직후 취소 금액은 공란이어야 합니다.`).toBe(0);
            expect(before.submitVisible, `${role} 취소신청 버튼이 표시되어야 합니다.`).toBe(true);
            expect(before.submitDisabled, `${role} 금액 입력 전 취소신청 버튼이 비활성화되어야 합니다.`).toBe(true);

            const reasonInput = modal.locator("input[placeholder*='취소사유']:visible").last();
            await expect(reasonInput, `${role} 기존 필수 취소사유 입력란이 표시되어야 합니다.`).toBeVisible();
            await reasonInput.fill("QA #4090 부분취소 입력 검증");
            const reasonOnly = await inspectModal(modal, rowText);
            expect(reasonOnly.submitDisabled, `${role} 취소사유만 입력하고 금액이 공란이면 버튼이 비활성화되어야 합니다.`).toBe(true);

            const amountInput = await findAmountInput(modal);
            await amountInput.fill("100");
            await amountInput.blur();
            const after = await inspectModal(modal, rowText);
            await attachEvidence(testInfo, page, role, "partial-cancel-input-enabled", accountLabel, after, [
              "단계: 필수 취소사유를 먼저 입력하고 부분취소 금액 100원 입력 후",
              "결론: 취소사유만 입력한 상태에서는 비활성화가 유지되고, 금액까지 입력한 뒤 취소신청 버튼이 활성화되었습니다. 실제 취소신청은 제출하지 않았습니다."
            ]);

            expect(parseMoney(after.amountValue), `${role} 입력한 부분취소 금액이 유지되어야 합니다.`).toBe(100);
            expect(after.submitDisabled, `${role} 금액 입력 후 취소신청 버튼이 활성화되어야 합니다.`).toBe(false);
          });
        });
      });
    }
  });
}

async function withRolePage(
  browser: Browser,
  runtime: RuntimeQaEnv,
  pageDefinition: QaPageDefinition,
  callback: (page: Page) => Promise<void>
): Promise<void> {
  const context = await browser.newContext({
    ...(runtime.storageState ? { storageState: runtime.storageState } : {}),
    viewport: { width: 1440, height: 900 }
  });
  const page = await context.newPage();
  try {
    await openTransactionPage(page, runtime, pageDefinition);
    await callback(page);
  } finally {
    await context.close();
  }
}

async function openTransactionPage(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), `${runtime.role} 로그인 세션이 유지되어야 합니다.`).toBe(false);
  await expect(page.locator("body"), "거래내역 화면이 표시되어야 합니다.").toContainText("거래내역", { timeout: 20_000 });
  await applyApprovedSearch(page);
}

async function applyApprovedSearch(page: Page): Promise<void> {
  const dates = page.locator('input[type="date"]:visible');
  const count = await dates.count();
  if (count < 2) {
    blocked("거래일시 시작일/종료일 입력란을 찾지 못했습니다.");
  }
  await dates.nth(count - 2).fill(SEARCH_FROM);
  await dates.last().fill(SEARCH_TO);

  const status = await findStatusControl(page);
  if (!status) {
    const detail = page.getByRole("button", { name: "상세검색", exact: true }).first();
    if (await detail.isVisible({ timeout: 1_000 }).catch(() => false)) {
      await detail.click();
    }
  }
  const visibleStatus = status ?? await findStatusControl(page);
  if (!visibleStatus) {
    blocked("거래내역 상태 검색 필터를 찾지 못했습니다.");
  }
  await selectOption(page, visibleStatus, "승인");
  await page.keyboard.press("Escape").catch(() => undefined);

  const responsePromise = page.waitForResponse((response) => {
    if (response.request().method() !== "GET" || !response.url().includes("/api/v2/transactions")) {
      return false;
    }
    const url = new URL(response.url());
    return url.searchParams.get("from") === SEARCH_FROM && url.searchParams.get("to") === SEARCH_TO;
  }, { timeout: 30_000 }).catch(() => undefined);
  const search = page.getByRole("button", { name: /^(검색|조회)$/ }).last();
  await expect(search, "거래내역 검색 버튼이 표시되어야 합니다.").toBeVisible({ timeout: 8_000 });
  await search.click();
  const response = await responsePromise;
  if (response) {
    expect(response.status(), "승인 거래 검색 API가 성공해야 합니다.").toBeLessThan(400);
  }
  await waitForSettle(page);
}

async function findStatusControl(page: Page): Promise<Locator | undefined> {
  const candidates = [
    page.getByRole("combobox", { name: /^상태(?:\s|$)/ }).last(),
    page.getByLabel("상태", { exact: true }).last()
  ];
  for (const candidate of candidates) {
    if (await candidate.isVisible({ timeout: 700 }).catch(() => false)) {
      return candidate;
    }
  }
  const label = page.getByText("상태", { exact: true }).last();
  if (!(await label.isVisible({ timeout: 700 }).catch(() => false))) {
    return undefined;
  }
  const nearby = label.locator("xpath=..//*[self::input or @role='combobox' or @aria-haspopup='listbox'][1]");
  return await nearby.isVisible({ timeout: 700 }).catch(() => false) ? nearby : undefined;
}

async function selectOption(page: Page, control: Locator, optionName: string): Promise<void> {
  const tag = await control.evaluate((element) => element.tagName).catch(() => "");
  if (tag === "SELECT") {
    await control.selectOption({ label: optionName });
    return;
  }
  await control.click({ force: true });
  const roleOption = page.getByRole("option", { name: optionName, exact: true }).last();
  if (await roleOption.isVisible({ timeout: 2_000 }).catch(() => false)) {
    await roleOption.click();
    return;
  }
  const textOption = page.getByText(optionName, { exact: true }).last();
  await expect(textOption, `${optionName} 옵션이 표시되어야 합니다.`).toBeVisible({ timeout: 5_000 });
  await textOption.click();
}

async function openCancelModalForApprovedTransaction(page: Page): Promise<{ modal: Locator; rowText: string }> {
  const grid = page.getByRole("grid").last();
  await expect(grid, "승인 거래 목록 그리드가 표시되어야 합니다.").toBeVisible({ timeout: 15_000 });
  await scrollGridToCancelColumn(grid);
  const actionCell = grid.locator('[role="gridcell"][data-field="pgCancel"]:visible')
    .filter({ hasText: "전산취소" })
    .first();
  await expect(actionCell, "승인 검색 결과에 전산취소 조작 셀이 표시되어야 합니다.").toBeVisible({ timeout: 10_000 });
  const row = actionCell.locator("xpath=ancestor::*[@role='row'][1]");
  const rowText = normalizeText(await row.innerText().catch(() => ""));
  const control = actionCell.getByText("전산취소", { exact: true }).last();
  await expect(control, "전산취소 조작 버튼이 표시되어야 합니다.").toBeVisible({ timeout: 5_000 });
  await control.click({ force: true });
  const modal = findCancelModal(page);
  await expect(modal, "전체취소/부분취소 선택 모달이 표시되어야 합니다.").toBeVisible({ timeout: 8_000 });
  return { modal, rowText };
}

async function scrollGridToCancelColumn(grid: Locator): Promise<void> {
  const scroller = grid.locator(".MuiDataGrid-virtualScroller").first();
  await expect(scroller, "거래내역 스크롤 영역이 표시되어야 합니다.").toBeVisible();
  await scroller.evaluate((element) => {
    element.scrollLeft = Math.floor((element.scrollWidth - element.clientWidth) * 0.62);
    element.dispatchEvent(new Event("scroll", { bubbles: true }));
  });
  await grid.locator(".MuiDataGrid-scrollbar--horizontal").first().evaluate((element) => {
    element.scrollLeft = Math.floor((element.scrollWidth - element.clientWidth) * 0.62);
    element.dispatchEvent(new Event("scroll", { bubbles: true }));
  }).catch(() => undefined);
  await pageWait(grid, 500);
}

async function pageWait(locator: Locator, timeout: number): Promise<void> {
  await locator.page().waitForTimeout(timeout);
}

function findCancelModal(page: Page): Locator {
  const heading = page.getByRole("heading", { name: /취소 방식 선택|전산취소 신청|취소 신청/ }).last();
  return heading.locator(
    "xpath=ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' fixed ') and contains(concat(' ', normalize-space(@class), ' '), ' inset-0 ')][1]"
  );
}

async function selectCancelType(modal: Locator, label: "전체취소" | "부분취소"): Promise<void> {
  const button = modal.getByRole("button", { name: new RegExp(`^${label}`) }).last();
  if (await button.isVisible({ timeout: 1_000 }).catch(() => false)) {
    await button.click();
  } else {
    const text = modal.getByText(label, { exact: true }).last();
    await expect(text, `${label} 선택 항목이 표시되어야 합니다.`).toBeVisible({ timeout: 5_000 });
    await text.click();
  }
  await modal.page().waitForTimeout(300);
}

async function inspectModal(modal: Locator, rowText: string): Promise<CancelModalState> {
  await expect(modal, "전산취소 모달이 표시되어야 합니다.").toBeVisible({ timeout: 8_000 });
  const text = normalizeText(await modal.innerText().catch(() => ""));
  const amount = await findAmountInputOptional(modal);
  const submit = await findSubmitButtonOptional(modal);
  return {
    modalText: text,
    fullCancelVisible: /전체취소/.test(text),
    partialCancelVisible: /부분취소/.test(text),
    amountValue: amount ? await amount.inputValue().catch(() => "") : "",
    submitVisible: submit ? await submit.isVisible({ timeout: 500 }).catch(() => false) : false,
    submitDisabled: submit
      ? await submit.isDisabled().catch(async () => (await submit.getAttribute("aria-disabled")) === "true")
      : false,
    rowText
  };
}

async function findAmountInput(modal: Locator): Promise<Locator> {
  const input = await findAmountInputOptional(modal);
  if (input) {
    return input;
  }
  const missing = modal.locator("input[inputmode='numeric']:visible, input[type='number']:visible").last();
  await expect(missing, "취소 금액 입력란이 표시되어야 합니다.").toBeVisible({ timeout: 5_000 });
  return missing;
}

async function findAmountInputOptional(modal: Locator): Promise<Locator | undefined> {
  const byLabel = modal.getByLabel(/취소\s*금액/).last();
  if (await byLabel.isVisible({ timeout: 700 }).catch(() => false)) {
    return byLabel;
  }
  const inputs = modal.locator("input:visible");
  for (let index = 0; index < await inputs.count(); index += 1) {
    const input = inputs.nth(index);
    const context = normalizeText([
      await input.getAttribute("name"),
      await input.getAttribute("placeholder"),
      await input.getAttribute("aria-label"),
      await input.locator("xpath=..").innerText().catch(() => "")
    ].filter(Boolean).join(" "));
    if (/금액|amount/i.test(context)) {
      return input;
    }
  }
  const numeric = modal.locator("input[inputmode='numeric']:visible, input[type='number']:visible").last();
  return await numeric.isVisible({ timeout: 700 }).catch(() => false) ? numeric : undefined;
}

async function findSubmitButtonOptional(modal: Locator): Promise<Locator | undefined> {
  const exact = modal.getByRole("button", { name: /취소\s*신청|신청/, exact: true }).last();
  if (await exact.isVisible({ timeout: 500 }).catch(() => false)) {
    return exact;
  }
  const fallback = modal.locator("button").filter({ hasText: /취소\s*신청|신청/ }).last();
  return await fallback.isVisible({ timeout: 500 }).catch(() => false) ? fallback : undefined;
}

async function attachEvidence(
  testInfo: TestInfo,
  page: Page,
  role: string,
  action: string,
  accountLabel: string,
  state: CancelModalState,
  extraLines: string[]
): Promise<void> {
  const baseName = `${safeFilename(role)}-${safeFilename(action)}`;
  const lines = [
    `검증 계정: ${accountLabel} (${role})`,
    `조회 조건: ${SEARCH_FROM} ~ ${SEARCH_TO} / 승인`,
    `대상 거래 행: ${snippet(state.rowText, 800)}`,
    `전체취소 노출: ${state.fullCancelVisible ? "예" : "아니오"}`,
    `부분취소 노출: ${state.partialCancelVisible ? "예" : "아니오"}`,
    `취소 금액 입력값: ${state.amountValue || "공란"}`,
    `취소신청 버튼 노출: ${state.submitVisible ? "예" : "아니오"}`,
    `취소신청 버튼 비활성화: ${state.submitDisabled ? "예" : "아니오"}`,
    `모달 문구: ${snippet(state.modalText, 1000)}`,
    ...extraLines
  ];
  await testInfo.attach(`${baseName}.md`, {
    body: `${lines.join("\n")}\n`,
    contentType: "text/markdown"
  });
  await testInfo.attach(`${baseName}.png`, {
    body: await page.screenshot({ fullPage: false }),
    contentType: "image/png"
  });
}

function resolveRuntimeRole(checklist: QaChecklist, role: string): string {
  return checklist.subjectAccounts?.find((account) => account.role.toUpperCase() === role)?.envRole ?? role;
}

function resolveAccountLabel(checklist: QaChecklist, role: string): string {
  return checklist.subjectAccounts?.find((account) => account.role.toUpperCase() === role)?.label ?? role;
}

function getRuntimeBlockReason(runtime: RuntimeQaEnv, role: string): string | undefined {
  if (!runtime.baseUrl || !runtime.tenantId) {
    return `BLOCKED: ${role} 개발계 baseUrl 또는 tenantId 설정이 필요합니다.`;
  }
  if (!runtime.storageState && (!runtime.loginUrl || !runtime.username || !runtime.password)) {
    return `BLOCKED: ${role} 개발계 storage state 또는 로그인 정보가 필요합니다.`;
  }
  return undefined;
}

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

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

function parseMoney(value: string): number {
  const digits = value.replace(/[^0-9.-]/g, "");
  return digits ? Number(digits) : 0;
}

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 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}`);
}
