from __future__ import annotations

import random
import re
from collections.abc import Iterable

from playwright.sync_api import Locator, expect

from e2e.pages.admin_page import AdminPage


class SaasSubscriptionAdminPage(AdminPage):
    def open_application_list_new(self) -> None:
        self.close_all_visible_modals_or_drawers()
        self._click_left_menu_item(["应用管理"], required=False)
        self.page.wait_for_timeout(800)
        if not self._click_left_menu_item(["应用列表新"], required=False):
            self.click_first_visible(
                [
                    ".ant-menu-item:has-text('应用列表新')",
                    "text=应用列表新",
                ],
                timeout=5000,
            )
        self.page.wait_for_load_state("domcontentloaded")
        self.page.wait_for_timeout(1500)

    def _click_left_menu_item(self, texts: list[str], *, required: bool = True) -> bool:
        for text in texts:
            locators = self.page.locator(
                f"aside .ant-menu-submenu-title:has-text('{text}'), aside .ant-menu-item:has-text('{text}'), "
                f".ant-layout-sider .ant-menu-submenu-title:has-text('{text}'), "
                f".ant-layout-sider .ant-menu-item:has-text('{text}'), "
                f".ant-menu-submenu-title:has-text('{text}'), .ant-menu-item:has-text('{text}')"
            )
            for index in range(locators.count()):
                locator = locators.nth(index)
                try:
                    locator.scroll_into_view_if_needed(timeout=1000)
                except Exception:
                    pass
                try:
                    locator.click(timeout=1500, force=True)
                    return True
                except Exception:
                    try:
                        locator.evaluate("(element) => element.click()")
                        return True
                    except Exception:
                        continue
        if self._click_visible_menu_text(texts):
            return True
        if required:
            raise AssertionError(f"未找到菜单项：{texts}")
        return False

    def _click_visible_menu_text(self, texts: list[str]) -> bool:
        return bool(
            self.page.evaluate(
                """(texts) => {
                  const nodes = Array.from(document.querySelectorAll(
                    'aside .ant-menu span, aside .ant-menu li, .ant-layout-sider .ant-menu span, .ant-layout-sider .ant-menu li, .ant-menu span, .ant-menu li'
                  )).filter(node => node.offsetParent !== null);
                  for (const text of texts) {
                    const node = nodes.find(item => (item.innerText || item.textContent || '').trim().includes(text));
                    if (!node) continue;
                    const clickable = node.closest('.ant-menu-item, .ant-menu-submenu-title, li, [role="menuitem"], .ant-tabs-tab') || node;
                    clickable.click();
                    return true;
                  }
                  return false;
                }""",
                texts,
            )
        )

    def search_app_by_code(self, app_code: str) -> Locator:
        self.open_application_list_new()
        self.fill_first_visible(
            [
                "input[placeholder*='应用编码']",
                "input[placeholder*='应用ID']",
                "input[placeholder*='请输入应用编码']",
                "input[placeholder*='搜索应用编码']",
            ],
            app_code,
            timeout=5000,
        )
        self.click_first_visible(
            [
                "button:has-text('查询')",
                "button:has-text('搜索')",
                "text=查询",
            ],
            timeout=3000,
        )
        self.page.wait_for_timeout(1500)
        row = self.find_table_row(app_code)
        expect(row).to_be_visible(timeout=5000)
        return row

    def collect_visible_app_codes(self, *, limit: int = 20) -> list[str]:
        self.open_application_list_new()
        rows = self.page.locator(".ant-table-row")
        app_codes: list[str] = []
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                row_text = row.inner_text(timeout=1000)
            except Exception:
                continue
            for token in re.split(r"\s+", row_text.replace("\t", " ")):
                if re.fullmatch(r"APP\d{14,}", token) and token not in app_codes:
                    app_codes.append(token)
                    break
            if len(app_codes) >= limit:
                break
        return app_codes

    def collect_application_row_candidates_across_pages(self, *, max_pages: int = 10) -> list[dict[str, int | str]]:
        self.open_application_list_new()
        self._go_to_application_list_page(1)

        candidates: list[dict[str, int | str]] = []
        seen_codes: set[str] = set()
        for _ in range(max_pages):
            page_number = self._current_application_list_page()
            rows = self.page.locator(".ant-table-row")
            for row_index in range(rows.count()):
                row = rows.nth(row_index)
                try:
                    row_text = row.inner_text(timeout=1000)
                except Exception:
                    continue
                app_code = self.app_code_from_row_text(row_text)
                if not app_code or app_code in seen_codes:
                    continue
                seen_codes.add(app_code)
                candidates.append({"page": page_number, "row_index": row_index, "app_code": app_code})
            if not self._click_next_application_list_page():
                break
        return candidates

    def find_app_with_bindable_version_resource(self, candidate_app_codes: Iterable[str]) -> tuple[str, str]:
        for app_code in candidate_app_codes:
            try:
                self.open_app_version_management(app_code)
                resource_name = self.first_bindable_version_resource_name()
                if resource_name:
                    return app_code, resource_name
            except Exception:
                self.close_visible_modal_or_drawer()
                continue
            self.close_visible_modal_or_drawer()
        raise AssertionError("未找到存在可绑定资源菜单的应用，无法新增完整应用版本")

    def find_visible_app_with_bindable_version_resource(self, *, max_rows: int = 20) -> tuple[str, str]:
        candidates = self.collect_application_row_candidates_across_pages(max_pages=max_rows)
        random.shuffle(candidates)
        for candidate in candidates:
            app_code = str(candidate["app_code"])
            try:
                self._open_application_version_management_from_candidate(candidate)
                self.page.wait_for_timeout(1500)
                expect(self.page.get_by_text("应用版本管理", exact=False).first).to_be_visible(timeout=5000)
                self.select_first_visible_app_version_if_any()
                resource_name = self.first_bindable_version_resource_name()
                if resource_name:
                    return app_code, resource_name
            except Exception:
                pass
            self.close_visible_modal_or_drawer()

        raise AssertionError("应用列表所有已收集分页候选中未找到存在可绑定资源菜单的应用")

    def app_code_from_row_text(self, row_text: str) -> str | None:
        for token in re.split(r"\s+", row_text.replace("\t", " ")):
            if re.fullmatch(r"APP\d{14,}", token):
                return token
        return None

    def open_app_version_management(self, app_code: str) -> None:
        row = self.search_app_by_code(app_code)
        self.click_row_action(row, ["版本管理", "版本"])
        self.page.wait_for_timeout(1500)
        expect(self.page.get_by_text("应用版本管理", exact=False).first).to_be_visible(timeout=5000)

    def _open_application_version_management_from_candidate(self, candidate: dict[str, int | str]) -> None:
        self._go_to_application_list_page(int(candidate["page"]))
        app_code = str(candidate["app_code"])
        row = self.find_table_row(app_code)
        expect(row).to_be_visible(timeout=5000)
        self.click_row_action(row, ["版本管理", "版本"])

    def _current_application_list_page(self) -> int:
        active = self.page.locator(".ant-pagination-item-active").first
        if active.count():
            text = active.inner_text(timeout=1000).strip()
            if text.isdigit():
                return int(text)
            title = active.get_attribute("title")
            if title and title.isdigit():
                return int(title)
        return 1

    def _go_to_application_list_page(self, page_number: int) -> None:
        self.open_application_list_new()
        if self._current_application_list_page() == page_number:
            return

        page_item = self.page.locator(
            f".ant-pagination-item-{page_number}, .ant-pagination-item[title='{page_number}']"
        ).first
        if page_item.count():
            try:
                page_item.click()
                self.page.wait_for_timeout(1200)
                return
            except Exception:
                pass

        for _ in range(10):
            current = self._current_application_list_page()
            if current == page_number:
                return
            if current < page_number:
                if not self._click_next_application_list_page():
                    break
            else:
                if not self._click_prev_application_list_page():
                    break
        assert self._current_application_list_page() == page_number, f"Could not navigate to app list page: {page_number}"

    def _click_next_application_list_page(self) -> bool:
        return self._click_application_pager(".ant-pagination-next")

    def _click_prev_application_list_page(self) -> bool:
        return self._click_application_pager(".ant-pagination-prev")

    def _click_application_pager(self, selector: str) -> bool:
        button = self.page.locator(selector).first
        if not button.count():
            return False
        class_name = button.get_attribute("class") or ""
        aria_disabled = button.get_attribute("aria-disabled") or ""
        if "disabled" in class_name or aria_disabled == "true":
            return False
        try:
            button.click()
            self.page.wait_for_timeout(1200)
            return True
        except Exception:
            return False

    def create_app_version(self, version_name: str, description: str) -> None:
        self.click_first_visible(
            [
                ".ant-modal button:has-text('新增版本')",
                ".ant-drawer button:has-text('新增版本')",
                "button:has-text('新增版本')",
            ],
            timeout=5000,
        )
        self.page.wait_for_timeout(800)
        modal = self.page.locator(".ant-modal").filter(has_text="新增应用版本").last
        expect(modal).to_be_visible(timeout=5000)

        self.fill_and_verify(
            modal.locator("input[placeholder*='版本名称'], input[placeholder*='请输入版本名称']").first,
            version_name,
            timeout=5000,
        )

        description_input = modal.locator(
            "textarea[placeholder*='版本说明'], textarea[placeholder*='版本描述'], textarea"
        ).first
        expect(description_input).to_be_visible(timeout=3000)
        description_input.fill(description)

        self.ensure_radio_or_select_value_in_container(modal, ["启用", "正常"])
        self.click_ant_modal_button_by_span_text(["保 存", "保存", "确 定", "确定"], timeout=5000)
        self.page.wait_for_timeout(2500)

    def select_first_visible_app_version_if_any(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return false;
              const leftPanel = Array.from(modal.querySelectorAll('div, section, aside'))
                .filter(element => element.offsetParent !== null)
                .find(element => /应用版本/.test(element.innerText || '') && /新增版本/.test(element.innerText || '')) || modal;
              const candidates = Array.from(leftPanel.querySelectorAll('button, .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 /\\d+(\\.\\d+)*|V\\d+/i.test(text);
                });
              const target = candidates
                .sort((left, right) => (left.innerText || '').length - (right.innerText || '').length)[0];
              if (!target) return false;
              target.click();
              return true;
            }"""
        )
        if clicked:
            self.page.wait_for_timeout(800)

    def edit_app_version(self, old_version_name: str, new_version_name: str, description: str) -> None:
        self.select_app_version(old_version_name)
        self.click_first_visible(
            [
                ".ant-modal button:has-text('编辑版本')",
                ".ant-drawer button:has-text('编辑版本')",
                "button:has-text('编辑版本')",
            ],
            timeout=5000,
        )
        self.page.wait_for_timeout(800)
        modal = self.page.locator(".ant-modal").filter(has_text="编辑应用版本").last
        expect(modal).to_be_visible(timeout=5000)

        self.fill_and_verify(
            modal.locator("input[placeholder*='版本名称'], input[placeholder*='请输入版本名称']").first,
            new_version_name,
            timeout=5000,
        )

        description_input = modal.locator(
            "textarea[placeholder*='版本说明'], textarea[placeholder*='版本描述'], textarea"
        ).first
        expect(description_input).to_be_visible(timeout=3000)
        description_input.fill(description)

        self.ensure_radio_or_select_value_in_container(modal, ["启用", "正常"])
        self.click_ant_modal_button_by_span_text(["保 存", "保存", "确 定", "确定"], timeout=5000)
        self.page.wait_for_timeout(2500)

    def search_version_resource(self, resource_name: str) -> None:
        self._fill_version_resource_keyword(resource_name)
        self._click_version_resource_query()
        self.page.wait_for_timeout(1500)
        expect(self._version_resource_row(resource_name)).to_be_visible(timeout=8000)

    def has_version_resource(self, resource_name: str) -> bool:
        self._fill_version_resource_keyword(resource_name)
        self._click_version_resource_query()
        self.page.wait_for_timeout(1500)
        try:
            return self._version_resource_row(resource_name).is_visible(timeout=3000)
        except Exception:
            return False

    def first_bindable_version_resource_name(self) -> str | None:
        resource_name = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null);
              if (!modal) return null;
              const modalText = modal.innerText || '';
              if (/请选择应用版本/.test(modalText)) return null;
              if (/暂无数据|无数据|No Data/i.test(modalText) && !/资源|菜单/.test(modalText)) return null;

              const checkboxRows = Array.from(modal.querySelectorAll(
                '.ant-tree-treenode, .ant-table-row, tr, label, .ant-checkbox-wrapper'
              )).filter(element => {
                if (element.offsetParent === null) return false;
                if (!element.querySelector('input[type="checkbox"], .ant-checkbox, .ant-tree-checkbox')) return false;
                const text = (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim();
                if (!text) return false;
                if (/全选|已勾选|原已绑定|清空选择/.test(text)) return false;
                return true;
              });
              for (const element of checkboxRows) {
                const text = (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim();
                if (!text) continue;
                if (/新增版本|编辑版本|应用版本管理|保存|查询|搜索|版本名称|版本说明|版本描述|操作|状态/.test(text)) continue;
                if (/暂无数据|无数据|No Data/i.test(text)) continue;
                const firstCell = text
                  .split(' ')
                  .map(item => item.trim())
                  .find(item => item && !/^R\\d{14,}$/.test(item) && !/^\\d+$/.test(item) && !/^菜单$/.test(item));
                if (!firstCell || /^\\d+$/.test(firstCell)) continue;
                return firstCell;
              }

              const resourceTitles = Array.from(modal.querySelectorAll(
                '.ant-tree-title, .ant-tree-node-content-wrapper, .ant-table-cell'
              )).filter(element => element.offsetParent !== null);
              for (const element of resourceTitles) {
                const text = (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim();
                if (!text) continue;
                if (/新增版本|编辑版本|应用版本管理|保存|查询|搜索|全选|已勾选|原已绑定|请选择应用版本|暂无数据/.test(text)) continue;
                if (/^R\\d{14,}$/.test(text) || /^\\d+$/.test(text) || text === '菜单') continue;
                return text.split(' ')[0].trim();
              }
              return null;
            }"""
        )
        if not resource_name:
            return None
        return str(resource_name).strip() or None

    def ensure_version_resource_checked(self, resource_name: str, checked: bool = True) -> None:
        self.search_version_resource(resource_name)
        if self.is_version_resource_checked(resource_name) == checked:
            return

        clicked = self.page.evaluate(
            """([resourceName]) => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null) || document.body;
              const treeNodes = Array.from(modal.querySelectorAll('.ant-tree-treenode'))
                .filter(node => node.offsetParent !== null && node.innerText.includes(resourceName));
              const treeNode = treeNodes[0];
              if (treeNode) {
                const checkbox = treeNode.querySelector('.ant-tree-checkbox[role="checkbox"], .ant-tree-checkbox');
                if (!checkbox) return false;
                checkbox.click();
                return true;
              }
              const rows = Array.from(modal.querySelectorAll('.ant-table-row, tr'))
                .filter(row => row.offsetParent !== null && row.innerText.includes(resourceName));
              const row = rows[0];
              if (!row) return false;
              const rowKey = row.getAttribute('data-row-key');
              const allRows = Array.from(modal.querySelectorAll('.ant-table-row, tr'))
                .filter(item => item.offsetParent !== null);
              const sameKeyRows = rowKey
                ? allRows.filter(item => item.getAttribute('data-row-key') === rowKey)
                : [];
              const textRows = allRows.filter(item => item.innerText.includes(resourceName));
              const rowIndex = textRows.indexOf(row);
              const bodyRows = allRows.filter(item => item.closest('.ant-table-tbody, tbody'));
              const indexRows = rowIndex >= 0 ? bodyRows.filter((_, index) => index === rowIndex) : [];
              const candidates = [row, ...sameKeyRows, ...indexRows];
              const checkbox = candidates
                .map(item => item.querySelector('input[type="checkbox"], .ant-checkbox-input, .ant-checkbox, [role="checkbox"]'))
                .find(Boolean);
              if (!checkbox) return false;
              const clickable = checkbox.closest('label, .ant-checkbox-wrapper, .ant-checkbox') || checkbox;
              clickable.click();
              return true;
            }""",
            [resource_name],
        )
        assert clicked, f"Could not click resource checkbox: {resource_name}"
        self.page.wait_for_timeout(800)
        assert self.is_version_resource_checked(resource_name) == checked, (
            f"Resource checkbox state mismatch: {resource_name}, expected={checked}"
        )

    def save_current_version_resources(self) -> None:
        self.click_first_visible(
            [
                "button:has-text('保存当前版本资源')",
                "button:has-text('淇濆瓨褰撳墠鐗堟湰璧勬簮')",
                "button:has-text('保存')",
                "button:has-text('淇濆瓨')",
            ],
            timeout=5000,
        )
        self.page.wait_for_timeout(2500)

    def select_all_filtered_version_resources(self) -> None:
        clicked = self.page.evaluate(
            """() => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null) || document.body;
              const candidates = Array.from(modal.querySelectorAll('label, .ant-checkbox-wrapper, div, span'))
                .filter(element => element.offsetParent !== null && /全选当前筛选结果|鍏ㄩ€夊綋鍓嶇瓫閫夌粨鏋?/.test(element.innerText || element.textContent || ''));
              const container = candidates
                .map(element => element.closest('label, .ant-checkbox-wrapper') || element)
                .find(Boolean);
              if (!container) return false;
              const input = container.querySelector('input[type="checkbox"], .ant-checkbox-input');
              const checkbox = container.querySelector('.ant-checkbox, [role="checkbox"]') || input;
              if (!checkbox) return false;
              const className = String((checkbox.closest('.ant-checkbox') || checkbox).className || '');
              const checked = !!(input && input.checked) || className.includes('ant-checkbox-checked');
              if (!checked) {
                const clickable = checkbox.closest('label, .ant-checkbox-wrapper, .ant-checkbox') || checkbox;
                clickable.click();
              }
              return true;
            }"""
        )
        assert clicked, "Could not select all filtered version resources"
        self.page.wait_for_timeout(800)

    def assert_version_resource_checked(self, resource_name: str, checked: bool = True) -> None:
        self.search_version_resource(resource_name)
        actual = self.is_version_resource_checked(resource_name)
        assert actual == checked, (
            f"Resource binding state mismatch: {resource_name}, expected={checked}, actual={actual}"
        )

    def app_version_has_bound_resource(self, app_code: str, version_name: str, resource_name: str) -> bool:
        self.open_app_version_management(app_code)
        try:
            self.select_app_version(version_name)
            self.search_version_resource(resource_name)
            return self.is_version_resource_checked(resource_name)
        except Exception:
            return False
        finally:
            self.close_visible_modal_or_drawer()

    def is_version_resource_checked(self, resource_name: str) -> bool:
        self._version_resource_row(resource_name).wait_for(state="visible", timeout=8000)
        state = self.page.evaluate(
            """([resourceName]) => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null) || document.body;
              const treeNodes = Array.from(modal.querySelectorAll('.ant-tree-treenode'))
                .filter(node => node.offsetParent !== null && node.innerText.includes(resourceName));
              const treeNode = treeNodes[0];
              if (treeNode) {
                const checkbox = treeNode.querySelector('.ant-tree-checkbox[role="checkbox"], .ant-tree-checkbox');
                if (!checkbox) return null;
                const className = String(checkbox.className || '');
                const ariaChecked = checkbox.getAttribute('aria-checked');
                return className.includes('ant-tree-checkbox-checked') || ariaChecked === 'true';
              }
              const rows = Array.from(modal.querySelectorAll('.ant-table-row, tr'))
                .filter(row => row.offsetParent !== null && row.innerText.includes(resourceName));
              const row = rows[0];
              if (!row) return null;
              const rowKey = row.getAttribute('data-row-key');
              const allRows = Array.from(modal.querySelectorAll('.ant-table-row, tr'))
                .filter(item => item.offsetParent !== null);
              const sameKeyRows = rowKey
                ? allRows.filter(item => item.getAttribute('data-row-key') === rowKey)
                : [];
              const textRows = allRows.filter(item => item.innerText.includes(resourceName));
              const rowIndex = textRows.indexOf(row);
              const bodyRows = allRows.filter(item => item.closest('.ant-table-tbody, tbody'));
              const indexRows = rowIndex >= 0 ? bodyRows.filter((_, index) => index === rowIndex) : [];
              const candidates = [row, ...sameKeyRows, ...indexRows];
              const input = candidates.map(item => item.querySelector('input[type="checkbox"]')).find(Boolean);
              const checkbox = candidates
                .map(item => item.querySelector('.ant-checkbox, .ant-checkbox-wrapper, [role="checkbox"]'))
                .find(Boolean);
              if (input) return !!input.checked;
              if (checkbox) {
                const className = String(checkbox.className || '');
                const ariaChecked = checkbox.getAttribute('aria-checked');
                return className.includes('ant-checkbox-checked') || ariaChecked === 'true';
              }
              return null;
            }""",
            [resource_name],
        )
        assert state is not None, f"Could not read resource checkbox state: {resource_name}"
        return bool(state)

    def assert_version_visible(self, version_name: str) -> None:
        expect(self.page.get_by_text(version_name, exact=False).first).to_be_visible(timeout=8000)

    def select_app_version(self, version_name: str) -> None:
        version_card = self.page.locator(".ant-modal button", has_text=version_name).first
        if not version_card.count():
            version_card = self.page.get_by_text(version_name, exact=False).first
        expect(version_card).to_be_visible(timeout=8000)
        version_card.click()
        self.page.wait_for_timeout(800)

    def reopen_app_version_management_and_assert_version(self, app_code: str, version_name: str) -> None:
        self.close_visible_modal_or_drawer()
        self.open_app_version_management(app_code)
        self.assert_version_visible(version_name)

    def open_operation_log(self) -> None:
        self.close_all_visible_modals_or_drawers()
        target_code_input = self.page.locator(
            "input[placeholder*='目标编码'], input[placeholder*='请输入目标编码']"
        ).first
        try:
            if target_code_input.is_visible(timeout=500):
                return
        except Exception:
            pass
        self.maybe_click_text(["产品管理"], timeout=3000)
        self.click_first_visible(
            [
                ".ant-menu-item:has-text('操作日志')",
                "li:has-text('操作日志')",
                "text=操作日志",
            ],
            timeout=5000,
        )
        self.page.wait_for_timeout(1500)

    def assert_operation_log(self, target_type: str, target_code: str, operation_info: str) -> None:
        self.open_operation_log()
        self.select_filter_value("目标类型", target_type)
        self.fill_first_visible(
            [
                "input[placeholder*='目标编码']",
                "input[placeholder*='请输入目标编码']",
            ],
            target_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_operation_log_row(target_code, operation_info)
        row_text = row.inner_text(timeout=1000)
        assert target_code in row_text, f"操作日志目标编码不匹配，期望 {target_code}，实际行：{row_text}"
        assert "成功" in row_text, f"操作日志未显示成功状态，实际行：{row_text}"
        self.open_operation_log_detail(row)
        self.assert_operation_log_detail(target_code, operation_info)

    def assert_operation_log_count(
        self,
        target_type: str,
        target_code: str,
        operation_info: str,
        *,
        min_count: int,
    ) -> None:
        self.open_operation_log()
        self.select_filter_value("目标类型", target_type)
        self.fill_first_visible(
            [
                "input[placeholder*='目标编码']",
                "input[placeholder*='请输入目标编码']",
            ],
            target_code,
            timeout=5000,
        )
        self.click_first_visible(
            [
                "button:has-text('查询')",
                "button:has-text('搜索')",
            ],
            timeout=3000,
        )
        self.page.wait_for_timeout(1500)
        count = 0
        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 target_code in row_text and operation_info in row_text and "成功" in row_text:
                count += 1
        assert count >= min_count, (
            f"操作日志数量不足：target_code={target_code}, operation_info={operation_info}, "
            f"expected>={min_count}, actual={count}"
        )

    def ensure_modal_radio_or_select_value(self, labels: Iterable[str]) -> None:
        for label in labels:
            radio = self.page.locator(".ant-modal label", has_text=label).first
            if radio.count():
                try:
                    radio.click(timeout=1000)
                    return
                except Exception:
                    pass
            option = self.page.get_by_text(label, exact=True).first
            if option.count():
                try:
                    option.click(timeout=1000)
                    return
                except Exception:
                    pass

    def ensure_radio_or_select_value_in_container(self, container: Locator, labels: Iterable[str]) -> None:
        for label in labels:
            radio = container.locator("label", has_text=label).first
            if radio.count():
                try:
                    radio.click(timeout=1000)
                    return
                except Exception:
                    pass
            option = container.get_by_text(label, exact=True).first
            if option.count():
                try:
                    option.click(timeout=1000)
                    return
                except Exception:
                    pass

    def select_filter_value(self, label_text: str, option_text: str) -> None:
        selector = self.page.locator(
            f"xpath=//*[contains(normalize-space(), '{label_text}')]/following::*[contains(@class, 'ant-select-selector')][1]"
        ).first
        if selector.count():
            selector.click()
            self.page.get_by_text(option_text, exact=True).first.click()
            self.page.wait_for_timeout(500)

    def close_visible_modal_or_drawer(self) -> None:
        close_buttons = self.page.locator(".ant-modal-close, .ant-drawer-close")
        for index in range(close_buttons.count()):
            button = close_buttons.nth(index)
            try:
                if button.is_visible(timeout=500):
                    button.click()
                    self.page.wait_for_timeout(800)
                    return
            except Exception:
                continue

    def close_all_visible_modals_or_drawers(self) -> None:
        for _ in range(5):
            close_buttons = self.page.locator(".ant-modal-close, .ant-drawer-close")
            clicked = False
            for index in range(close_buttons.count() - 1, -1, -1):
                button = close_buttons.nth(index)
                try:
                    if button.is_visible(timeout=300):
                        button.click()
                        self.page.wait_for_timeout(500)
                        clicked = True
                        break
                except Exception:
                    continue
            if not clicked:
                return

    def click_visible_modal_button(self, button_texts: Iterable[str], timeout: int = 3000) -> None:
        last_error: Exception | None = None
        for button_text in button_texts:
            buttons = self.page.locator(f".ant-modal button:has-text('{button_text}'), button:has-text('{button_text}')")
            for index in range(buttons.count() - 1, -1, -1):
                button = buttons.nth(index)
                try:
                    expect(button).to_be_visible(timeout=timeout)
                    button.click(force=True)
                    return
                except Exception as exc:
                    last_error = exc

        raise AssertionError(f"未找到可点击的弹窗按钮：{list(button_texts)}") from last_error

    def click_ant_modal_button_by_span_text(self, span_texts: Iterable[str], timeout: int = 3000) -> None:
        last_error: Exception | None = None
        for span_text in span_texts:
            buttons = self.page.locator(
                "xpath=//div[contains(@class,'ant-modal') and not(contains(@style,'display: none'))]"
                f"//button[.//span[normalize-space()='{span_text}']]"
            )
            for index in range(buttons.count() - 1, -1, -1):
                button = buttons.nth(index)
                try:
                    expect(button).to_be_visible(timeout=timeout)
                    try:
                        button.click(timeout=timeout)
                    except Exception:
                        button.evaluate("(element) => element.click()")
                    return
                except Exception as exc:
                    last_error = exc

        raise AssertionError(f"未找到可点击的弹窗按钮 span：{list(span_texts)}") from last_error

    def _fill_version_resource_keyword(self, resource_name: str) -> None:
        filled = self.page.evaluate(
            """([resourceName]) => {
              const modal = Array.from(document.querySelectorAll('.ant-modal'))
                .reverse()
                .find(element => element.offsetParent !== null) || document.body;
              const inputs = Array.from(modal.querySelectorAll('input'))
                .filter(input => input.offsetParent !== null && !input.disabled);
              const preferred = inputs.find(input => {
                const text = `${input.placeholder || ''} ${input.getAttribute('aria-label') || ''}`;
                return /资源|名称|编码|keyword|name|code/i.test(text);
              }) || inputs[inputs.length - 1];
              if (!preferred) return false;
              preferred.focus();
              preferred.value = '';
              preferred.dispatchEvent(new Event('input', { bubbles: true }));
              preferred.value = resourceName;
              preferred.dispatchEvent(new Event('input', { bubbles: true }));
              preferred.dispatchEvent(new Event('change', { bubbles: true }));
              return true;
            }""",
            [resource_name],
        )
        assert filled, f"Could not find resource search input: {resource_name}"

    def _click_version_resource_query(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 queryButton = buttons.find(button => /查询|搜索/.test(button.innerText || ''));
              if (!queryButton) return false;
              queryButton.click();
              return true;
            }"""
        )
        assert clicked, "Could not find resource query button"

    def _version_resource_row(self, resource_name: str) -> Locator:
        tree_nodes = self.page.locator(".ant-modal:visible .ant-tree-treenode", has_text=resource_name)
        if tree_nodes.count():
            return tree_nodes.first

        rows = self.page.locator(".ant-modal:visible .ant-table-row, .ant-modal:visible tr")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                if row.is_visible(timeout=300) and resource_name in row.inner_text(timeout=1000):
                    return row
            except Exception:
                continue
        return self.page.locator(".ant-table-row", has_text=resource_name).first

    def click_button_in_container(
        self,
        container: Locator,
        button_texts: Iterable[str],
        timeout: int = 3000,
    ) -> None:
        last_error: Exception | None = None
        for button_text in button_texts:
            buttons = container.locator(f"button:has-text('{button_text}')")
            for index in range(buttons.count() - 1, -1, -1):
                button = buttons.nth(index)
                try:
                    expect(button).to_be_visible(timeout=timeout)
                    button.click(force=True)
                    return
                except Exception as exc:
                    last_error = exc

        raise AssertionError(f"未找到可点击按钮：{list(button_texts)}") from last_error

    def find_latest_operation_log_row(self, target_code: str, operation_info: 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 target_code in row_text and operation_info in row_text and "成功" in row_text:
                expect(row).to_be_visible(timeout=3000)
                return row
        raise AssertionError(f"未找到最新成功操作日志：target_code={target_code}, operation_info={operation_info}")

    def open_operation_log_detail(self, row: Locator) -> None:
        self.click_row_action(row, ["查看"])
        self.page.wait_for_timeout(1000)

    def assert_operation_log_detail(self, target_code: str, operation_info: str) -> None:
        detail = self.page.locator(".ant-modal, .ant-drawer, body").filter(has_text=operation_info).last
        expect(detail).to_be_visible(timeout=5000)
        detail_text = detail.inner_text(timeout=3000)
        assert target_code in detail_text, f"操作日志详情目标编码不匹配，期望 {target_code}，实际：{detail_text}"
        assert operation_info in detail_text, f"操作日志详情操作信息不匹配，期望 {operation_info}，实际：{detail_text}"
        assert "成功" in detail_text, f"操作日志详情未显示成功状态，实际：{detail_text}"
        self.close_visible_modal_or_drawer()

    def find_table_row(self, text: str) -> Locator:
        rows = self.page.locator(".ant-table-row")
        for index in range(rows.count()):
            row = rows.nth(index)
            try:
                if text in row.inner_text(timeout=1000):
                    return row
            except Exception:
                continue
        return self.page.locator(".ant-table-row", has_text=text).first

    def click_row_action(self, row: Locator, action_texts: Iterable[str]) -> None:
        for action_text in action_texts:
            action = row.get_by_text(action_text, exact=True).first
            if action.count():
                action.click()
                return

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