from __future__ import annotations

import os
from datetime import datetime
from dataclasses import dataclass

from playwright.sync_api import Frame, Page

from e2e.pages.wanzhanggui_order_page import WANYOUJI_CONSOLE_URL, WanyoujiFulfillmentPage
from e2e.pages.wanzhanggui_push_page import WANZHANGGUI_CONSOLE_URL
from e2e.utils.bug_reporter import BugReporter


@dataclass(frozen=True)
class PaycenterSection:
    menu: str
    route: str
    ready_keywords: tuple[str, ...]
    editor_keywords: tuple[str, ...]


PAYCENTER_SECTIONS = [
    PaycenterSection(
        menu="交易应用",
        route="/tradingApp",
        ready_keywords=("交易应用", "应用名称", "应用编码", "回调地址", "状态"),
        editor_keywords=("应用名称", "应用编码", "回调地址", "状态"),
    ),
    PaycenterSection(
        menu="交易通道",
        route="/payAppChannelList",
        ready_keywords=("交易通道", "通道名称", "通道编码", "业务类型", "支付方式"),
        editor_keywords=("交易应用", "通道名称", "通道编码", "业务类型", "支付方式"),
    ),
    PaycenterSection(
        menu="账号配置",
        route="/payAppConfig",
        ready_keywords=("账号配置", "账号名称", "账号号", "支付应用ID", "支付方式"),
        editor_keywords=("编码", "名称", "账号名称", "账号号", "支付方式", "支付应用ID"),
    ),
]


class PaycenterConfigPage(WanyoujiFulfillmentPage):
    def __init__(self, page: Page, tenant_home_url: str, bug_reporter: BugReporter | None = None):
        super().__init__(page, tenant_home_url, bug_reporter)
        suffix = os.getenv("E2E_PAYCENTER_SUFFIX", datetime.now().strftime("%m%d%H%M%S"))
        self.paycenter_app_name = os.getenv("E2E_PAYCENTER_APP_NAME", f"丸掌柜-结算中心{suffix}")
        self.paycenter_app_code = os.getenv("E2E_PAYCENTER_APP_CODE", f"wzg_dlsettle_test_{suffix}")
        self.paycenter_channel_name = os.getenv("E2E_PAYCENTER_CHANNEL_NAME", f"支付宝提现{suffix}")
        self.paycenter_channel_code = os.getenv("E2E_PAYCENTER_CHANNEL_CODE", f"ali_transfer_{suffix}")
        self.paycenter_account_code = os.getenv("E2E_PAYCENTER_ACCOUNT_CODE", f"alipay_transfer_test_wanzhanggui_{suffix}")

    def ensure_paycenter_config_sections(self, product: str) -> dict[str, str]:
        self.open_product_console(product)
        result: dict[str, str] = {}
        for section in PAYCENTER_SECTIONS:
            frame = self.open_paycenter_section(section)
            before_text = self.assert_section_ready(section, frame)
            self.create_section_config(section, frame)
            result[section.menu] = self.assert_section_after_save(section, frame, before_text)[:500]
        return result

    def verify_paycenter_config_sections(self, product: str) -> dict[str, str]:
        return self.ensure_paycenter_config_sections(product)

    def open_product_console(self, product: str) -> None:
        if product == "wanzhanggui":
            url = WANZHANGGUI_CONSOLE_URL
        elif product == "wanyouji":
            url = WANYOUJI_CONSOLE_URL
        else:
            raise AssertionError(f"未知支付中心产品入口：{product}")
        self.goto(url)
        self.wait_for_enterprise_console_shell()

    def open_paycenter_section(self, section: PaycenterSection) -> Frame:
        self.expand_paycenter_until_leaf_visible(section.menu)
        clicked_leaf = self.click_sidebar_leaf_precise(section.menu) or self.click_sidebar_text_scrolled(section.menu)
        assert clicked_leaf, (
            f"支付中心未找到 {section.menu} 菜单；text={self.page.locator('body').inner_text(timeout=5000)[:1500]}"
        )
        self.page.wait_for_timeout(3500)
        self.retry_if_app_load_timeout()

        return self.paycenter_frame_by_route(section.route)

    def expand_paycenter_until_leaf_visible(self, leaf_text: str) -> None:
        last_text = ""
        for _ in range(5):
            last_text = self.page.locator("body").inner_text(timeout=5000)
            if leaf_text in last_text and "交易通道" in last_text and "账号配置" in last_text:
                return
            clicked_parent = (
                self.click_sidebar_visible_text("支付中心", right_edge=True)
                or self.click_sidebar_text_scrolled("支付中心", right_edge=True)
            )
            assert clicked_parent, f"未找到支付中心菜单；text={last_text[:1500]}"
            self.page.wait_for_timeout(1200)
        last_text = self.page.locator("body").inner_text(timeout=5000)
        assert leaf_text in last_text, f"支付中心菜单未展开出 {leaf_text}；text={last_text[:1500]}"

    def paycenter_frame_by_route(self, route: str) -> Frame:
        last: list[str] = []
        for _ in range(12):
            gh_candidates: list[Frame] = []
            route_candidates: list[Frame] = []
            for frame in self.page.frames:
                if frame == self.page.main_frame:
                    continue
                try:
                    text = frame.locator("body").inner_text(timeout=1200)
                except Exception:
                    text = ""
                if route in frame.url:
                    route_candidates.append(frame)
                    if "test-gh-payments" in frame.url:
                        gh_candidates.append(frame)
                if text.strip():
                    last.append(f"{frame.url}: {text[:300]}")
            if gh_candidates:
                return gh_candidates[-1]
            if route_candidates and not any("test-order-pc" in item.url for item in route_candidates):
                return route_candidates[-1]
            self.page.wait_for_timeout(1000)
        raise AssertionError(f"未找到支付中心业务 iframe route={route}; frames={last[-5:]}")

    def reload_frame_if_dashboard_shell(self, frame: Frame) -> None:
        for _ in range(2):
            text = frame.locator("body").inner_text(timeout=5000)
            if "数据统计最近" not in text and "商户公告通知" not in text:
                return
            try:
                frame.evaluate("() => window.location.reload()")
            except Exception:
                pass
            self.page.wait_for_timeout(3000)

    def assert_section_ready(self, section: PaycenterSection, frame: Frame) -> str:
        last_text = ""
        for _ in range(6):
            last_text = frame.locator("body").inner_text(timeout=5000)
            if self.section_has_config_surface(section, last_text, frame):
                return last_text
            self.page.wait_for_timeout(1000)
        raise AssertionError(
            f"支付中心-{section.menu} 未渲染出配置页面；"
            f"当前 iframe={frame.url}; text={last_text[:1500]}; "
            "需要看到配置字段、列表、或新增入口，而不是仅进入菜单或工作台。"
        )

    def section_has_config_surface(self, section: PaycenterSection, text: str, frame: Frame) -> bool:
        if "数据统计最近" in text and "商户公告通知" in text:
            return False
        if any(keyword in text for keyword in section.ready_keywords):
            return True
        return bool(self.visible_action_texts(frame)) or self.visible_inputs_or_selects(frame)

    def create_section_config(self, section: PaycenterSection, frame: Frame) -> None:
        self.open_add_editor_or_assert(section, frame)
        self.fill_section_form(section, frame)
        self.save_section_form(section, frame)

    def open_editor_or_assert(self, section: PaycenterSection, frame: Frame) -> None:
        self.open_add_editor_or_assert(section, frame)

    def open_add_editor_or_assert(self, section: PaycenterSection, frame: Frame) -> None:
        text_before = frame.locator("body").inner_text(timeout=5000)
        if self.modal_has_keywords(frame, section.editor_keywords):
            return

        if not self.click_config_action(frame, ["新增", "新 增", "创建", "添加"]):
            allow_edit_fallback = os.getenv("E2E_PAYCENTER_ALLOW_EDIT_FALLBACK", "").strip() == "1"
            if not allow_edit_fallback or not self.click_config_action(frame, ["编辑", "修改"]):
                raise AssertionError(
                    f"支付中心-{section.menu} 未找到新增配置入口；"
                    "本用例以新增为首要流程目标，未开启编辑兜底；"
                    f"iframe={frame.url}; actions={self.visible_action_texts(frame)}; text={text_before[:1500]}"
                )

        self.page.wait_for_timeout(1800)
        combined = f"{self.visible_modal_text(frame)}\n{frame.locator('body').inner_text(timeout=5000)}"
        assert any(keyword in combined for keyword in section.editor_keywords), (
            f"支付中心-{section.menu} 点击新增入口后未出现对应配置表单；"
            f"期望字段={section.editor_keywords}; text={combined[:1500]}"
        )

    def click_config_action(self, frame: Frame, actions: list[str]) -> bool:
        return bool(
            frame.evaluate(
                """actions => {
                    const normalized = actions.map(action => action.replace(/\\s+/g, ''));
                    const nodes = Array.from(document.querySelectorAll('button, a, [role="button"], span, div'))
                      .map(el => ({
                        el,
                        text: (el.innerText || el.textContent || '').replace(/\\s+/g, '').trim(),
                        r: el.getBoundingClientRect(),
                        disabled: Boolean(el.disabled),
                      }))
                      .filter(item => item.r.width > 0 && item.r.height > 0 && !item.disabled)
                      .filter(item => normalized.some(action => item.text === action || item.text.includes(action)))
                      .sort((a, b) => {
                        const exactA = normalized.some(action => a.text === action) ? 0 : 1;
                        const exactB = normalized.some(action => b.text === action) ? 0 : 1;
                        return exactA - exactB || (a.r.width * a.r.height) - (b.r.width * b.r.height);
                      });
                    const target = nodes[0];
                    if (!target) return false;
                    (target.el.closest('button, a, [role="button"]') || target.el).click();
                    return true;
                }""",
                actions,
            )
        )

    def click_first_config_action(self, frame: Frame) -> bool:
        return self.click_config_action(frame, ["新增", "新 增", "创建", "添加", "编辑", "修改"])

    def fill_section_form(self, section: PaycenterSection, frame: Frame) -> None:
        self.fill_visible_form_controls(frame, self.section_form_values(section))
        if section.route == "/payAppChannelList":
            self.click_radio_or_option(frame, "提现")
        self.select_section_dropdowns(section, frame)
        self.page.wait_for_timeout(800)

    def section_form_values(self, section: PaycenterSection) -> dict[str, str]:
        app_code = self.paycenter_app_code
        app_name = self.paycenter_app_name
        callback_url = os.getenv("E2E_PAYCENTER_CALLBACK_URL", "")
        channel_code = self.paycenter_channel_code
        channel_name = self.paycenter_channel_name
        account_code = self.paycenter_account_code
        account_name = os.getenv("E2E_PAYCENTER_ACCOUNT_NAME", "外飒支付宝转账")
        account_display_name = os.getenv("E2E_PAYCENTER_ACCOUNT_DISPLAY_NAME", "支付宝提现")
        account_no = os.getenv("E2E_PAYCENTER_ACCOUNT_NO", "外飒")
        app_id = os.getenv("E2E_PAYCENTER_ALIPAY_APP_ID", "2021004124610005")
        private_key = os.getenv(
            "E2E_PAYCENTER_PRIVATE_KEY",
            "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCf7VqHOXo0xVR50JPovpptZoVSx60yAmSpeVDi1iFaHKj5FzbOAcFYqSuyoB6MxV8CZRkfZEIAg0KHtLYQ8XADyRGcOr2jhQDHQwl4C94Pi5nwa7pr5rcw4XBe3YptMHizxWKBJhy0cCYADllkGb",
        )
        alipay_public_key = os.getenv(
            "E2E_PAYCENTER_ALIPAY_PUBLIC_KEY",
            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn+1ahzl6NMVUedCT6L6abWaFUsetMgJkqXlQ4tYhWhyo+Rc2zgHBWKkrsqAejMVfAmUZH2RCAINCh7S2EPFwA8kRnDq9o4UAx0MJeAveD4uZ8Gu6a+a3MOFwXt2KbTB4s8VigSYctHAmAA5ZZBm",
        )
        app_cert = os.getenv(
            "E2E_PAYCENTER_APP_CERT",
            "-----BEGIN CERTIFICATE----- MIIEmjCCA4KgAwIBAgIQICMQE/asbTtVwhEx3X37FjANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWw= -----END CERTIFICATE-----",
        )
        alipay_cert = os.getenv(
            "E2E_PAYCENTER_ALIPAY_CERT",
            "-----BEGIN CERTIFICATE----- MIIDrDCCApSgAwIBAgIQICMQE00obexKu98hbwV06jANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWw= -----END CERTIFICATE-----",
        )
        root_cert = os.getenv(
            "E2E_PAYCENTER_ROOT_CERT",
            "-----BEGIN CERTIFICATE----- MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQGEwJDTjEOMAwGA1UECgwFTlJDQUM= -----END CERTIFICATE-----",
        )

        common = {"状态": "启用", "排序": "1", "备注": "E2E B-SETTLE-PAYCENTER-001"}
        if section.route == "/tradingApp":
            return {
                **common,
                "应用名称": app_name,
                "应用编码": app_code,
                "交易应用": app_name,
                "回调地址": callback_url,
                "回调URL": callback_url,
            }
        if section.route == "/payAppChannelList":
            return {
                **common,
                "交易应用": app_name,
                "应用名称": app_name,
                "通道名称": channel_name,
                "通道编码": channel_code,
                "业务类型": "提现",
                "支付方式": "支付宝",
            }
        return {
            **common,
            "编码": account_code,
            "名称": account_name,
            "显示名称": account_display_name,
            "账号名称": account_name,
            "账号号": account_no,
            "账户号": account_no,
            "账号": account_no,
            "支付方式": "支付宝",
            "支付应用ID": app_id,
            "应用ID": app_id,
            "支付宝应用ID": app_id,
            "私钥": private_key,
            "支付宝公钥": alipay_public_key,
            "接口秘钥": private_key,
            "接口密钥": private_key,
            "应用公钥证书内容": app_cert,
            "应用公钥证书": app_cert,
            "支付宝公钥证书内容": alipay_cert,
            "支付宝公钥证书": alipay_cert,
            "支付宝根证书内容": root_cert,
            "支付宝根证书": root_cert,
        }

    def fill_visible_form_controls(self, frame: Frame, values: dict[str, str]) -> None:
        frame.evaluate(
            """values => {
                const roots = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer, form'))
                  .filter(el => {
                    const r = el.getBoundingClientRect();
                    return r.width > 0 && r.height > 0;
                  });
                const root = roots[roots.length - 1] || document;
                const controls = Array.from(root.querySelectorAll('input, textarea'))
                  .map(el => {
                    const r = el.getBoundingClientRect();
                    let label = el.getAttribute('placeholder') || '';
                    for (let node = el.parentElement; node && node !== root; node = node.parentElement) {
                      const text = (node.innerText || node.textContent || '').replace(/\\s+/g, '');
                      if (text && text.length < 140) {
                        label = `${label}${text}`;
                        break;
                      }
                    }
                    return { el, r, label, placeholder: el.getAttribute('placeholder') || '' };
                  })
                  .filter(item => item.r.width > 0 && item.r.height > 0)
                  .filter(item => !item.el.disabled && !item.el.readOnly && item.el.type !== 'hidden' && item.el.type !== 'file');
                const setValue = (el, value) => {
                  const proto = el.tagName === 'TEXTAREA'
                    ? window.HTMLTextAreaElement.prototype
                    : window.HTMLInputElement.prototype;
                  const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
                  setter.call(el, value);
                  el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }));
                  el.dispatchEvent(new Event('change', { bubbles: true }));
                  el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
                };
                const used = new Set();
                for (const [key, value] of Object.entries(values)) {
                  const target = controls.find(item =>
                    !used.has(item.el) &&
                    (item.placeholder.includes(key) || item.label.includes(key))
                  );
                  if (!target) continue;
                  setValue(target.el, value);
                  used.add(target.el);
                }
            }""",
            values,
        )

    def click_radio_or_option(self, frame: Frame, text: str) -> None:
        frame.evaluate(
            """text => {
                const roots = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer, form'))
                  .filter(el => {
                    const r = el.getBoundingClientRect();
                    return r.width > 0 && r.height > 0;
                  });
                const root = roots[roots.length - 1] || document;
                const nodes = Array.from(root.querySelectorAll('label, .ant-radio-wrapper, .ant-checkbox-wrapper, span, div'))
                  .map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
                  .filter(item => item.r.width > 0 && item.r.height > 0)
                  .filter(item => item.text === text || item.text.includes(text))
                  .sort((a, b) => (a.r.width * a.r.height) - (b.r.width * b.r.height));
                const target = nodes[0];
                (target?.el.closest('label, .ant-radio-wrapper, .ant-checkbox-wrapper') || target?.el)?.click();
            }""",
            text,
        )
        self.page.wait_for_timeout(300)

    def select_section_dropdowns(self, section: PaycenterSection, frame: Frame) -> None:
        if section.route == "/tradingApp":
            self.select_modal_select_by_index(frame, 0, "启用")
            return
        if section.route == "/payAppChannelList":
            self.select_modal_select_by_index(frame, 0, self.paycenter_app_name)
            self.select_modal_select_by_index(frame, 1, "支付宝转账")
            return
        self.select_modal_select_by_index(frame, 0, "提现")
        self.select_modal_select_by_index(frame, 1, "支付宝转账")
        self.fill_visible_form_controls(frame, self.section_form_values(section))

    def select_visible_dropdowns(self, frame: Frame) -> None:
        for label in ["交易应用", "应用", "业务类型", "支付方式", "状态"]:
            self.select_dropdown_by_label_or_first(frame, label)

    def select_dropdown_by_label_or_first(self, frame: Frame, label: str, preferred_text: str = "") -> None:
        clicked = frame.evaluate(
            """({ label }) => {
                const roots = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer, form'))
                  .filter(el => {
                    const r = el.getBoundingClientRect();
                    return r.width > 0 && r.height > 0;
                  });
                const root = roots[roots.length - 1] || document;
                const selectors = Array.from(root.querySelectorAll('.ant-select-selector'))
                  .map(el => {
                    const r = el.getBoundingClientRect();
                    let text = (el.innerText || el.textContent || '').replace(/\\s+/g, '');
                    for (let node = el.parentElement; node && node !== root; node = node.parentElement) {
                      const nodeText = (node.innerText || node.textContent || '').replace(/\\s+/g, '');
                      if (nodeText && nodeText.length < 140) {
                        text = `${text}${nodeText}`;
                        break;
                      }
                    }
                    return { el, r, text };
                  })
                  .filter(item => item.r.width > 0 && item.r.height > 0)
                  .filter(item => item.text.includes(label) || item.text.includes('请选择') || !item.text);
                const target = selectors.find(item => item.text.includes(label)) || selectors[0];
                if (!target) return false;
                target.el.click();
                return true;
            }""",
            {"label": label},
        )
        if not clicked:
            return
        self.page.wait_for_timeout(500)
        frame.evaluate(
            """preferredText => {
                const options = Array.from(document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option:not(.ant-select-item-option-disabled)'))
                  .map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
                  .filter(item => item.r.width > 0 && item.r.height > 0 && item.text && item.text !== 'No data');
                const target = (preferredText ? options.find(item => item.text.includes(preferredText)) : null) ||
                  options.find(item => item.text.includes('丸掌柜-结算中心')) ||
                  options.find(item => item.text.includes('提现')) ||
                  options.find(item => item.text.includes('支付宝转账')) ||
                  options.find(item => item.text.includes('启用')) ||
                  options.find(item => item.text.includes('支付宝')) ||
                  options[0];
                target?.el.click();
            }""",
            preferred_text,
        )
        self.page.wait_for_timeout(500)

    def select_modal_select_by_index(self, frame: Frame, index: int, preferred_text: str) -> None:
        selector = frame.locator(".ant-modal:visible .ant-select-selector, .ant-drawer:visible .ant-select-selector").nth(index)
        if selector.count() == 0:
            return
        selector.click(force=True, timeout=5000)
        self.page.wait_for_timeout(700)
        option = frame.locator(
            ".ant-select-dropdown:not(.ant-select-dropdown-hidden) "
            ".ant-select-item-option:not(.ant-select-item-option-disabled)"
        ).filter(has_text=preferred_text).first
        if option.count() == 0:
            option = frame.locator(
                ".ant-select-dropdown:not(.ant-select-dropdown-hidden) "
                ".ant-select-item-option:not(.ant-select-item-option-disabled)"
            ).first
        assert option.count(), (
            f"支付中心下拉第 {index + 1} 个未展开可选项 {preferred_text}；"
            f"text={frame.locator('body').inner_text(timeout=5000)[:1200]}"
        )
        option.click(force=True, timeout=5000)
        self.page.wait_for_timeout(700)

    def save_section_form(self, section: PaycenterSection, frame: Frame) -> None:
        saved = self.click_latest_confirm_primary_button(frame) or self.click_overlay_action(
            frame,
            ["保存", "确 定", "确定", "确 认", "确认", "提交"],
        )
        assert saved, (
            f"支付中心-{section.menu} 新增表单未找到保存/确认按钮；text={self.visible_modal_text(frame)[:1500]}"
        )
        self.page.wait_for_timeout(3000)
        self.assert_no_visible_form_errors(section, frame)

    def assert_no_visible_form_errors(self, section: PaycenterSection, frame: Frame) -> None:
        errors = list(
            frame.evaluate(
                """() => Array.from(document.querySelectorAll(
                    '.ant-form-item-explain-error, .ant-message-notice, .ant-notification-notice, [role="alert"]'
                  ))
                  .map(el => {
                    const r = el.getBoundingClientRect();
                    return { text: (el.innerText || el.textContent || '').trim(), r };
                  })
                  .filter(item => item.text && item.r.width > 0 && item.r.height > 0)
                  .map(item => item.text)"""
            )
        )
        assert not errors, f"支付中心-{section.menu} 新增保存后出现表单/接口错误：{errors}"

    def assert_section_after_save(self, section: PaycenterSection, frame: Frame, previous_text: str) -> str:
        text = frame.locator("body").inner_text(timeout=5000)
        modal_text = self.visible_modal_text(frame)
        if modal_text and any(keyword in modal_text for keyword in ["必填", "不能为空", "请选择", "错误", "失败"]):
            raise AssertionError(f"支付中心-{section.menu} 新增保存后弹窗仍提示异常：{modal_text[:1500]}")
        if "数据统计最近" in text and "商户公告通知" in text:
            raise AssertionError(f"支付中心-{section.menu} 保存后仍回到工作台内容；iframe={frame.url}; text={text[:1500]}")
        return text if text else previous_text

    def modal_has_keywords(self, frame: Frame, keywords: tuple[str, ...]) -> bool:
        text = self.visible_modal_text(frame)
        return any(keyword in text for keyword in keywords)

    def visible_action_texts(self, frame: Frame) -> list[str]:
        return list(
            frame.evaluate(
                """() => Array.from(document.querySelectorAll('button, a, [role="button"]'))
                  .map(el => ({ text: (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim(), r: el.getBoundingClientRect(), disabled: Boolean(el.disabled) }))
                  .filter(item => item.text && item.r.width > 0 && item.r.height > 0 && !item.disabled)
                  .map(item => item.text)"""
            )
        )

    def visible_inputs_or_selects(self, frame: Frame) -> bool:
        return bool(
            frame.evaluate(
                """() => Array.from(document.querySelectorAll('input, textarea, .ant-select-selector, [role="combobox"]'))
                  .some(el => {
                    const r = el.getBoundingClientRect();
                    return r.width > 0 && r.height > 0;
                  })"""
            )
        )

    def visible_modal_text(self, frame: Frame) -> str:
        return str(
            frame.evaluate(
                """() => {
                    const roots = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer, .ant-popover, .ant-popconfirm'))
                      .filter(el => {
                        const r = el.getBoundingClientRect();
                        return r.width > 0 && r.height > 0;
                      });
                    const root = roots[roots.length - 1];
                    return root ? (root.innerText || root.textContent || '') : '';
                }"""
            )
        )
