from __future__ import annotations

import re
from pathlib import Path

from playwright.sync_api import Locator, expect

from e2e.pages.saas_subscription_admin_page import SaasSubscriptionAdminPage


class ProductSolutionAdminPage(SaasSubscriptionAdminPage):
    def open_product_solution_list(self) -> None:
        self.maybe_click_text(["产品管理", "浜у搧绠＄悊"], timeout=3000)
        self.click_first_visible(
            [
                "text=产品解决方案",
                "text=浜у搧瑙ｅ喅鏂规",
                ".ant-menu-item:has-text('产品解决方案')",
                ".ant-menu-item:has-text('浜у搧瑙ｅ喅鏂规')",
            ],
            timeout=5000,
        )
        self.page.wait_for_load_state("domcontentloaded")
        self.page.wait_for_timeout(1500)

    def create_product_with_subscription_version(
        self,
        *,
        product_name: str,
        product_description: str,
        version_name: str,
        version_description: str,
        logo_path: Path,
    ) -> str:
        self.open_product_solution_list()
        self.click_first_visible(
            [
                "button:has-text('新建产品')",
                "button:has-text('新增产品')",
                "button:has-text('鏂板缓浜у搧')",
                "button:has-text('鏂板浜у搧')",
            ],
            timeout=5000,
        )
        self.page.wait_for_timeout(1500)

        self.fill_product_basic_info(product_name, product_description, logo_path)
        self.click_next_step()
        self.page.wait_for_timeout(2500)

        self.add_subscription_version(version_name, version_description)
        self.click_finish_product_creation()
        self.search_product_solution(product_name)
        row = self.assert_solution_row(product_name)
        return self.product_code_from_row(row)

    def create_product_basic_info_and_go_next(
        self,
        *,
        product_name: str,
        product_description: str,
        logo_path: Path,
    ) -> None:
        self.open_product_solution_list()
        self.click_first_visible(
            [
                "button:has-text('新建产品')",
                "button:has-text('新增产品')",
                "button:has-text('鏂板缓浜у搧')",
                "button:has-text('鏂板浜у搧')",
            ],
            timeout=5000,
        )
        self.page.wait_for_timeout(1500)
        self.fill_required_product_basic_info(product_name, product_description, logo_path)
        self.click_next_step()
        self.page.wait_for_timeout(2500)
        self._ensure_product_version_step_active()
        self.assert_product_version_step_visible()

    def fill_product_basic_info(self, product_name: str, product_description: str, logo_path: Path) -> None:
        self.fill_required_product_basic_info(product_name, product_description, logo_path)
        self._select_by_text(["平台", "骞冲彴"], required=False)
        self._ensure_checked_texts(["企业商户", "个体工商户", "个人用户", "浼佷笟", "涓綋", "涓汉"])
        self._select_by_text(["到期提醒", "鍒版湡鎻愰啋"], required=False)
        self._ensure_checked_texts(["到期前30天", "到期前14天", "到期前7天", "到期前3天", "到期前1天", "当天"])

    def fill_required_product_basic_info(self, product_name: str, product_description: str, logo_path: Path) -> None:
        self._fill_product_name(product_name)
        self._upload_first_file_input(logo_path)
        self._fill_product_description(product_description)

    def _fill_product_name(self, product_name: str) -> None:
        field = self.page.locator(
            ".ant-modal:visible input[placeholder='请输入产品名称'], "
            ".ant-modal:visible input[placeholder*='产品名称']"
        ).first
        self.fill_and_verify(field, product_name, timeout=5000)

    def _fill_product_description(self, product_description: str) -> None:
        field = self.page.locator(
            ".ant-modal:visible textarea[placeholder='请输入产品描述'], "
            ".ant-modal:visible textarea[placeholder*='产品描述']"
        ).first
        expect(field).to_be_visible(timeout=5000)
        field.fill(product_description)
        expect(field).to_have_value(product_description, timeout=3000)

    def add_subscription_version(self, version_name: str, version_description: str) -> None:
        self._click_add_subscription_version_button()
        self.page.wait_for_timeout(1200)
        container = self._active_subscription_version_modal()

        self._fill_in_container(container, ["版本名称", "鐗堟湰鍚嶇О"], version_name)
        self._fill_textarea_in_container(container, ["版本描述", "版本说明", "鐗堟湰鎻忚堪", "鐗堟湰璇存槑"], version_description)
        self._select_by_text(["订阅开通", "璁㈤槄寮€閫"], required=False, container=container)
        self._select_by_text(["启用", "鍚敤"], required=False, container=container)

        self._click_save_subscription_version()
        self.page.wait_for_timeout(2500)
        expect(self.page.get_by_text(version_name, exact=False).first).to_be_visible(timeout=8000)

    def click_next_step(self) -> None:
        self.click_first_visible(
            [
                "button:has-text('下一步')",
                "button:has-text('下一步，版本管理')",
                "button:has-text('涓嬩竴姝')",
            ],
            timeout=5000,
        )

    def assert_product_version_step_visible(self) -> None:
        heading = self.page.locator("h3, h2, .ant-modal").filter(has_text="版本管理").first
        expect(heading).to_be_visible(timeout=8000)
        expect(
            self.page.locator("button:has-text('新增版本'), button:has-text('新建版本'), button:has-text('鏂板鐗堟湰')").first
        ).to_be_visible(timeout=8000)

    def _ensure_product_version_step_active(self) -> None:
        if self.page.locator("h3:has-text('版本管理'), h2:has-text('版本管理')").count():
            return
        step_button = self.page.locator("button", has_text="步骤 2：版本管理").first
        if step_button.count():
            step_button.click()
            self.page.wait_for_timeout(1000)

    def click_finish_product_creation(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null) || document.body;
              const buttons = Array.from(modal.querySelectorAll('button'))
                .filter(button => button.offsetParent !== null && !button.disabled);
              const byText = buttons.find(button => /完成|保存|提交|确定|瀹屾垚|淇濆瓨|鎻愪氦|纭畾/.test(button.innerText || ''));
              const fallback = buttons.reverse().find(button => !/取消|关闭|返回|取 消|鍙栨秷|鍏抽棴/.test(button.innerText || ''));
              const button = byText || fallback;
              if (!button) return false;
              button.click();
              return true;
            }"""
        )
        assert clicked, "Could not click finish product creation button"
        self.page.wait_for_timeout(2500)

    def search_product_solution(self, product_name: str) -> None:
        self.open_product_solution_list()
        self.fill_first_visible(
            [
                "input[placeholder*='产品名称']",
                "input[placeholder*='产品编号']",
                "input[placeholder*='浜у搧鍚嶇О']",
                "input[placeholder*='浜у搧缂栧彿']",
            ],
            product_name,
            timeout=5000,
        )
        self.click_first_visible(
            [
                "button:has-text('查询')",
                "button:has-text('搜索')",
                "button:has-text('鏌ヨ')",
                "button:has-text('鎼滅储')",
            ],
            timeout=3000,
        )
        self.page.wait_for_timeout(1500)

    def assert_solution_row(self, product_name: str) -> Locator:
        row = self.find_table_row(product_name)
        expect(row).to_be_visible(timeout=8000)
        row_text = row.inner_text(timeout=3000)
        assert product_name in row_text, f"Product row does not contain product name: {product_name}; row={row_text}"
        return row

    def latest_product_code_by_name_keyword(self, product_name_keyword: str) -> str:
        self.search_product_solution(product_name_keyword)
        rows = self.page.locator(".ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            if product_name_keyword in row_text:
                return self.product_code_from_row(row)
        raise AssertionError(f"Could not find product row by keyword: {product_name_keyword}")

    def edit_latest_product_basic_info_and_finish(
        self,
        *,
        product_name_keyword: str,
        new_product_name: str,
        new_description: str,
    ) -> tuple[str, str]:
        self.search_product_solution(product_name_keyword)
        row = self.find_product_row_by_keyword(product_name_keyword)
        product_code = self.product_code_from_row(row)
        before_version_count = self.version_count_from_row(row)

        self.click_row_action(row, ["编辑", "缂栬緫"])
        self.page.wait_for_timeout(1500)
        self._fill_product_name(new_product_name)
        self._fill_product_description(new_description)
        self.click_next_step()
        self.page.wait_for_timeout(2000)
        self._ensure_product_version_step_active()
        self.assert_product_version_step_visible()
        self.click_finish_product_creation()

        self.search_product_solution(new_product_name)
        updated_row = self.assert_solution_row(new_product_name)
        updated_text = updated_row.inner_text(timeout=3000)
        assert new_product_name in updated_text, f"Edited product name not found in list row: {updated_text}"
        assert new_description in updated_text, f"Edited product description not found in list row: {updated_text}"
        after_version_count = self.version_count_from_row(updated_row)
        assert after_version_count == before_version_count, (
            f"Version count changed unexpectedly: before={before_version_count}, after={after_version_count}"
        )
        return product_code, new_product_name

    def assert_app_has_existing_version(self, app_code: str) -> tuple[str, str]:
        row = self.search_app_by_code(app_code)
        app_name = self.app_name_from_app_row(row, app_code)
        self.open_app_version_management(app_code)
        has_version = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return false;
              const text = modal.innerText || '';
              if (/暂无数据|无数据|No Data/i.test(text)) return false;
              const rows = Array.from(modal.querySelectorAll('.ant-table-row, .ant-list-item, [class*="version"]'))
                .filter(element => element.offsetParent !== null && (element.innerText || '').trim());
              return rows.length > 0 || /\\b\\d+(\\.\\d+)*\\b/.test(text);
            }"""
        )
        assert has_version, f"App has no readable version in version management: {app_code}"
        self.close_visible_modal_or_drawer()
        return app_code, app_name

    def pack_existing_product_and_read_back(
        self,
        *,
        product_name_keywords: list[str],
        apps: list[tuple[str, str]],
    ) -> tuple[str, str]:
        row = self.open_product_pack_by_keywords(product_name_keywords)
        product_code = self.product_code_from_row(row)
        product_name = self.product_name_from_row(row)

        for app_code, _app_name in apps:
            self.add_pack_app_by_code(app_code)

        self.assert_pack_selected_apps(apps)
        self.save_product_pack()

        self.open_product_pack_by_keywords([product_name, product_code])
        self.assert_pack_selected_apps(apps)
        self.close_visible_modal_or_drawer()
        return product_code, product_name

    def edit_product_version_app_config_and_finish(
        self,
        *,
        product_name_keywords: list[str],
    ) -> tuple[str, str, str]:
        row = self.open_product_edit_version_step_by_keywords(product_name_keywords)
        product_code = self.product_code_from_row(row)
        product_name = self.product_name_from_row(row)

        self.open_first_subscription_version_edit()
        self.scroll_subscription_version_modal_to_bottom()
        app_code, selected_version_name = self.switch_enabled_experience_app_version_in_subscription_version()
        self.assert_subscription_version_selected_app_version(app_code, selected_version_name)
        self._click_save_subscription_version()
        self.page.wait_for_timeout(2500)

        self.assert_product_version_step_visible()
        self.click_finish_product_creation()
        self.search_product_solution(product_name)
        self.assert_solution_row(product_name)
        return product_code, product_name, f"{app_code}/{selected_version_name}"

    def add_version_with_app_and_config_feature_compare(
        self,
        *,
        product_name_keywords: list[str],
        version_name: str,
        version_description: str,
        switch_item_name: str,
        text_item_name: str,
        text_item_value: str,
    ) -> tuple[str, str, str]:
        row = self.open_any_existing_version_product_edit_version_step()
        product_code = self.product_code_from_row(row)
        product_name = self.product_name_from_row(row)

        app_code, selected_app_version_name = self.add_subscription_version_with_enabled_app_version(
            version_name,
            version_description,
        )
        self.assert_product_version_row_state(version_name)
        self.click_finish_product_creation()
        self.search_product_solution(product_name)
        self.assert_solution_row(product_name)

        self.open_product_view(product_name)
        self.open_feature_compare_config()
        self.add_feature_compare_switch_and_text_rows(
            version_name=version_name,
            switch_item_name=switch_item_name,
            text_item_name=text_item_name,
            text_item_value=text_item_value,
        )
        self.save_feature_compare_config()
        return product_code, product_name, f"{app_code}/{selected_app_version_name}"

    def add_subscription_version_with_enabled_app_version(
        self,
        version_name: str,
        version_description: str,
    ) -> tuple[str, str]:
        self._click_add_subscription_version_button()
        self.page.wait_for_timeout(1200)
        container = self._active_subscription_version_modal()

        self._fill_in_container(container, ["版本名称", "鐗堟湰鍚嶇О"], version_name)
        self._fill_textarea_in_container(
            container,
            ["版本描述", "版本说明", "鐗堟湰鎻忚堪", "鐗堟湰璇存槑"],
            version_description,
        )
        self._select_by_text(["订阅开通", "鐠併垽妲勫鈧柅"], required=False, container=container)
        self._select_by_text(["启用", "鍚敤"], required=False, container=container)

        self.scroll_subscription_version_modal_to_bottom()
        app_code, selected_app_version_name = self.switch_enabled_experience_app_version_in_subscription_version()
        self.assert_subscription_version_selected_app_version(app_code, selected_app_version_name)
        self._click_save_subscription_version()
        self.page.wait_for_timeout(2500)
        expect(self.page.get_by_text(version_name, exact=False).first).to_be_visible(timeout=8000)
        return app_code, selected_app_version_name

    def open_product_view(self, product_name: str) -> None:
        self.search_product_solution(product_name)
        row = self.assert_solution_row(product_name)
        self._click_row_action_flexible(row, ["查看"])
        self.page.wait_for_load_state("domcontentloaded")
        self.page.wait_for_timeout(1800)
        expect(self.page.get_by_text(product_name, exact=False).first).to_be_visible(timeout=8000)

    def open_any_existing_version_product_view(self) -> tuple[str, str]:
        self.open_product_solution_list()
        self.reset_product_solution_search_filters()
        last_error: AssertionError | None = None
        for _ in range(8):
            rows = self.page.locator(".ant-table-row")
            for index in range(rows.count()):
                row = rows.nth(index)
                try:
                    row_text = row.inner_text(timeout=1000)
                    if not row_text.strip():
                        continue
                    version_count = self.version_count_from_row(row)
                    if version_count <= 0:
                        continue
                    if not self._row_has_button_text(row, "查看"):
                        continue
                    product_code = self.product_code_from_row(row)
                    product_name = self.product_name_from_row(row)
                    self._click_row_action_flexible(row, ["查看"])
                    self.page.wait_for_load_state("domcontentloaded")
                    self.page.wait_for_timeout(1800)
                    expect(self.page.get_by_text(product_name, exact=False).first).to_be_visible(timeout=8000)
                    return product_code, product_name
                except AssertionError as exc:
                    last_error = exc
                    self.close_all_visible_modals_or_drawers()
                    continue
            if not self.go_next_product_solution_page():
                break
        raise AssertionError("Could not find viewable product with existing versions") from last_error

    def open_feature_compare_config(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const buttons = Array.from(document.querySelectorAll('button, a, span'))
                .filter(element => element.offsetParent !== null && !element.disabled);
              const button = buttons.find(element => {
                const text = (element.innerText || element.textContent || '').replace(/\\s+/g, '');
                return text.includes('功能对比配置');
              });
              if (!button) return false;
              const clickable = button.closest('button, a') || button;
              clickable.click();
              return true;
            }"""
        )
        assert clicked, "Could not click feature compare config entry"
        self.page.wait_for_timeout(1500)
        expect(self._active_feature_compare_modal()).to_be_visible(timeout=8000)

    def add_feature_compare_switch_and_text_rows(
        self,
        *,
        version_name: str,
        switch_item_name: str,
        text_item_name: str,
        text_item_value: str,
    ) -> None:
        self._ensure_feature_compare_row(
            version_name=version_name,
            item_name=text_item_name,
            item_value=text_item_value,
            is_switch=False,
            add_button_text="添加文本行",
        )

        self._ensure_feature_compare_row(
            version_name=version_name,
            item_name=switch_item_name,
            item_value="",
            is_switch=True,
            add_button_text="添加开关行",
        )

        self.assert_feature_compare_preview(
            version_name=version_name,
            switch_item_name=switch_item_name,
            text_item_name=text_item_name,
            text_item_value=text_item_value,
        )

    def save_feature_compare_config(self) -> None:
        modal = self._active_feature_compare_modal()
        self.click_button_in_container(modal, ["保存配置", "保存"], timeout=5000)
        self.page.wait_for_timeout(2000)

    def _ensure_feature_compare_row(
        self,
        *,
        version_name: str,
        item_name: str,
        item_value: str,
        is_switch: bool,
        add_button_text: str,
    ) -> None:
        if self._fill_latest_feature_compare_row(
            version_name=version_name,
            item_name=item_name,
            item_value=item_value,
            is_switch=is_switch,
            prefer_blank=True,
            required=False,
        ):
            return

        modal = self._active_feature_compare_modal()
        self.click_button_in_container(modal, [add_button_text], timeout=5000)
        self.page.wait_for_timeout(800)
        self._fill_latest_feature_compare_row(
            version_name=version_name,
            item_name=item_name,
            item_value=item_value,
            is_switch=is_switch,
            prefer_blank=False,
            required=True,
        )

    def assert_product_version_create_operation_log(self, *, product_code: str) -> None:
        self.assert_operation_log(target_type="产品", target_code=product_code, operation_info="新建产品版本")

    def assert_feature_compare_config_operation_log(self, *, product_code: str) -> None:
        self.open_operation_log()
        self.select_filter_value("目标类型", "产品")
        self.fill_first_visible(
            [
                "input[placeholder*='目标编码']",
                "input[placeholder*='请输入目标编码']",
            ],
            product_code,
            timeout=5000,
        )
        self.click_first_visible(
            [
                "button:has-text('查询')",
                "button:has-text('搜索')",
            ],
            timeout=3000,
        )
        self.page.wait_for_timeout(1500)
        row = self.find_latest_feature_compare_operation_log_row(product_code)
        row_text = row.inner_text(timeout=1000)
        assert product_code in row_text, f"操作日志目标编码不匹配，期望 {product_code}，实际行：{row_text}"
        assert "成功" in row_text, f"功能对比配置操作日志未显示成功状态，实际行：{row_text}"

    def find_latest_feature_compare_operation_log_row(self, product_code: str) -> Locator:
        rows = self.page.locator(".ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            normalized_text = re.sub(r"\s+", "", row_text)
            if (
                product_code in row_text
                and "成功" in row_text
                and (
                    "功能对比" in normalized_text
                    or "保存配置" in normalized_text
                    or "对比配置" in normalized_text
                )
            ):
                expect(row).to_be_visible(timeout=3000)
                return row
        raise AssertionError(f"未找到功能对比配置成功操作日志：target_code={product_code}")

    def product_version_status_copy_delete_and_finish(
        self,
        *,
        product_name_keywords: list[str],
        copied_version_name: str,
    ) -> tuple[str, str, str]:
        row = self.open_product_edit_version_step_by_keywords(product_name_keywords)
        product_code = self.product_code_from_row(row)
        product_name = self.product_name_from_row(row)

        target_version_name = self.first_product_version_name_with_action("禁用", status_text="启用")
        self.click_product_version_row_action("禁用", version_name=target_version_name)
        self.confirm_visible_popconfirm_or_modal()
        self.page.wait_for_timeout(1500)
        self.assert_product_version_row_state(target_version_name, status_text="禁用", action_text="启用")

        self.click_product_version_row_action("启用", version_name=target_version_name)
        self.confirm_visible_popconfirm_or_modal()
        self.page.wait_for_timeout(1500)
        self.assert_product_version_row_state(target_version_name, status_text="启用", action_text="禁用")

        self.click_product_version_row_action("复制", version_name=target_version_name)
        self.page.wait_for_timeout(1500)
        copy_modal = self._active_subscription_version_modal()
        self._fill_in_container(copy_modal, ["版本名称"], copied_version_name)
        self._click_save_subscription_version()
        self.page.wait_for_timeout(2500)
        self.assert_product_version_row_state(copied_version_name)

        self.click_product_version_row_action("删除", version_name=copied_version_name)
        self.confirm_visible_popconfirm_or_modal()
        self.page.wait_for_timeout(2000)
        self.assert_product_version_not_present(copied_version_name)

        self.click_finish_product_creation()
        self.search_product_solution(product_name)
        self.assert_solution_row(product_name)
        return product_code, product_name, copied_version_name

    def publish_online_offline_online_and_read_back(
        self,
        *,
        product_name_keywords: list[str],
    ) -> tuple[str, str]:
        row = self.find_product_row_by_keywords_and_action(product_name_keywords, "发布")
        product_code = self.product_code_from_row(row)
        product_name = self.product_name_from_row(row)

        self.click_product_row_action_and_confirm(product_name, "发布")
        self.assert_product_row_state_and_action(product_name, status_text="已下架", action_text="上架")

        self.click_product_row_action_and_confirm(product_name, "上架")
        self.assert_product_row_state_and_action(product_name, status_text="已上架", action_text="下架")

        self.click_product_row_action_and_confirm(product_name, "下架")
        self.assert_product_row_state_and_action(product_name, status_text="已下架", action_text="上架")

        self.click_product_row_action_and_confirm(product_name, "上架")
        self.assert_product_row_state_and_action(product_name, status_text="已上架", action_text="下架")
        return product_code, product_name

    def create_product_menu_and_read_back(
        self,
        *,
        product_name_keywords: list[str],
        menu_name: str,
        menu_code: str,
    ) -> tuple[str, str, str]:
        row = self.open_product_menu_config_by_keywords(product_name_keywords)
        product_code = self.product_code_from_row(row)
        product_name = self.product_name_from_row(row)
        self.create_solution_menu(
            category_text="产品菜单",
            menu_name=menu_name,
            menu_code=menu_code,
            route_path=f"/auto-product-menu/{menu_code}",
            component_path=f"views/auto/product/{menu_code}",
            icon_text="Menu",
        )
        self.search_solution_menu(menu_name, menu_code)
        self.assert_solution_menu_row(menu_name, menu_code)
        self.close_visible_modal_or_drawer()
        return product_code, product_name, menu_code

    def create_app_menu_and_read_back(
        self,
        *,
        product_name_keywords: list[str],
        menu_name: str,
        menu_code: str,
    ) -> tuple[str, str, str]:
        row = self.open_product_menu_config_by_keywords(product_name_keywords)
        product_code = self.product_code_from_row(row)
        product_name = self.product_name_from_row(row)
        self.create_solution_menu(
            category_text="应用菜单",
            menu_name=menu_name,
            menu_code=menu_code,
            route_path=f"/auto-app-menu/{menu_code}",
            component_path=f"views/auto/app/{menu_code}",
            icon_text="Menu",
        )
        self.search_solution_menu(menu_name, menu_code)
        self.assert_solution_menu_row(menu_name, menu_code)
        self.close_visible_modal_or_drawer()
        return product_code, product_name, menu_code

    def open_product_menu_config_by_keywords(self, product_name_keywords: list[str]) -> Locator:
        last_error: AssertionError | None = None
        for keyword in [item for item in product_name_keywords if item]:
            self.search_product_solution(keyword)
            try:
                row = self.find_product_row_with_action(keyword, ["配置菜单"])
                self._click_row_action_flexible(row, ["配置菜单"])
                self.page.wait_for_timeout(1500)
                expect(self._active_solution_menu_modal()).to_be_visible(timeout=8000)
                return row
            except AssertionError as exc:
                last_error = exc
                self.close_all_visible_modals_or_drawers()
                continue
        raise AssertionError(f"Could not open product menu config by keywords: {product_name_keywords}") from last_error

    def _active_solution_menu_modal(self) -> Locator:
        modal = self.page.locator(".ant-modal:visible, .ant-drawer:visible").filter(has_text="菜单").last
        if modal.count():
            return modal
        raise AssertionError("Product solution menu modal is not visible")

    def create_solution_menu(
        self,
        *,
        category_text: str,
        menu_name: str,
        menu_code: str,
        route_path: str,
        component_path: str,
        icon_text: str,
    ) -> None:
        modal = self._active_solution_menu_modal()
        self.click_button_in_container(modal, ["创建菜单", "新建菜单", "新增菜单"], timeout=5000)
        self.page.wait_for_timeout(1200)
        form = self._active_solution_menu_form()

        self._choose_solution_menu_radio(category_text)
        self._choose_solution_menu_radio("菜单")
        self._choose_solution_menu_radio("正常")
        self._select_first_available_app_in_menu_form()

        self._fill_solution_menu_form_fields(
            menu_name=menu_name,
            menu_code=menu_code,
            route_path=route_path,
            component_path=component_path,
            icon_text=icon_text,
        )
        self._select_first_solution_menu_option(["菜单图标", "请选择图标", "应用图标", "请选择应用图标"], required=False)
        self._click_create_solution_menu_submit()
        self.page.wait_for_timeout(2500)
        self.assert_create_solution_menu_submitted()

    def _active_solution_menu_form(self) -> Locator:
        modal = self.page.locator(".ant-modal:visible, .ant-drawer:visible").filter(has_text="创建菜单").last
        if modal.count():
            return modal
        modal = self.page.locator(".ant-modal:visible, .ant-drawer:visible").filter(has_text="编辑菜单").last
        if modal.count():
            return modal
        return self._active_solution_menu_modal()

    def _choose_solution_menu_radio(self, label_text: str) -> None:
        clicked = self.page.evaluate(
            """(labelText) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .filter(element => element.offsetParent !== null);
              const root = [...modals].reverse().find(element => {
                const text = element.innerText || '';
                return /当前产品/.test(text) && /菜单名称/.test(text);
              }) || [...modals].reverse().find(element => /创建菜单|编辑菜单/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!root) return false;
              const valueByLabel = {
                '产品菜单': 'product',
                '应用菜单': 'application',
                '菜单': 'menu',
                '正常': '1',
                '禁用': '0',
              };
              const expectedValue = valueByLabel[labelText];
              if (expectedValue) {
                const input = Array.from(root.querySelectorAll('input[type="radio"]'))
                  .find(item => item.offsetParent !== null && item.value === expectedValue);
                if (input) {
                  if (!input.checked) {
                    (input.closest('label') || input).click();
                  }
                  return true;
                }
              }
              const labels = Array.from(root.querySelectorAll('label.ant-radio-wrapper, label'))
                .filter(label => label.offsetParent !== null);
              const label = labels.find(item => (item.innerText || item.textContent || '').replace(/\\s+/g, '') === labelText.replace(/\\s+/g, '')) ||
                labels.find(item => (item.innerText || item.textContent || '').includes(labelText));
              if (!label) return false;
              const input = label.querySelector('input[type="radio"]');
              if (!input || input.checked) return true;
              label.click();
              return true;
            }""",
            label_text,
        )
        assert clicked, f"Could not choose menu radio: {label_text}"
        self.page.wait_for_timeout(500)

    def _select_first_available_app_in_menu_form(self) -> None:
        selected = self._select_first_solution_menu_app()
        if selected:
            return
        self._select_first_solution_menu_option(["选择应用", "请选择应用"], required=True)

    def _select_first_solution_menu_app(self) -> bool:
        app_input = self.page.locator(".ant-modal:visible input#appId, .ant-drawer:visible input#appId").last
        if app_input.count():
            try:
                app_input.scroll_into_view_if_needed(timeout=1000)
                select = app_input.locator("xpath=ancestor::*[contains(@class, 'ant-select')][1]")
                box = select.bounding_box(timeout=1000)
                if box:
                    self.page.mouse.click(box["x"] + box["width"] - 16, box["y"] + box["height"] / 2)
                    for _ in range(12):
                        option = self.page.locator(
                            ".ant-select-dropdown:not(.ant-select-dropdown-hidden) "
                            ".ant-select-item-option:not(.ant-select-item-option-disabled)"
                        ).first
                        if option.count():
                            try:
                                expect(option).to_be_visible(timeout=500)
                                option.click(force=True)
                                self.page.wait_for_timeout(800)
                                return True
                            except Exception:
                                pass
                        self.page.wait_for_timeout(500)
            except Exception:
                pass

        opened = self.page.evaluate(
            """() => {
              const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .filter(element => element.offsetParent !== null);
              const root = [...modals].reverse().find(element => {
                const text = element.innerText || '';
                return /当前产品/.test(text) && /菜单名称/.test(text);
              }) || modals[modals.length - 1];
              if (!root) return false;
              const input = root.querySelector('input#appId, input[aria-owns="appId_list"], input[aria-controls="appId_list"]');
              const select = input && input.closest('.ant-select');
              const target = select && (select.querySelector('.ant-select-arrow') || select.querySelector('.ant-select-selector') || select);
              if (!target) return false;
              const rect = target.getBoundingClientRect();
              target.dispatchEvent(new MouseEvent('mousedown', {
                bubbles: true,
                clientX: rect.right - 8,
                clientY: rect.top + rect.height / 2,
              }));
              target.click();
              return true;
            }"""
        )
        if not opened:
            return False
        for _ in range(12):
            self.page.wait_for_timeout(500)
            selected = self.page.evaluate(
                """() => {
                  const dropdowns = Array.from(document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)'))
                    .filter(dropdown => dropdown.offsetParent !== null && /appId_list|请选择应用|应用/.test(dropdown.innerHTML || dropdown.innerText || ''));
                  const roots = dropdowns.length ? dropdowns : Array.from(document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)'))
                    .filter(dropdown => dropdown.offsetParent !== null);
                  const options = roots.flatMap(root => Array.from(root.querySelectorAll('.ant-select-item-option:not(.ant-select-item-option-disabled)')))
                    .filter(option => {
                      const text = (option.innerText || option.textContent || '').replace(/\\s+/g, ' ').trim();
                      const rect = option.getBoundingClientRect();
                      return text && rect.width > 0 && rect.height > 0 && !/无数据|暂无数据/.test(text);
                    });
                  const option = options[0];
                  if (!option) return false;
                  option.click();
                  return true;
                }"""
            )
            if selected:
                self.page.wait_for_timeout(800)
                return True
        return False

    def _select_first_solution_menu_option(self, label_texts: list[str], *, required: bool) -> bool:
        for label_text in label_texts:
            selects = self.page.locator(".ant-modal:visible .ant-select, .ant-drawer:visible .ant-select").filter(
                has_text=label_text
            )
            for index in range(selects.count() - 1, -1, -1):
                select = selects.nth(index)
                try:
                    expect(select).to_be_visible(timeout=1000)
                    arrow = select.locator(".ant-select-arrow").first
                    if arrow.count():
                        arrow.click(force=True)
                    else:
                        box = select.bounding_box(timeout=1000)
                        if box:
                            select.click(position={"x": max(box["width"] - 8, 1), "y": box["height"] / 2}, force=True)
                        else:
                            select.click(force=True)
                    for _ in range(10):
                        option = self.page.locator(
                            ".ant-select-dropdown:not(.ant-select-dropdown-hidden) "
                            ".ant-select-item-option:not(.ant-select-item-option-disabled)"
                        ).first
                        if option.count():
                            try:
                                expect(option).to_be_visible(timeout=500)
                                option.click(force=True)
                                self.page.wait_for_timeout(500)
                                return True
                            except Exception:
                                pass
                        self.page.wait_for_timeout(300)
                except Exception:
                    continue

        opened = self.page.evaluate(
            """(labelTexts) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .filter(element => element.offsetParent !== null);
              const root = [...modals].reverse().find(element => {
                const text = element.innerText || '';
                return /当前产品/.test(text) && /菜单名称/.test(text);
              }) || [...modals].reverse().find(element => /创建菜单|编辑菜单/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!root) return false;
              const compact = (value) => (value || '').replace(/\\s+/g, '');
              const selects = Array.from(root.querySelectorAll('.ant-select'))
                .filter(item => item.offsetParent !== null);
              const selectRoot = selects.find(item => {
                const text = compact(item.innerText || item.textContent || '');
                return labelTexts.some(label => text.includes(compact(label)));
              });
              const select = selectRoot && (
                selectRoot.querySelector('.ant-select-arrow') ||
                selectRoot.querySelector('.ant-select-selector') ||
                selectRoot
              );
              if (!select) return false;
              const rect = select.getBoundingClientRect();
              select.dispatchEvent(new MouseEvent('mousedown', {
                bubbles: true,
                clientX: rect.right - 8,
                clientY: rect.top + rect.height / 2,
              }));
              select.click();
              return true;
            }""",
            label_texts,
        )
        if not opened:
            assert not required, f"Could not open menu select: {label_texts}"
            return False
        selected = False
        for _ in range(10):
            self.page.wait_for_timeout(500)
            selected = self.page.evaluate(
                """() => {
                  const options = Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option, ' +
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content, ' +
                    '.rc-virtual-list-holder-inner .ant-select-item-option'
                  )).filter(option => {
                    const text = (option.innerText || option.textContent || '').replace(/\\s+/g, ' ').trim();
                    const rect = option.getBoundingClientRect();
                    return text && rect.width > 0 && rect.height > 0 && !/禁用|无数据|暂无数据/.test(text);
                  });
                  const option = options[0];
                  if (option) {
                    (option.closest('.ant-select-item-option') || option).click();
                    return true;
                  }
                  return false;
                }"""
            )
            if selected:
                break
        assert selected or not required, f"Could not select menu option: {label_texts}"
        self.page.wait_for_timeout(500)
        return bool(selected)

    def _click_create_solution_menu_submit(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .filter(element => element.offsetParent !== null);
              const root = [...modals].reverse().find(element => {
                const text = element.innerText || '';
                return /当前产品/.test(text) && /菜单名称/.test(text);
              }) || modals[modals.length - 1];
              if (!root) return false;
              const buttons = Array.from(root.querySelectorAll('button'))
                .filter(button => button.offsetParent !== null && !button.disabled);
              const button = buttons.find(item => (item.innerText || item.textContent || '').replace(/\\s+/g, '') === '创建') ||
                buttons.find(item => item.type === 'submit') ||
                buttons.reverse().find(item => !/取消|关闭/.test((item.innerText || item.textContent || '').replace(/\\s+/g, '')));
              if (!button) return false;
              button.click();
              return true;
            }"""
        )
        assert clicked, "Could not click create solution menu submit button"

    def assert_create_solution_menu_submitted(self) -> None:
        still_open = self.page.locator(".ant-modal:visible, .ant-drawer:visible").filter(
            has_text="当前产品"
        ).filter(has_text="菜单名称").last
        if not still_open.count():
            return
        text = still_open.inner_text(timeout=3000)
        errors = self.page.locator(
            ".ant-modal:visible .ant-form-item-explain-error, "
            ".ant-modal:visible .ant-message-error, "
            ".ant-modal:visible .ant-alert-error"
        )
        error_texts: list[str] = []
        for index in range(errors.count()):
            try:
                value = errors.nth(index).inner_text(timeout=500).strip()
            except Exception:
                continue
            if value:
                error_texts.append(value)
        raise AssertionError(f"创建菜单提交后弹窗未关闭；errors={error_texts}; text={text[:1500]}")

    def _fill_solution_menu_form_fields(
        self,
        *,
        menu_name: str,
        menu_code: str,
        route_path: str,
        component_path: str,
        icon_text: str,
    ) -> None:
        menu_name_input = self.page.locator(".ant-modal:visible input[placeholder*='菜单名称']").last
        if menu_name_input.count():
            self.fill_and_verify(menu_name_input, menu_name, timeout=5000)
            description = self.page.locator(".ant-modal:visible textarea[placeholder*='菜单描述']").last
            if description.count():
                description.fill(f"自动化创建菜单 {menu_code}")
            route_input = self.page.locator(".ant-modal:visible input[placeholder*='路由路径']").last
            if route_input.count():
                self.fill_and_verify(route_input, route_path, timeout=3000)
            component_input = self.page.locator(".ant-modal:visible input[placeholder*='组件路径']").last
            if component_input.count():
                self.fill_and_verify(component_input, component_path, timeout=3000)
            self._select_first_solution_menu_option(["上级菜单", "顶级菜单"], required=False)
            self._select_first_solution_menu_option(["菜单图标", "请选择图标", "应用图标", "请选择应用图标"], required=False)
            return

        filled = self.page.evaluate(
            """({ menuName, menuCode, routePath, componentPath, iconText }) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .filter(element => element.offsetParent !== null);
              const root = [...modals].reverse().find(element => {
                const text = element.innerText || '';
                return /当前产品/.test(text) && /菜单名称/.test(text);
              }) || [...modals].reverse().find(element => /创建菜单|编辑菜单/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!root) return false;
              const setValue = (element, value) => {
                const proto = element.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
                const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
                element.focus();
                descriptor.set.call(element, '');
                element.dispatchEvent(new Event('input', { bubbles: true }));
                descriptor.set.call(element, value);
                element.dispatchEvent(new Event('input', { bubbles: true }));
                element.dispatchEvent(new Event('change', { bubbles: true }));
              };
              const visibleInputs = Array.from(root.querySelectorAll('input:not([type="hidden"]):not([disabled]), textarea:not([disabled])'))
                .filter(input => input.offsetParent !== null && !['search', 'checkbox', 'radio'].includes((input.getAttribute('type') || '').toLowerCase()));
              const byMeta = (patterns) => visibleInputs.find(input => {
                const text = `${input.placeholder || ''} ${input.getAttribute('aria-label') || ''} ${input.name || ''}`.replace(/\\s+/g, ' ');
                return patterns.some(pattern => pattern.test(text));
              });
              const fillNearLabel = (patterns, value) => {
                const nodes = Array.from(root.querySelectorAll('label, .ant-form-item-label, span, div'))
                  .filter(node => node.offsetParent !== null && patterns.some(pattern => pattern.test(node.innerText || node.textContent || '')));
                for (const node of nodes) {
                  const scope = node.closest('.ant-form-item, .ant-row, div') || node.parentElement;
                  const input = scope && Array.from(scope.querySelectorAll('input:not([type="hidden"]):not([disabled]), textarea:not([disabled])'))
                    .find(item => item.offsetParent !== null && !['search', 'checkbox', 'radio'].includes((item.getAttribute('type') || '').toLowerCase()));
                  if (input) {
                    setValue(input, value);
                    return true;
                  }
                }
                const input = byMeta(patterns);
                if (input) {
                  setValue(input, value);
                  return true;
                }
                return false;
              };
              const menuNameInput = visibleInputs.find(input => /请输入菜单名称|菜单名称/.test(input.placeholder || ''));
              if (menuNameInput) {
                setValue(menuNameInput, menuName);
              } else {
                fillNearLabel([/菜单名称|名称|menu.*name/i], menuName);
              }
              fillNearLabel([/排序|sort|order/i], '100');
              fillNearLabel([/菜单描述|描述|说明|remark|description/i], `自动化创建菜单 ${menuCode}`);
              return root.innerText.includes(menuName) || visibleInputs.some(input => input.value === menuName);
            }""",
            {
                "menuName": menu_name,
                "menuCode": menu_code,
                "routePath": route_path,
                "componentPath": component_path,
                "iconText": icon_text,
            },
        )
        assert filled, f"Could not fill solution menu form: {menu_name}/{menu_code}"

    def search_solution_menu(self, menu_name: str, menu_code: str) -> None:
        modal = self._active_solution_menu_modal()
        filled = self.page.evaluate(
            """({ menuName, menuCode }) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .filter(element => element.offsetParent !== null);
              const root = modals[modals.length - 1];
              if (!root) return false;
              const inputs = Array.from(root.querySelectorAll('input:not([type="hidden"]):not([disabled])'))
                .filter(input => input.offsetParent !== null && !['search', 'checkbox', 'radio'].includes((input.getAttribute('type') || '').toLowerCase()));
              const input = inputs.find(item => /菜单|名称|编码|查询|搜索|keyword|code|name/i.test(
                `${item.placeholder || ''} ${item.getAttribute('aria-label') || ''} ${item.name || ''}`
              )) || inputs[0];
              if (!input) return true;
              const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
              input.focus();
              descriptor.set.call(input, '');
              input.dispatchEvent(new Event('input', { bubbles: true }));
              descriptor.set.call(input, menuName || menuCode);
              input.dispatchEvent(new Event('input', { bubbles: true }));
              input.dispatchEvent(new Event('change', { bubbles: true }));
              return true;
            }""",
            {"menuName": menu_name, "menuCode": menu_code},
        )
        assert filled, f"Could not search solution menu: {menu_name}/{menu_code}"
        try:
            self.click_button_in_container(modal, ["查询", "搜索"], timeout=2000)
        except AssertionError:
            pass
        self.page.wait_for_timeout(1500)

    def assert_solution_menu_row(self, menu_name: str, menu_code: str) -> Locator:
        modal = self._active_solution_menu_modal()
        rows = modal.locator(".ant-table-row, tr, .ant-tree-treenode, .ant-list-item")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                if row.is_visible(timeout=500):
                    row_text = row.inner_text(timeout=1000)
                    if menu_name in row_text or menu_code in row_text:
                        return row
            except Exception:
                continue
        text = modal.inner_text(timeout=3000)
        assert menu_name in text or menu_code in text, f"新建菜单未在列表回读：{menu_name}/{menu_code}; text={text[:1500]}"
        return modal

    def open_product_edit_version_step_by_keywords(self, product_name_keywords: list[str]) -> Locator:
        last_error: AssertionError | None = None
        for keyword in [item for item in product_name_keywords if item]:
            self.search_product_solution(keyword)
            try:
                row = self.find_product_row_by_keyword(keyword)
                self._click_row_action_flexible(row, ["编辑"])
                self.page.wait_for_timeout(1500)
                self.click_next_step()
                self.page.wait_for_timeout(2000)
                self._ensure_product_version_step_active()
                self.assert_product_version_step_visible()
                return row
            except AssertionError as exc:
                last_error = exc
                self.close_all_visible_modals_or_drawers()
                continue
        raise AssertionError(f"Could not open product edit version step by keywords: {product_name_keywords}") from last_error

    def open_existing_version_product_edit_version_step_by_keywords(self, product_name_keywords: list[str]) -> Locator:
        last_error: AssertionError | None = None
        for keyword in [item for item in product_name_keywords if item]:
            self.search_product_solution(keyword)
            try:
                row = self.find_product_row_by_keyword(keyword)
                version_count = self.version_count_from_row(row)
                assert version_count > 0, f"Product has no existing version: {keyword}"
                self._click_row_action_flexible(row, ["编辑"])
                self.page.wait_for_timeout(1500)
                self.click_next_step()
                self.page.wait_for_timeout(2000)
                self._ensure_product_version_step_active()
                self.assert_product_version_step_visible()
                return row
            except AssertionError as exc:
                last_error = exc
                self.close_all_visible_modals_or_drawers()
                continue
        raise AssertionError(
            f"Could not open existing-version product edit version step by keywords: {product_name_keywords}"
        ) from last_error

    def open_any_existing_version_product_edit_version_step(self) -> Locator:
        self.open_product_solution_list()
        self.reset_product_solution_search_filters()
        last_error: AssertionError | None = None
        for _ in range(8):
            rows = self.page.locator(".ant-table-row")
            for index in range(rows.count()):
                row = rows.nth(index)
                try:
                    row_text = row.inner_text(timeout=1000)
                    if not row_text.strip():
                        continue
                    version_count = self.version_count_from_row(row)
                    if version_count <= 0:
                        continue
                    if not self._row_has_button_text(row, "编辑"):
                        continue
                    self._click_row_action_flexible(row, ["编辑"])
                    self.page.wait_for_timeout(1500)
                    self.click_next_step()
                    self.page.wait_for_timeout(2000)
                    self._ensure_product_version_step_active()
                    self.assert_product_version_step_visible()
                    return row
                except AssertionError as exc:
                    last_error = exc
                    self.close_all_visible_modals_or_drawers()
                    continue
            if not self.go_next_product_solution_page():
                break
        raise AssertionError("Could not find editable product with existing versions") from last_error

    def reset_product_solution_search_filters(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const buttons = Array.from(document.querySelectorAll('button'))
                .filter(button => button.offsetParent !== null && !button.disabled);
              const reset = buttons.find(button => /重置/.test(button.innerText || button.textContent || ''));
              if (reset) {
                reset.click();
                return true;
              }
              const inputs = Array.from(document.querySelectorAll('input:not([type="hidden"]):not([disabled])'))
                .filter(input => input.offsetParent !== null);
              for (const input of inputs) {
                const text = `${input.placeholder || ''} ${input.getAttribute('aria-label') || ''}`;
                if (/产品|名称|编号|搜索|查询/.test(text)) {
                  input.focus();
                  input.value = '';
                  input.dispatchEvent(new Event('input', { bubbles: true }));
                  input.dispatchEvent(new Event('change', { bubbles: true }));
                }
              }
              const query = buttons.find(button => /查询|搜索/.test(button.innerText || button.textContent || ''));
              if (query) query.click();
              return true;
            }"""
        )
        if clicked:
            self.page.wait_for_timeout(1500)

    def go_next_product_solution_page(self) -> bool:
        clicked = self.page.evaluate(
            """() => {
              const candidates = Array.from(document.querySelectorAll(
                '.ant-pagination-next, .ant-pagination-next button, button[aria-label="Next Page"]'
              )).filter(element => element.offsetParent !== null);
              const next = candidates.find(element => {
                const item = element.closest('.ant-pagination-next') || element;
                const disabled = item.className && String(item.className).includes('disabled');
                return !disabled && !element.disabled && element.getAttribute('aria-disabled') !== 'true';
              });
              if (!next) return false;
              const clickable = next.closest('button') || next;
              clickable.click();
              return true;
            }"""
        )
        if clicked:
            self.page.wait_for_timeout(1500)
            return True
        return False

    def open_first_subscription_version_edit(self) -> None:
        version_rows = self.page.locator(".ant-modal:visible .ant-table-tbody .ant-table-row")
        for index in range(version_rows.count()):
            row = version_rows.nth(index)
            try:
                if not row.is_visible(timeout=500):
                    continue
            except Exception:
                continue
            buttons = row.locator("button:not(.ant-btn-dangerous)")
            for button_index in range(buttons.count()):
                button = buttons.nth(button_index)
                try:
                    text = button.inner_text(timeout=500).replace(" ", "")
                except Exception:
                    continue
                if text == "编辑":
                    button.click(force=True)
                    self.page.wait_for_timeout(1500)
                    expect(self._active_subscription_version_modal()).to_be_visible(timeout=8000)
                    return

        clicked = self.page.evaluate(
            """() => {
              const root = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .reverse()
                .find(element => element.offsetParent !== null) || document.body;
              const rows = Array.from(root.querySelectorAll('.ant-table-row, tr, .ant-card, .ant-list-item, div'))
                .filter(element => {
                  if (element.offsetParent === null) return false;
                  const text = (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim();
                  if (!text) return false;
                  if (/新增版本|版本管理|产品名称|产品描述/.test(text)) return false;
                  return /编辑/.test(text);
                });
              const clickControl = (element) => {
                const clickable = element.closest('button, a') || element;
                clickable.click();
              };
              for (const row of rows) {
                const controls = Array.from(row.querySelectorAll('button, a, span'))
                  .filter(element => element.offsetParent !== null && !element.disabled);
                const edit = controls.find(element => (element.innerText || element.textContent || '').replace(/\\s+/g, '') === '编辑') ||
                  controls.find(element => /编辑/.test((element.innerText || element.textContent || '').replace(/\\s+/g, '')));
                if (edit) {
                  clickControl(edit);
                  return true;
                }
              }
              const buttons = Array.from(root.querySelectorAll('button, a, span'))
                .filter(element => element.offsetParent !== null && !element.disabled);
              const edit = buttons.find(element => (element.innerText || element.textContent || '').replace(/\\s+/g, '') === '编辑') ||
                buttons.find(element => /编辑/.test((element.innerText || element.textContent || '').replace(/\\s+/g, '')));
              if (!edit) return false;
              clickControl(edit);
              return true;
            }"""
        )
        assert clicked, "Could not click subscription version edit entry"
        self.page.wait_for_timeout(1500)
        expect(self._active_subscription_version_modal()).to_be_visible(timeout=8000)

    def add_enabled_experience_app_version_to_subscription_version(
        self,
        *,
        app_code: str,
        preferred_app_version_name: str,
    ) -> str:
        container = self._active_subscription_version_modal()
        self._fill_subscription_app_keyword(container, app_code)
        self._click_subscription_app_query(container)
        self.page.wait_for_timeout(1500)
        self._click_subscription_app_version_config_action(app_code)
        self.page.wait_for_timeout(1200)

        selected_version_name = self._select_enabled_experience_app_version(preferred_app_version_name)
        self.click_visible_modal_button(["确定", "确认", "添加", "保存"], timeout=5000)
        self.page.wait_for_timeout(1500)
        return selected_version_name

    def switch_enabled_experience_app_version_in_subscription_version(self) -> tuple[str, str]:
        self.scroll_subscription_version_modal_to_bottom()
        app_code = self._click_first_available_subscription_app_version_config_action()
        self.page.wait_for_timeout(1200)
        selected_version_name = self._select_enabled_experience_app_version("")
        self.click_visible_modal_button(["确认添加", "确定", "确认", "添加"], timeout=5000)
        self.page.wait_for_timeout(1500)
        return app_code, selected_version_name

    def scroll_subscription_version_modal_to_bottom(self) -> None:
        self.page.evaluate(
            """() => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = [...modals].reverse().find(element => /保存版本|可选应用版本|已选应用版本/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!modal) return;
              const scrollables = Array.from(modal.querySelectorAll('*'))
                .filter(element => {
                  const style = window.getComputedStyle(element);
                  return /(auto|scroll)/.test(style.overflowY) && element.scrollHeight > element.clientHeight;
                });
              for (const element of scrollables) {
                element.scrollTop = element.scrollHeight;
              }
              modal.scrollTop = modal.scrollHeight;
              const body = modal.querySelector('.ant-modal-body');
              if (body) body.scrollTop = body.scrollHeight;
            }"""
        )
        self.page.wait_for_timeout(500)

    def assert_subscription_version_selected_app_version(self, app_code: str, app_version_name: str) -> None:
        container = self._active_subscription_version_modal()
        text = container.inner_text(timeout=3000)
        assert app_version_name in text, (
            f"Selected enabled experience app version not found in subscription version modal: "
            f"{app_version_name}; text={text}"
        )

    def open_product_pack_by_keywords(self, product_name_keywords: list[str]) -> Locator:
        last_error: AssertionError | None = None
        for keyword in product_name_keywords:
            self.search_product_solution(keyword)
            try:
                row = self.find_product_row_with_action(keyword, ["打包"])
                self._click_row_action_flexible(row, ["打包"])
                self.page.wait_for_timeout(1500)
                expect(self._active_pack_modal()).to_be_visible(timeout=8000)
                return row
            except AssertionError as exc:
                last_error = exc
                continue
        raise AssertionError(f"Could not open product pack entry by keywords: {product_name_keywords}") from last_error

    def add_pack_app_by_code(self, app_code: str) -> None:
        modal = self._active_pack_modal()
        self._fill_pack_app_keyword(modal, app_code)
        self._click_pack_query(modal)
        self.page.wait_for_timeout(1500)
        self._click_pack_add_for_app(app_code)
        self.page.wait_for_timeout(800)

    def assert_pack_selected_apps(self, apps: list[tuple[str, str]]) -> None:
        modal = self._active_pack_modal()
        selected_text = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return '';
              const selectedContainers = Array.from(modal.querySelectorAll('div, section, aside, .ant-card, .ant-table-wrapper'))
                .filter(element => {
                  if (element.offsetParent === null) return false;
                  const text = element.innerText || '';
                  return /已选|已选择|已添加|Selected/i.test(text);
                });
              const smallest = selectedContainers
                .sort((left, right) => (left.innerText || '').length - (right.innerText || '').length)[0];
              return (smallest || modal).innerText || '';
            }"""
        )
        if not selected_text.strip():
            selected_text = modal.inner_text(timeout=3000)

        missing = []
        for app_code, app_name in apps:
            if app_code not in selected_text and app_name not in selected_text:
                missing.append(f"{app_code}/{app_name}")
        if missing:
            selected_count_match = re.search(r"已选应用\s*(\d+)", selected_text)
            if selected_count_match and int(selected_count_match.group(1)) >= len(apps):
                return
        assert not missing, f"Packed selected apps not readable: missing={missing}; selected_text={selected_text}"

    def save_product_pack(self) -> None:
        self.click_visible_modal_button(["确定", "保存", "提交", "确 定"], timeout=5000)
        self.page.wait_for_timeout(2500)

    def find_product_row_with_action(self, keyword: str, action_texts: list[str]) -> Locator:
        rows = self.page.locator(".ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            if keyword not in row_text:
                continue
            if any(action_text in row_text for action_text in action_texts):
                expect(row).to_be_visible(timeout=3000)
                return row
        raise AssertionError(f"Could not find product row by keyword/action: {keyword}/{action_texts}")

    def find_product_row_by_keywords_and_action(self, product_name_keywords: list[str], action_text: str) -> Locator:
        last_error: AssertionError | None = None
        for keyword in [item for item in product_name_keywords if item]:
            self.search_product_solution(keyword)
            try:
                row = self.find_product_row_with_action(keyword, [action_text])
                expect(row).to_be_visible(timeout=3000)
                return row
            except AssertionError as exc:
                last_error = exc
                continue
        raise AssertionError(f"Could not find product row by keywords/action: {product_name_keywords}/{action_text}") from last_error

    def click_product_row_action_and_confirm(self, product_name: str, action_text: str) -> None:
        self.search_product_solution(product_name)
        row = self.find_product_row_with_action(product_name, [action_text])
        self._click_row_action_flexible(row, [action_text])
        self.confirm_visible_popconfirm_or_modal()
        self.page.wait_for_timeout(2000)

    def assert_product_row_state_and_action(self, product_name: str, *, status_text: str, action_text: str) -> None:
        self.search_product_solution(product_name)
        row = self.assert_solution_row(product_name)
        row_text = row.inner_text(timeout=3000)
        assert status_text in row_text, f"Product status mismatch: expected={status_text}; row={row_text}"
        assert action_text in row_text, f"Product action mismatch: expected={action_text}; row={row_text}"

    def app_name_from_app_row(self, row: Locator, app_code: str) -> str:
        columns = [item.strip() for item in row.inner_text(timeout=3000).split("\t") if item.strip()]
        for item in columns:
            if item == app_code:
                continue
            if item.startswith("APP") and any(char.isdigit() for char in item):
                continue
            if item.isdigit():
                continue
            if item in {"查看", "编辑", "版本管理", "启用", "禁用", "已启用", "已禁用"}:
                continue
            return item
        return app_code

    def find_product_row_by_keyword(self, product_name_keyword: str) -> Locator:
        rows = self.page.locator(".ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            if product_name_keyword in row_text:
                expect(row).to_be_visible(timeout=3000)
                return row
        raise AssertionError(f"Could not find product row by keyword: {product_name_keyword}")

    def product_code_from_row(self, row: Locator) -> str:
        row_text = row.inner_text(timeout=3000)
        for token in row_text.replace("\t", " ").split():
            if token.startswith(("PROD", "PD", "P")) and any(char.isdigit() for char in token):
                return token
        raise AssertionError(f"Could not read product code from row: {row_text}")

    def product_name_from_row(self, row: Locator) -> str:
        columns = [item.strip() for item in row.inner_text(timeout=3000).split("\t") if item.strip()]
        if len(columns) >= 2:
            return columns[1]
        raise AssertionError(f"Could not read product name from row: {columns}")

    def version_count_from_row(self, row: Locator) -> int:
        columns = [item.strip() for item in row.inner_text(timeout=3000).split("\t") if item.strip()]
        for item in columns:
            if item.isdigit():
                return int(item)
        raise AssertionError(f"Could not read version count from row: {columns}")

    def assert_product_create_operation_logs(self, *, product_code: str) -> None:
        self.assert_operation_log(target_type="产品", target_code=product_code, operation_info="新建产品")
        self.assert_operation_log(target_type="产品", target_code=product_code, operation_info="新建产品版本")

    def assert_product_edit_operation_log(self, *, product_code: str) -> None:
        self.assert_operation_log(target_type="产品", target_code=product_code, operation_info="编辑产品")

    def assert_product_version_edit_operation_log(self, *, product_code: str) -> None:
        self.assert_operation_log(target_type="产品", target_code=product_code, operation_info="编辑产品版本")

    def assert_product_version_status_copy_delete_operation_logs(self, *, product_code: str) -> None:
        for operation_info in ["禁用产品版本", "启用产品版本", "新建产品版本", "删除产品版本"]:
            self.assert_operation_log(target_type="产品", target_code=product_code, operation_info=operation_info)

    def assert_product_publish_online_offline_operation_logs(self, *, product_code: str) -> None:
        self.assert_operation_log(target_type="产品", target_code=product_code, operation_info="发布产品")
        self.assert_operation_log(target_type="产品", target_code=product_code, operation_info="下架产品")
        self.assert_operation_log_count(target_type="产品", target_code=product_code, operation_info="上架产品", min_count=2)

    def _active_feature_compare_modal(self) -> Locator:
        modal = self.page.locator(".ant-modal:visible").filter(has_text="功能对比").last
        if modal.count():
            return modal
        modal = self.page.locator(".ant-modal:visible").filter(has_text="官网展示预览").last
        if modal.count():
            return modal
        raise AssertionError("Feature compare modal is not visible")

    def _fill_latest_feature_compare_row(
        self,
        *,
        version_name: str,
        item_name: str,
        item_value: str,
        is_switch: bool,
        prefer_blank: bool = False,
        required: bool = True,
    ) -> bool:
        filled = self.page.evaluate(
            """({ itemName, itemValue, isSwitch, preferBlank }) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = [...modals].reverse().find(element => /功能对比|官网展示预览/.test(element.innerText || ''));
              if (!modal) return false;

              const setValue = (element, value) => {
                const proto = element.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
                const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
                element.focus();
                descriptor.set.call(element, '');
                element.dispatchEvent(new Event('input', { bubbles: true }));
                descriptor.set.call(element, value);
                element.dispatchEvent(new Event('input', { bubbles: true }));
                element.dispatchEvent(new Event('change', { bubbles: true }));
              };

              const visible = (element) => element && (
                element.offsetParent !== null ||
                element.closest('.ant-checkbox, .ant-input-affix-wrapper')
              );
              const textInputsIn = (root) => Array.from(root.querySelectorAll('input[type="text"]:not([disabled]), textarea:not([disabled])'))
                .filter(input => {
                  const type = (input.getAttribute('type') || '').toLowerCase();
                  return visible(input) && type !== 'checkbox' && type !== 'radio' && type !== 'search';
                });
              const fillCustomGrid = () => {
                const modalRect = modal.getBoundingClientRect();
                const operationColumnLeft = modalRect.right - 140;
                const allTextInputs = textInputsIn(modal);
                const leftNameInputs = allTextInputs
                  .filter(input => {
                    const rect = input.getBoundingClientRect();
                    return rect.left < modalRect.left + 260 && rect.width > 80;
                  })
                  .sort((left, right) => left.getBoundingClientRect().top - right.getBoundingClientRect().top);
                const blankNamePattern = isSwitch ? /^(新开关项|开关)?$/ : /^(新文本项|文本)?$/;
                const rows = leftNameInputs.map(nameInput => {
                  const nameRect = nameInput.getBoundingClientRect();
                  const valueInputs = allTextInputs.filter(input => {
                    if (input === nameInput) return false;
                    const rect = input.getBoundingClientRect();
                    return rect.left > nameRect.right + 8 && rect.left < operationColumnLeft && Math.abs(rect.top - nameRect.top) < 70;
                  });
                  const boxes = Array.from(modal.querySelectorAll('input[type="checkbox"]:not([disabled])'))
                    .filter(input => {
                      if (!visible(input)) return false;
                      const rect = input.getBoundingClientRect();
                      return rect.left > nameRect.right + 8 && rect.left < operationColumnLeft && Math.abs(rect.top - nameRect.top) < 100;
                    });
                  const nameValue = (nameInput.value || '').replace(/\\s+/g, ' ').trim();
                  const valuesEmpty = valueInputs.every(input => !(input.value || '').trim());
                  const boxesEmpty = boxes.every(input => !input.checked);
                  const reusableBlank = blankNamePattern.test(nameValue) && (isSwitch ? boxesEmpty : valuesEmpty);
                  return { nameInput, valueInputs, boxes, reusableBlank };
                }).filter(row => isSwitch ? row.boxes.length : row.valueInputs.length);
                const row = preferBlank ? rows.find(item => item.reusableBlank) : rows[rows.length - 1];
                if (!row) return false;
                setValue(row.nameInput, itemName);
                if (isSwitch) {
                  for (const box of row.boxes) {
                    if (!box.checked) {
                      (box.closest('label, .ant-checkbox-wrapper, .ant-checkbox') || box).click();
                    }
                  }
                } else {
                  for (const input of row.valueInputs) {
                    if (!(input.value || '').trim()) {
                      setValue(input, itemValue);
                    }
                  }
                }
                return true;
              };
              const editableTable = Array.from(modal.querySelectorAll('.ant-table, table'))
                .filter(table => table.offsetParent !== null && table.querySelector('input:not([type="hidden"]), textarea'))
                .sort((left, right) => {
                  const leftPreview = /官网展示预览/.test(left.innerText || '');
                  const rightPreview = /官网展示预览/.test(right.innerText || '');
                  return Number(leftPreview) - Number(rightPreview);
                })[0];
              if (!editableTable) return fillCustomGrid();

              const textOfCell = (cell) => {
                const input = cell.querySelector('input:not([type="hidden"]), textarea');
                return ((input && input.value) || cell.innerText || cell.textContent || '').replace(/\\s+/g, ' ').trim();
              };
              const rowCells = (row) => Array.from(row.querySelectorAll(':scope > .ant-table-cell, :scope > td, :scope > [role="cell"]'))
                .filter(cell => cell.offsetParent !== null);
              const rows = Array.from(editableTable.querySelectorAll('.ant-table-tbody > tr, tbody > tr, .ant-table-row'))
                .filter(row => row.offsetParent !== null && rowCells(row).length >= 2);
              const typedRows = rows.filter(row => {
                const text = (row.innerText || row.textContent || '').replace(/\\s+/g, ' ').trim();
                const hasTextInput = !!row.querySelector('input[type="text"]:not([disabled]), textarea:not([disabled])');
                const hasCheckbox = !!row.querySelector('input[type="checkbox"]:not([disabled])');
                return isSwitch ? hasCheckbox || /开关/.test(text) : hasTextInput || /文本/.test(text);
              });
              let row = typedRows[typedRows.length - 1] || rows[rows.length - 1];
              if (!row) return false;

              const cellsForName = rowCells(row);
              const nameScope = cellsForName[0] || row;
              const nameInputs = textInputsIn(nameScope);
              const nameInput = nameInputs.find(input => !(input.value || '').trim()) || nameInputs[0];
              if (!nameInput) return false;
              setValue(nameInput, itemName);

              const valueCells = cellsForName.slice(1).filter(cell => !/删除|操作/.test(textOfCell(cell)));
              if (isSwitch) {
                const boxes = valueCells.flatMap(cell =>
                  Array.from(cell.querySelectorAll('input[type="checkbox"]:not([disabled])')).filter(input => visible(input))
                );
                if (!boxes.length) return false;
                for (const box of boxes) {
                  if (!box.checked) {
                    (box.closest('label, .ant-checkbox-wrapper, .ant-checkbox') || box).click();
                  }
                }
              } else {
                const valueInputs = valueCells.flatMap(cell => textInputsIn(cell).filter(input => input !== nameInput));
                if (!valueInputs.length) return false;
                for (const input of valueInputs) {
                  if (!(input.value || '').trim()) {
                    setValue(input, itemValue);
                  }
                }
              }
              return true;
            }""",
            {
                "itemName": item_name,
                "itemValue": item_value,
                "isSwitch": is_switch,
                "preferBlank": prefer_blank,
            },
        )
        if required:
            assert filled, f"Could not fill feature compare row: {item_name}"
        if filled:
            self.page.wait_for_timeout(800)
        return bool(filled)

    def assert_feature_compare_preview(
        self,
        *,
        version_name: str,
        switch_item_name: str,
        text_item_name: str,
        text_item_value: str,
    ) -> None:
        preview = self.page.evaluate(
            """() => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = [...modals].reverse().find(element => /功能对比|官网展示预览/.test(element.innerText || ''));
              if (!modal) return null;
              const text = modal.innerText || '';
              const index = text.indexOf('官网展示预览');
              return index >= 0 ? text.slice(index) : text;
            }"""
        )
        assert preview, "Feature compare preview is not readable"
        assert version_name in preview, f"New product version is not shown in official preview: {version_name}; preview={preview}"
        assert switch_item_name in preview, f"Switch feature row is not shown in official preview: {switch_item_name}; preview={preview}"
        assert text_item_name in preview, f"Text feature row is not shown in official preview: {text_item_name}; preview={preview}"
        assert text_item_value in preview, f"Text feature value is not shown in official preview: {text_item_value}; preview={preview}"

    def _active_pack_modal(self) -> Locator:
        modal = self.page.locator(".ant-modal:visible, .ant-drawer:visible").filter(has_text="打包").last
        if modal.count():
            return modal
        modal = self.page.locator(".ant-modal:visible, .ant-drawer:visible").filter(has_text="应用编码").last
        if modal.count():
            return modal
        modal = self.page.locator(".ant-modal:visible, .ant-drawer:visible").filter(has_text="已选").last
        if modal.count():
            return modal
        raise AssertionError("Product pack modal is not visible")

    def _click_row_action_flexible(self, row: Locator, action_texts: list[str]) -> None:
        try:
            self.click_row_action(row, action_texts)
            return
        except AssertionError:
            pass

        clicked = row.evaluate(
            """(row, actionTexts) => {
              const elements = Array.from(row.querySelectorAll('button, a, span'))
                .filter(element => element.offsetParent !== null);
              const direct = elements.find(element => actionTexts.includes((element.innerText || element.textContent || '').trim()));
              if (direct) {
                direct.click();
                return true;
              }
              const more = elements.find(element => /更多|操作|\\.\\.\\./.test(element.innerText || element.textContent || ''));
              if (more) {
                more.click();
                return 'menu';
              }
              return false;
            }""",
            action_texts,
        )
        if clicked == "menu":
            self.page.wait_for_timeout(500)
            for action_text in action_texts:
                menu_item = self.page.locator(".ant-dropdown:visible, .ant-popover:visible").get_by_text(action_text, exact=True).first
                if menu_item.count():
                    menu_item.click()
                    return
        if clicked is True:
            return

        row_text = row.inner_text(timeout=1000)
        raise AssertionError(f"当前行未找到操作入口 {action_texts}，行内容：{row_text}")

    def first_product_version_name_with_action(self, action_text: str, *, status_text: str | None = None) -> str:
        rows = self.page.locator(".ant-modal:visible .ant-table-tbody .ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            if status_text and status_text not in row_text:
                continue
            if not self._row_has_button_text(row, action_text):
                continue
            return self.product_version_name_from_row(row)
        raise AssertionError(f"Could not find product version row with action={action_text}, status={status_text}")

    def click_product_version_row_action(self, action_text: str, *, version_name: str) -> None:
        rows = self.page.locator(".ant-modal:visible .ant-table-tbody .ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            if version_name not in row_text:
                continue
            buttons = row.locator("button")
            for button_index in range(buttons.count()):
                button = buttons.nth(button_index)
                try:
                    text = self._normalize_button_text(button.inner_text(timeout=500))
                except Exception:
                    continue
                if text == action_text:
                    button.click(force=True)
                    return
        raise AssertionError(f"Could not click product version action={action_text}, version={version_name}")

    def assert_product_version_row_state(
        self,
        version_name: str,
        *,
        status_text: str | None = None,
        action_text: str | None = None,
    ) -> None:
        row = self.product_version_row(version_name)
        row_text = row.inner_text(timeout=3000)
        if status_text:
            assert status_text in row_text, f"Product version status mismatch: {status_text}; row={row_text}"
        if action_text:
            assert self._row_has_button_text(row, action_text), (
                f"Product version action not found: {action_text}; row={row_text}"
            )

    def assert_product_version_not_present(self, version_name: str) -> None:
        rows = self.page.locator(".ant-modal:visible .ant-table-tbody .ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            assert version_name not in row_text, f"Copied product version was not deleted: {version_name}"

    def product_version_row(self, version_name: str) -> Locator:
        rows = self.page.locator(".ant-modal:visible .ant-table-tbody .ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                if version_name in row.inner_text(timeout=1000):
                    expect(row).to_be_visible(timeout=3000)
                    return row
            except Exception:
                continue
        raise AssertionError(f"Could not find product version row: {version_name}")

    def product_version_name_from_row(self, row: Locator) -> str:
        cells = row.locator(".ant-table-cell, td")
        if cells.count() >= 2:
            text = cells.nth(1).inner_text(timeout=1000)
            first_line = next((item.strip() for item in text.splitlines() if item.strip()), "")
            if first_line:
                return first_line
        row_text = row.inner_text(timeout=1000).replace("\t", " ")
        for token in row_text.split():
            if "版本" in token:
                return token
        raise AssertionError(f"Could not read product version name from row: {row_text}")

    def confirm_visible_popconfirm_or_modal(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const containers = Array.from(document.querySelectorAll('.ant-popover, .ant-modal'))
                .filter(element => element.offsetParent !== null)
                .reverse();
              for (const container of containers) {
                const buttons = Array.from(container.querySelectorAll('button'))
                  .filter(button => button.offsetParent !== null && !button.disabled);
                const confirm = buttons.find(button => {
                  const text = (button.innerText || button.textContent || '').replace(/\\s+/g, '');
                  return ['确定', '确认', '是', '确认删除', '确认禁用', '确认启用'].includes(text);
                }) || buttons.find(button => {
                  const text = (button.innerText || button.textContent || '').replace(/\\s+/g, '');
                  return /确定|确认/.test(text);
                });
                if (confirm) {
                  confirm.click();
                  return true;
                }
              }
              return false;
            }"""
        )
        if clicked:
            self.page.wait_for_timeout(800)

    def _row_has_button_text(self, row: Locator, expected_text: str) -> bool:
        buttons = row.locator("button")
        for index in range(buttons.count()):
            try:
                text = self._normalize_button_text(buttons.nth(index).inner_text(timeout=500))
            except Exception:
                continue
            if text == expected_text:
                return True
        return False

    def _normalize_button_text(self, text: str) -> str:
        return re.sub(r"\s+", "", text or "")

    def _fill_pack_app_keyword(self, modal: Locator, app_code: str) -> None:
        inputs = modal.locator(
            "input[placeholder*='应用编码'], input[placeholder*='应用'], "
            "input[placeholder*='编码'], input:not([type='hidden']):not([disabled])"
        )
        if inputs.count():
            self.fill_and_verify(inputs.first, app_code, timeout=5000)
            return

        filled = self.page.evaluate(
            """(appCode) => {
              const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return false;
              const inputs = Array.from(modal.querySelectorAll('input:not([type="hidden"]):not([disabled])'))
                .filter(input => input.offsetParent !== null);
              const input = inputs.find(item => /应用编码|应用名称|编码|名称|搜索|keyword|code|name/i.test(
                `${item.placeholder || ''} ${item.getAttribute('aria-label') || ''}`
              )) || inputs[0];
              if (!input) return false;
              input.focus();
              input.value = '';
              input.dispatchEvent(new Event('input', { bubbles: true }));
              input.value = appCode;
              input.dispatchEvent(new Event('input', { bubbles: true }));
              input.dispatchEvent(new Event('change', { bubbles: true }));
              return true;
            }""",
            app_code,
        )
        assert filled, f"Could not fill app code in product pack modal: {app_code}"

    def _click_pack_query(self, modal: Locator) -> None:
        try:
            self.click_button_in_container(modal, ["查询", "搜索"], timeout=2000)
            return
        except AssertionError:
            pass

        clicked_icon = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return false;
              const candidates = Array.from(modal.querySelectorAll(
                '.ant-input-search-button, .ant-input-suffix, [aria-label="search"], .anticon-search'
              )).filter(element => element.offsetParent !== null);
              const target = candidates
                .map(element => element.closest('button') || element)
                .find(element => element.offsetParent !== null);
              if (!target) return false;
              target.click();
              return true;
            }"""
        )
        if clicked_icon:
            return

        pressed = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return false;
              const input = Array.from(modal.querySelectorAll('input:not([type="hidden"]):not([disabled])'))
                .filter(element => element.offsetParent !== null)
                .find(element => document.activeElement === element) ||
                Array.from(modal.querySelectorAll('input:not([type="hidden"]):not([disabled])'))
                  .filter(element => element.offsetParent !== null)[0];
              if (!input) return false;
              input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
              input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true }));
              return true;
            }"""
        )
        assert pressed, "Could not trigger product pack search"

    def _click_pack_add_for_app(self, app_code: str) -> None:
        clicked = self.page.evaluate(
            """(appCode) => {
              const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return false;
              const rows = Array.from(modal.querySelectorAll('.ant-table-row, tr, .ant-list-item'))
                .filter(row => row.offsetParent !== null && (row.innerText || '').includes(appCode));
              const row = rows.find(item => /添加|加入|选择/.test(item.innerText || '')) || rows[0];
              if (!row) return false;
              const buttons = Array.from(row.querySelectorAll('button, a, span'))
                .filter(element => element.offsetParent !== null && !element.disabled);
              const addButton = buttons.find(element => /添加|加入|选择/.test(element.innerText || element.textContent || ''));
              if (!addButton) {
                return 'already-or-unavailable';
              }
              addButton.click();
              return true;
            }""",
            app_code,
        )
        assert clicked, f"Could not find app in product pack modal: {app_code}"

    def _fill_subscription_app_keyword(self, container: Locator, app_code: str) -> None:
        filled = self.page.evaluate(
            """({ appCode }) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = [...modals].reverse().find(element => /保存版本|可选应用版本|已选应用版本/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!modal) return false;
              const inputs = Array.from(modal.querySelectorAll('input:not([type="hidden"]):not([disabled])'))
                .filter(input => input.offsetParent !== null);
              const input = inputs.find(item => /应用|编码|名称|搜索|keyword|code|name/i.test(
                `${item.placeholder || ''} ${item.getAttribute('aria-label') || ''}`
              ));
              if (!input) return true;
              input.focus();
              input.value = '';
              input.dispatchEvent(new Event('input', { bubbles: true }));
              input.value = appCode;
              input.dispatchEvent(new Event('input', { bubbles: true }));
              input.dispatchEvent(new Event('change', { bubbles: true }));
              return true;
            }""",
            {"appCode": app_code},
        )
        assert filled, f"Could not fill app keyword in subscription version modal: {app_code}"

    def _click_subscription_app_query(self, container: Locator) -> None:
        clicked = self.page.evaluate(
            """() => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = [...modals].reverse().find(element => /保存版本|可选应用版本|已选应用版本/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!modal) return false;
              const buttons = Array.from(modal.querySelectorAll('button'))
                .filter(button => button.offsetParent !== null && !button.disabled);
              const button = buttons.find(item => /查询|搜索/.test(item.innerText || ''));
              if (!button) return true;
              button.click();
              return true;
            }"""
        )
        assert clicked, "Could not trigger app query in subscription version modal"

    def _click_subscription_app_version_config_action(self, app_code: str) -> None:
        clicked = self.page.evaluate(
            """(appCode) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = [...modals].reverse().find(element => /保存版本|可选应用版本|已选应用版本/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!modal) return false;
              const rows = Array.from(modal.querySelectorAll('.ant-table-row, tr, .ant-card, .ant-list-item, div'))
                .filter(row => row.offsetParent !== null && (row.innerText || '').includes(appCode));
              for (const row of rows) {
                const controls = Array.from(row.querySelectorAll('button, a, span'))
                  .filter(element => element.offsetParent !== null && !element.disabled);
                const action = controls.find(element => /添加|加入|选择|切换|更换|编辑/.test(
                  (element.innerText || element.textContent || '').replace(/\\s+/g, '')
                ));
                if (action) {
                  const clickable = action.closest('button, a') || action;
                  clickable.click();
                  return true;
                }
              }
              return false;
            }""",
            app_code,
        )
        assert clicked, f"Could not open app version selector for app: {app_code}"

    def _click_first_available_subscription_app_version_config_action(self) -> str:
        app_code = self.page.evaluate(
            """() => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = [...modals].reverse().find(element => /保存版本|可选应用版本|已选应用版本/.test(element.innerText || '')) ||
                modals[modals.length - 1];
              if (!modal) return null;
              const appCodeFromText = (text) => {
                const match = text.match(/APP\\d{8,}/);
                return match ? match[0] : text.split(' ')[0];
              };
              const buttons = Array.from(modal.querySelectorAll('button, a, span'))
                .filter(element => element.offsetParent !== null && !element.disabled);
              const actions = buttons.filter(element => /添加|加入|选择|切换|更换/.test(
                (element.innerText || element.textContent || '').replace(/\\s+/g, '')
              ));
              for (const action of actions) {
                const clickable = action.closest('button, a') || action;
                const buttonText = (clickable.innerText || clickable.textContent || '').replace(/\\s+/g, '');
                if (buttonText !== '添加' && !/加入|选择|切换|更换/.test(buttonText)) continue;
                const appNodes = Array.from(modal.querySelectorAll('div, span, p, li'))
                  .filter(node => node.offsetParent !== null && /APP\\d{8,}/.test(node.innerText || node.textContent || ''));
                const precedingAppNodes = appNodes.filter(node => {
                  const position = node.compareDocumentPosition(clickable);
                  return !!(position & Node.DOCUMENT_POSITION_FOLLOWING);
                });
                const nearestAppNode = precedingAppNodes[precedingAppNodes.length - 1];
                if (nearestAppNode) {
                  const text = (nearestAppNode.innerText || nearestAppNode.textContent || '').replace(/\\s+/g, ' ').trim();
                  clickable.click();
                  return appCodeFromText(text);
                }
                let scope = clickable;
                for (let depth = 0; depth < 5 && scope && scope !== modal; depth += 1) {
                  const text = (scope.innerText || scope.textContent || '').replace(/\\s+/g, ' ').trim();
                  if (/APP\\d{8,}/.test(text)) {
                    clickable.click();
                    return appCodeFromText(text);
                  }
                  scope = scope.parentElement;
                }
              }
              return null;
            }"""
        )
        assert app_code, "Could not find available app version config action in subscription version modal"
        return str(app_code)

    def _select_enabled_experience_app_version(self, preferred_app_version_name: str) -> str:
        selected_version_name = self.page.evaluate(
            """({ preferredVersionName }) => {
              const modals = Array.from(document.querySelectorAll('.ant-modal'))
                .filter(element => element.offsetParent !== null);
              const modal = modals.reverse().find(element => /选择应用版本|资源数量|应用版本|状态/.test(element.innerText || ''));
              if (!modal) return null;

              const rowText = (row) => (row.innerText || row.textContent || '').replace(/\\s+/g, ' ').trim();
              const cellsOf = (row) => Array.from(row.querySelectorAll('.ant-table-cell, td, th, span, div'))
                .filter(cell => cell.offsetParent !== null)
                .map(cell => (cell.innerText || cell.textContent || '').replace(/\\s+/g, ' ').trim())
                .filter(Boolean);
              const positiveExperienceCount = (row) => {
                const text = rowText(row);
                if (/无资源/.test(text)) return false;
                const direct = text.match(/(?:体验资源|资源)(?:数量)?\\s*[:：]?\\s*(\\d+)/);
                if (direct) return Number(direct[1]) > 0;
                const cells = cellsOf(row);
                return cells.some(cell => /^\\d+$/.test(cell) && Number(cell) > 0);
              };
              const enabled = (row) => {
                const text = rowText(row);
                return /启用|已启用|正常/.test(text) && !/禁用|停用|已禁用/.test(text);
              };
              const versionName = (row) => {
                const cells = cellsOf(row);
                return cells.find(cell => {
                  if (/启用|禁用|停用|正常|操作|选择|确定|添加|体验资源|资源数量|引用情况/.test(cell)) return false;
                  if (/^\\d+$/.test(cell)) return false;
                  return cell.length > 0;
                }) || rowText(row).split(' ')[0];
              };
              const rows = Array.from(modal.querySelectorAll('.ant-table-row, tr, .ant-list-item'))
                .filter(row => row.offsetParent !== null && rowText(row) && !/版本名称.*状态.*操作/.test(rowText(row)));
              const matchingRows = rows.filter(row => enabled(row) && positiveExperienceCount(row));
              const preferred = matchingRows.find(row => preferredVersionName && rowText(row).includes(preferredVersionName));
              const target = preferred || matchingRows[0];
              if (!target) return null;
              const input = target.querySelector('input[type="radio"], input[type="checkbox"]');
              const marker = target.querySelector('.ant-radio, .ant-checkbox, [role="radio"], [role="checkbox"]');
              const clickable = input || marker || target;
              clickable.click();
              return versionName(target);
            }""",
            {"preferredVersionName": preferred_app_version_name},
        )
        assert selected_version_name, (
            "Could not select an enabled app version with experience resource count > 0"
        )
        return str(selected_version_name)

    def _active_modal_or_body(self) -> Locator:
        modal = self.page.locator(".ant-modal:visible").last
        if modal.count():
            return modal
        return self.page.locator("body")

    def _active_subscription_version_modal(self) -> Locator:
        modal = self.page.locator(".ant-modal:visible").filter(has_text="保存版本").last
        if modal.count():
            return modal
        modal = self.page.locator(".ant-modal:visible").filter(has_text="应用版本配置").last
        if modal.count():
            return modal
        modal = self.page.locator(".ant-modal:visible").filter(has_text="基准月份").last
        if modal.count():
            return modal
        raise AssertionError("Subscription version modal is not visible")

    def _click_add_subscription_version_button(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const buttons = Array.from(document.querySelectorAll('button'))
                .filter(button => button.offsetParent !== null && !button.disabled);
              const button = buttons.find(item => {
                const text = (item.innerText || item.textContent || '').replace(/\\s+/g, '');
                return ['新增版本', '新建版本', '鏂板鐗堟湰', '鏂板缓鐗堟湰'].includes(text);
              });
              if (!button) return false;
              button.click();
              return true;
            }"""
        )
        assert clicked, "Could not click add subscription version button"

    def _click_save_subscription_version(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null) || document.body;
              const buttons = Array.from(modal.querySelectorAll('button'))
                .filter(button => button.offsetParent !== null && !button.disabled);
              const byText = buttons.find(button => /保存|提交|确定|淇濆瓨|鎻愪氦|纭畾/.test(button.innerText || ''));
              const button = byText || buttons.reverse().find(item => !/取消|关闭|返回|取 消/.test(item.innerText || ''));
              if (!button) return false;
              button.click();
              return true;
            }"""
        )
        assert clicked, "Could not click save subscription version button"

    def _fill_by_label_or_placeholder(self, labels: list[str], value: str) -> None:
        self._fill_in_container(self.page.locator("body"), labels, value)

    def _fill_textarea_by_label_or_placeholder(self, labels: list[str], value: str) -> None:
        self._fill_textarea_in_container(self.page.locator("body"), labels, value)

    def _fill_in_container(self, container: Locator, labels: list[str], value: str) -> None:
        selector_parts = []
        for label in labels:
            selector_parts.extend([f"input[placeholder*='{label}']", f"input[aria-label*='{label}']"])
        input_locator = container.locator(", ".join(selector_parts)).first
        if input_locator.count():
            self.fill_and_verify(input_locator, value, timeout=5000)
            return
        try:
            self._fill_near_text(container, labels, value)
            return
        except AssertionError:
            pass
        input_locator = container.locator("input:not([type='hidden']):not([disabled])").first
        if input_locator.count():
            self.fill_and_verify(input_locator, value, timeout=5000)
            return
        raise AssertionError(f"Could not find input for labels: {labels}")

    def _fill_textarea_in_container(self, container: Locator, labels: list[str], value: str) -> None:
        selector_parts = []
        for label in labels:
            selector_parts.extend([f"textarea[placeholder*='{label}']", f"textarea[aria-label*='{label}']"])
        textarea = container.locator(", ".join(selector_parts)).first
        if textarea.count():
            expect(textarea).to_be_visible(timeout=5000)
            textarea.fill(value)
            return
        textareas = container.locator("textarea")
        if textareas.count():
            textarea = textareas.first
            expect(textarea).to_be_visible(timeout=5000)
            textarea.fill(value)
            return
        raise AssertionError(f"Could not find textarea for labels: {labels}")

    def _fill_near_text(self, container: Locator, labels: list[str], value: str) -> None:
        filled = self.page.evaluate(
            """({ labels, value }) => {
              const root = document.querySelector('.ant-modal:not([style*="display: none"])') || document.body;
              for (const label of labels) {
                const nodes = Array.from(root.querySelectorAll('label, span, div'))
                  .filter(node => node.offsetParent !== null && (node.innerText || node.textContent || '').includes(label));
                for (const node of nodes) {
                  const scope = node.closest('.ant-form-item, .ant-row, .ant-col, div') || node.parentElement || root;
                  const input = scope.querySelector('input:not([type="hidden"]):not([disabled])');
                  if (!input) continue;
                  input.focus();
                  input.value = '';
                  input.dispatchEvent(new Event('input', { bubbles: true }));
                  input.value = value;
                  input.dispatchEvent(new Event('input', { bubbles: true }));
                  input.dispatchEvent(new Event('change', { bubbles: true }));
                  return true;
                }
              }
              return false;
            }""",
            {"labels": labels, "value": value},
        )
        if filled:
            return

        filled_empty_input = self.page.evaluate(
            """(value) => {
              const root = document.querySelector('.ant-modal:not([style*="display: none"])') || document.body;
              const input = Array.from(root.querySelectorAll('input:not([type="hidden"]):not([disabled])'))
                .find(element => element.offsetParent !== null && !element.value);
              if (!input) return false;
              input.focus();
              input.value = value;
              input.dispatchEvent(new Event('input', { bubbles: true }));
              input.dispatchEvent(new Event('change', { bubbles: true }));
              return true;
            }""",
            value,
        )
        assert filled_empty_input, f"Could not fill field near labels: {labels}"

    def _select_by_text(self, texts: list[str], *, required: bool = True, container: Locator | None = None) -> bool:
        root = container or self.page.locator("body")
        for text in texts:
            locator = root.get_by_text(text, exact=False).first
            if not locator.count():
                continue
            try:
                locator.click(timeout=1000)
                self.page.wait_for_timeout(300)
                return True
            except Exception:
                continue
        if required:
            raise AssertionError(f"Could not select text: {texts}")
        return False

    def _check_any_text(self, texts: list[str]) -> None:
        for text in texts:
            try:
                self._select_by_text([text], required=False)
            except Exception:
                continue

    def _ensure_checked_texts(self, texts: list[str]) -> None:
        for text in texts:
            self.page.evaluate(
                """(text) => {
                  const nodes = Array.from(document.querySelectorAll('label, .ant-checkbox-wrapper, .ant-radio-wrapper, span, div'))
                    .filter(node => node.offsetParent !== null && (node.innerText || node.textContent || '').includes(text));
                  const node = nodes[0];
                  if (!node) return false;
                  const wrapper = node.closest('label, .ant-checkbox-wrapper, .ant-radio-wrapper') || node;
                  const input = wrapper.querySelector('input[type="checkbox"], input[type="radio"]');
                  const marker = wrapper.querySelector('.ant-checkbox, .ant-radio') || wrapper;
                  const className = String(marker.className || '') + ' ' + String(wrapper.className || '');
                  const checked = !!(input && input.checked) ||
                    className.includes('ant-checkbox-checked') ||
                    className.includes('ant-radio-wrapper-checked') ||
                    className.includes('ant-radio-checked');
                  if (!checked) {
                    const clickable = input || marker || wrapper;
                    clickable.click();
                  }
                  return true;
                }""",
                text,
            )
            self.page.wait_for_timeout(100)

    def _upload_first_file_input(self, logo_path: Path) -> None:
        file_inputs = self.page.locator("input[type='file']")
        if not file_inputs.count():
            return
        file_inputs.first.set_input_files(str(logo_path))
        self.page.wait_for_timeout(1500)

    def _fill_subscription_terms(self, container: Locator) -> None:
        rows = container.locator(".ant-table-row, tr")
        if not rows.count():
            return
        values = [("1", "100"), ("6", "90"), ("12", "80")]
        for index, (months, discount) in enumerate(values):
            if index >= rows.count():
                break
            inputs = rows.nth(index).locator("input:not([type='hidden'])")
            if inputs.count() >= 1:
                inputs.nth(0).fill(months)
            if inputs.count() >= 2:
                inputs.nth(1).fill(discount)
