from __future__ import annotations
from playwright.sync_api import Frame, Page, expect

from e2e.pages.tenant_auth_page import TenantAuthPage
from e2e.utils.bug_reporter import BugReporter


WANZHANGGUI_CONSOLE_URL = "https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20251204180120231330"
PROD_WANZHANGGUI_CONSOLE_URL = (
    "https://merchant-lianlianyun.shunfju.com/productConsole?productCode=P20251222184412861622"
)


class WanzhangguiPushPage(TenantAuthPage):
    def __init__(self, page: Page, tenant_home_url: str, bug_reporter: BugReporter | None = None):
        super().__init__(page, tenant_home_url, bug_reporter)

    def open_workshop_authorization(self) -> Frame:
        self.goto_wanzhanggui_console()
        self.wait_for_enterprise_console_shell()
        if self.is_production():
            self.open_prod_push_config_menu("/merchantSupplyAuth")
        else:
            self.click_sidebar_text("履约工作室/平台", right_edge=True)
            self.click_sidebar_text("工作室管理")
            self.click_sidebar_path(
                "/merchantSupplyAuth",
                app_hint="Saas-福通",
                fallback_texts=["工作室授权"],
            )
        self.page.wait_for_timeout(2500)
        return self.futong_frame_by_path("/merchantSupplyAuth")

    def open_dispatch_strategy(self) -> Frame:
        self.goto_wanzhanggui_console()
        self.wait_for_enterprise_console_shell()
        if self.is_production():
            self.open_prod_push_config_menu("/dispatchStrategy")
        else:
            self.click_sidebar_text("履约工作室/平台", right_edge=True)
            self.click_sidebar_text("工作室管理")
            self.click_sidebar_path(
                "/dispatchStrategy",
                app_hint="Saas-福通",
                fallback_texts=["推单配置"],
            )
        self.page.wait_for_timeout(2500)
        return self.futong_frame_by_path("/dispatchStrategy")

    def goto_wanzhanggui_console(self) -> None:
        if "shunfju.com" in self.tenant_home_url:
            self.goto(PROD_WANZHANGGUI_CONSOLE_URL)
            self.page.wait_for_load_state("domcontentloaded")
            self.page.wait_for_timeout(3000)
            expect(self.page.locator("body")).to_contain_text("发单平台配置", timeout=15000)
            return
        self.goto(WANZHANGGUI_CONSOLE_URL)

    def is_production(self) -> bool:
        return "shunfju.com" in self.tenant_home_url

    def open_prod_push_config_menu(self, path: str) -> None:
        self.click_sidebar_text("发单平台配置", right_edge=True)
        self.click_sidebar_path(path, app_hint="Saas-福通")

    def futong_frame_by_path(self, path: str) -> Frame:
        matching_frames = [frame for frame in self.page.frames if path in frame.url]
        for frame in matching_frames:
            if "futong-pc" in frame.url:
                return frame
        for frame in matching_frames:
            try:
                if any(text in frame.locator("body").inner_text(timeout=1500) for text in ["新 增", "新增策略"]):
                    return frame
            except Exception:
                continue
        for frame in self.page.frames:
            if path in frame.url:
                return frame
        urls = [frame.url for frame in self.page.frames]
        raise AssertionError(f"未找到丸掌柜业务 iframe {path}，当前 frames：{urls}")

    def add_wanyouji_workshop_authorization(self, workshop_enterprise_name: str, auth_key: str = "") -> None:
        frame = self.open_workshop_authorization()
        if self.workshop_authorized(frame, workshop_enterprise_name):
            return

        self.click_frame_button_by_js(frame, "新 增")
        self.page.wait_for_timeout(1200)
        self.select_modal_option(frame, 0, "丸友集")
        self.page.wait_for_timeout(1200)
        selected_workshop = self.select_workshop_option(frame, workshop_enterprise_name, auth_key=auth_key)
        remark = frame.locator(".ant-modal textarea[placeholder='请输入备注']").first
        if remark.count():
            remark.fill(f"E2E授权{workshop_enterprise_name}")
        self.click_frame_button_by_js(frame, "确 认")
        self.page.wait_for_timeout(2500)
        self.click_optional_frame_buttons(frame, ["查 询", "查询"])
        self.page.wait_for_timeout(1500)

        assert self.workshop_authorized(frame, workshop_enterprise_name) or self.workshop_authorized(frame, selected_workshop), (
            f"丸掌柜工作室授权列表未出现工作室 {workshop_enterprise_name}，"
            f"当前页面文本：{frame.locator('body').inner_text(timeout=5000)[:1200]}"
        )

    def select_workshop_option(self, frame: Frame, workshop_enterprise_name: str, auth_key: str = "") -> str:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        expect(selectors.nth(1)).to_be_visible(timeout=5000)
        selectors.nth(1).click()
        self.page.wait_for_timeout(1000)
        option_locator = frame.locator(".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content")
        option = option_locator.filter(has_text=workshop_enterprise_name).first
        if option.count() == 0 and auth_key:
            option = option_locator.filter(has_text=auth_key).first
        if option.count() == 0:
            option = option_locator.first
        expect(option).to_be_visible(timeout=5000)
        selected_text = option.inner_text(timeout=5000).strip()
        option.click()
        self.page.wait_for_timeout(800)
        return selected_text

    def workshop_authorized(self, frame: Frame, workshop_enterprise_name: str) -> bool:
        try:
            text = frame.locator("body").inner_text(timeout=5000)
        except Exception:
            return False
        if workshop_enterprise_name in text and ("启用" in text or "正常" in text or "状态" in text):
            return True
        return self.is_production() and "自动化丸友集工作室" in text and ("启用" in text or "正常" in text)

    def create_dispatch_strategy(
        self,
        strategy_name: str,
        strategy_type: str,
        workshop_enterprise_name: str,
        workshop_enterprise_names: list[str] | None = None,
    ) -> list[str]:
        selected_workshops = workshop_enterprise_names or [workshop_enterprise_name]
        frame = self.open_dispatch_strategy()
        if self.dispatch_strategy_exists(frame, strategy_name):
            return selected_workshops

        self.click_frame_button_by_js(frame, "新增策略")
        self.page.wait_for_timeout(1200)
        name_input = frame.locator(".ant-modal input[placeholder='例如: 默认推单策略']").first
        expect(name_input).to_be_visible(timeout=5000)
        name_input.fill(strategy_name)
        expect(name_input).to_have_value(strategy_name, timeout=3000)

        self.click_modal_radio(frame, strategy_type)
        if strategy_type == "多平台分发":
            selected_workshops = self.add_multi_platform_wanyouji_dispatch_config(frame, selected_workshops)
        else:
            self.select_modal_option(frame, 0, "丸友集")
            self.page.wait_for_timeout(1200)
            selected_workshops = self.select_authorized_workshops_for_strategy(frame, selected_workshops, strategy_type)
            self.select_required_service_type_for_strategy(frame, 2)

        self.click_frame_button_by_js(frame, "确 认")
        self.page.wait_for_timeout(3000)
        self.click_optional_frame_buttons(frame, ["查 询", "查询"])
        self.page.wait_for_timeout(1500)

        assert self.dispatch_strategy_exists(frame, strategy_name), (
            f"丸掌柜推单配置列表未出现策略 {strategy_name}，"
            f"当前页面文本：{frame.locator('body').inner_text(timeout=5000)[:1200]}"
        )
        if strategy_type in {"多工作室同时分发", "多平台分发"}:
            text = frame.locator("body").inner_text(timeout=5000)
            matched_workshops = [name for name in selected_workshops if name in text]
            assert len(matched_workshops) >= 2, (
                f"{strategy_type}策略保存后未展示 2 家以上授权商户号，"
                f"期望：{selected_workshops}，实际命中：{matched_workshops}，当前页面文本：{text[:1500]}"
            )
        return selected_workshops

    def dispatch_strategy_exists(self, frame: Frame, strategy_name: str) -> bool:
        try:
            text = frame.locator("body").inner_text(timeout=5000)
        except Exception:
            return False
        return strategy_name in text

    def click_modal_radio(self, frame: Frame, label_text: str) -> None:
        clicked = frame.evaluate(
            """labelText => {
                const labels = Array.from(document.querySelectorAll('.ant-modal label, .ant-modal .ant-radio-wrapper'));
                const target = labels.find(el => (el.innerText || el.textContent || '').trim().includes(labelText));
                if (!target) return false;
                target.click();
                return true;
            }""",
            label_text,
        )
        if not clicked:
            body_text = frame.locator("body").inner_text(timeout=5000)
            raise AssertionError(f"未选择策略类型 {label_text}，当前弹窗文本：{body_text[:1000]}")
        self.page.wait_for_timeout(800)

    def select_authorized_workshop_for_strategy(
        self,
        frame: Frame,
        workshop_enterprise_name: str,
        strategy_type: str,
    ) -> None:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        if selectors.count() < 2:
            body_text = frame.locator("body").inner_text(timeout=5000)
            raise AssertionError(f"选择丸友集后未出现授权商户号下拉框，当前弹窗文本：{body_text[:1200]}")

        # 单平台指定和多工作室同时分发都需要先选一个已授权丸友集工作室。
        selectors.nth(1).click()
        self.page.wait_for_timeout(1000)
        option = frame.locator(
            ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content"
        ).filter(has_text=workshop_enterprise_name).first
        if option.count() == 0 and strategy_type == "多平台分发":
            option = frame.locator(
                ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content"
            ).filter(has_text="丸友集").first
        expect(option).to_be_visible(timeout=5000)
        option.click()
        self.page.wait_for_timeout(800)

    def select_authorized_workshops_for_strategy(
        self,
        frame: Frame,
        workshop_enterprise_names: list[str],
        strategy_type: str,
    ) -> list[str]:
        if strategy_type == "多工作室同时分发":
            selected_workshops = self.select_top_available_workshops_once(frame, 1, max_count=2)
            selected = self.selected_dispatch_values(frame, 1)
            missing = [name for name in selected_workshops if not any(name in item for item in selected)]
            assert not missing, (
                f"多工作室同时分发未按指定工作室完成多选，缺失：{missing}，"
                f"期望：{selected_workshops}，实际：{selected}"
            )
            return selected_workshops
        self.select_authorized_workshop_for_strategy(frame, workshop_enterprise_names[0], strategy_type)
        return [workshop_enterprise_names[0]]

    def select_required_service_type_for_strategy(self, frame: Frame, service_type_index: int) -> None:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        if selectors.count() <= service_type_index:
            body_text = frame.locator("body").inner_text(timeout=5000)
            raise AssertionError(f"新增策略弹窗未出现服务类型下拉框，当前弹窗文本：{body_text[:1500]}")
        current_text = selectors.nth(service_type_index).inner_text(timeout=5000).strip()
        if current_text and "请选择" not in current_text and current_text != "No data":
            return

        self.wait_dispatch_combobox_enabled(frame, service_type_index)
        for _ in range(2):
            self.select_dispatch_config_combobox_first_available_option(frame, service_type_index)
            selected_text = selectors.nth(service_type_index).inner_text(timeout=5000).strip()
            if selected_text and "请选择" not in selected_text and selected_text != "No data":
                return
            self.select_dispatch_config_combobox_by_keyboard(frame, service_type_index)
            selected_text = selectors.nth(service_type_index).inner_text(timeout=5000).strip()
            if selected_text and "请选择" not in selected_text and selected_text != "No data":
                return
        selected_text = selectors.nth(service_type_index).inner_text(timeout=5000).strip()
        assert selected_text and "请选择" not in selected_text and selected_text != "No data", (
            f"服务类型未选择成功，当前选择结果：{selected_text}，"
            f"当前弹窗文本：{frame.locator('body').inner_text(timeout=5000)[:1500]}"
        )

    def add_multi_platform_wanyouji_dispatch_config(
        self,
        frame: Frame,
        workshop_enterprise_names: list[str],
    ) -> list[str]:
        modal = frame.locator(".ant-modal").last
        before_count = modal.get_by_role("combobox").count()
        self.click_frame_button_by_js(frame, "添加供应链配置")
        self.page.wait_for_timeout(1200)
        for _ in range(10):
            if modal.get_by_role("combobox").count() >= before_count + 3:
                break
            self.page.wait_for_timeout(300)
        base_index = modal.get_by_role("combobox").count() - 3
        if base_index < 0:
            body_text = frame.locator("body").inner_text(timeout=5000)
            raise AssertionError(f"添加供应链配置后未出现配置行下拉框，当前页面文本：{body_text[:1200]}")
        self.select_dispatch_config_combobox_option(frame, base_index, "丸友集")
        self.assert_dispatch_combobox_contains(frame, base_index, "丸友集")
        self.wait_dispatch_combobox_enabled(frame, base_index + 1)
        selected_workshops = self.select_top_available_workshops_once(frame, base_index + 1, max_count=2)
        selected = self.selected_dispatch_values(frame, base_index + 1)
        missing = [name for name in selected_workshops if not any(name in item for item in selected)]
        assert not missing, (
            f"多平台分发未按指定工作室完成多选，缺失：{missing}，"
            f"期望：{selected_workshops}，实际：{selected}"
        )
        self.wait_dispatch_combobox_enabled(frame, base_index + 2)
        self.select_required_service_type_for_strategy(frame, base_index + 2)
        return selected_workshops

    def select_top_available_workshops_once(self, frame: Frame, select_index: int, max_count: int) -> list[str]:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        expect(selectors.nth(select_index)).to_be_visible(timeout=5000)
        selectors.nth(select_index).click(force=True)
        self.page.wait_for_timeout(800)

        initial_options = self.visible_unselected_dropdown_option_texts(frame)
        if not initial_options:
            raise AssertionError("授权商户号下拉没有可选工作室")

        expected_count = min(max_count, len(initial_options))
        clicked = self.click_dropdown_options_by_visible_order(frame, expected_count)

        selected = self.selected_dispatch_values(frame, select_index)
        if len(selected) != expected_count:
            still_visible = self.visible_dropdown_options(frame)
            raise AssertionError(
                f"授权商户号下拉顺位点击后真实选中数量不正确，"
                f"初始可选：{initial_options}，期望选中数量：{expected_count}，"
                f"已点击：{clicked}，真实已选：{selected}，当前下拉可见：{still_visible}"
            )
        selectors.nth(select_index).press("Escape")
        self.page.wait_for_timeout(300)
        return selected

    def click_dropdown_options_by_visible_order(self, frame: Frame, expected_count: int) -> list[str]:
        option_locator = frame.locator(
            ".ant-select-dropdown:not(.ant-select-dropdown-hidden) "
            ".ant-select-item-option:not(.ant-select-item-option-disabled)"
        )
        available_count = option_locator.count()
        if available_count < expected_count:
            options = self.visible_unselected_dropdown_option_texts(frame)
            raise AssertionError(
                f"授权商户号下拉可选项不足，期望顺位点击 {expected_count} 个，当前可选：{options}"
            )

        clicked: list[str] = []
        for index in range(expected_count):
            option = option_locator.nth(index)
            text = option.locator(".ant-select-item-option-content").inner_text(timeout=5000).strip()
            option.click(force=True, timeout=5000)
            clicked.append(text)
            self.page.wait_for_timeout(700)
        return clicked

    def close_dropdown_by_clicking_modal_blank(self, frame: Frame) -> None:
        box = frame.evaluate(
            """() => {
                const frameElement = window.frameElement;
                const frameRect = frameElement ? frameElement.getBoundingClientRect() : { x: 0, y: 0 };
                const modal = Array.from(document.querySelectorAll('.ant-modal'))
                  .filter(el => {
                    const r = el.getBoundingClientRect();
                    return r.width > 0 && r.height > 0;
                  }).pop();
                if (!modal) return null;
                const r = modal.getBoundingClientRect();
                return { x: frameRect.x + r.x + Math.min(80, r.width / 2), y: frameRect.y + r.y + 24 };
            }"""
        )
        if box:
            self.page.mouse.click(box["x"], box["y"])

    def dropdown_option_box_by_index(self, frame: Frame, index: int) -> dict:
        box = frame.evaluate(
            """index => {
                const frameElement = window.frameElement;
                const frameRect = frameElement ? frameElement.getBoundingClientRect() : { x: 0, y: 0 };
                const options = Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) ' +
                    '.ant-select-item-option:not(.ant-select-item-option-disabled)'
                )).map(option => {
                    const text = (option.innerText || option.textContent || '').trim();
                    const r = option.getBoundingClientRect();
                    return { text, x: frameRect.x + r.x, y: frameRect.y + r.y, width: r.width, height: r.height };
                }).filter(item => item.width > 0 && item.height > 0)
                  .filter(item => item.text && item.text !== 'No data' && !item.text.includes('请选择'));
                const target = options[index];
                if (!target) return { options: options.map(item => item.text) };
                return target;
            }""",
            index,
        )
        if "text" not in box:
            raise AssertionError(f"授权商户号下拉第 {index + 1} 个可选项不存在，当前可选：{box.get('options')}")
        return box

    def visible_unselected_dropdown_option_texts(self, frame: Frame) -> list[str]:
        return list(
            frame.evaluate(
                """() => Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) ' +
                    '.ant-select-item-option:not(.ant-select-item-option-disabled)'
                )).map(option => {
                    const text = (option.innerText || option.textContent || '').trim();
                    const r = option.getBoundingClientRect();
                    return { text, r };
                }).filter(item => item.r.width > 0 && item.r.height > 0)
                  .map(item => item.text)
                  .filter(text => text && text !== 'No data' && !text.includes('请选择'))"""
            )
        )

    def available_dispatch_options(self, frame: Frame, select_index: int) -> list[str]:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        expect(selectors.nth(select_index)).to_be_visible(timeout=5000)
        selectors.nth(select_index).click(force=True)
        self.page.wait_for_timeout(800)
        options = list(
            frame.evaluate(
                """() => Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) ' +
                    '.ant-select-item-option:not(.ant-select-item-option-disabled) .ant-select-item-option-content'
                )).map(el => (el.innerText || el.textContent || '').trim())
                  .filter(Boolean)
                  .filter(text => text !== 'No data' && !text.includes('请选择'))"""
            )
        )
        selectors.nth(select_index).press("Escape")
        self.page.wait_for_timeout(300)
        return list(dict.fromkeys(options))

    def visible_modal_select_count(self, frame: Frame) -> int:
        return int(
            frame.evaluate(
                """() => Array.from(document.querySelectorAll('.ant-modal .ant-select-selector'))
                    .filter(el => {
                        const r = el.getBoundingClientRect();
                        return r.width > 0 && r.height > 0;
                    }).length"""
            )
        )

    def latest_dispatch_config_select_indexes(self, frame: Frame) -> list[int]:
        return list(
            frame.evaluate(
                """() => {
                    const items = Array.from(document.querySelectorAll('.ant-modal .ant-select-selector'))
                      .map((el, index) => {
                        const r = el.getBoundingClientRect();
                        return { index, x: r.x, y: r.y, width: r.width, height: r.height };
                      })
                      .filter(item => item.width > 0 && item.height > 0)
                      .sort((a, b) => b.y - a.y || a.x - b.x);
                    if (!items.length) return [];
                    const rowY = items[0].y;
                    return items
                      .filter(item => Math.abs(item.y - rowY) < 8)
                      .sort((a, b) => a.x - b.x)
                      .map(item => item.index);
                }"""
            )
        )

    def select_dispatch_config_combobox_option(self, frame: Frame, combobox_index: int, option_text: str) -> None:
        self.select_dispatch_config_option(frame, combobox_index, option_text)

    def select_dispatch_config_combobox_options(
        self,
        frame: Frame,
        combobox_index: int,
        option_texts: list[str],
    ) -> None:
        for option_text in option_texts:
            self.select_dispatch_config_multi_option(frame, combobox_index, option_text)

    def select_all_dispatch_config_options(self, frame: Frame, select_index: int, min_count: int) -> None:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        expect(selectors.nth(select_index)).to_be_visible(timeout=5000)
        selectors.nth(select_index).click(force=True)
        self.page.wait_for_timeout(1000)
        clicked = self.click_all_visible_dropdown_options(frame)
        selected = self.selected_dispatch_values(frame, select_index)
        selected_count = len(selected)
        if selected_count < min_count:
            selected_text = selectors.nth(select_index).inner_text(timeout=5000)
            raise AssertionError(
                f"授权商户号下拉可选商户不足，期望至少选择 {min_count} 家，"
                f"实际选中 {selected_count} 家；已点击：{clicked}；最终已选：{selected}；"
                f"当前选择结果：{selected_text}"
            )

    def selected_dispatch_values(self, frame: Frame, select_index: int) -> list[str]:
        return list(
            frame.evaluate(
                """selectIndex => {
                    const selector = document.querySelectorAll('.ant-modal .ant-select-selector')[selectIndex];
                    if (!selector) return [];
                    const labels = Array.from(selector.querySelectorAll('.ant-select-selection-item-content'))
                      .map(el => (el.innerText || el.textContent || '').trim())
                      .filter(Boolean)
                      .filter(text => text !== '+ 0 ...' && !/^\\+\\s*\\d+/.test(text));
                    return Array.from(new Set(labels));
                }""",
                select_index,
            )
        )

    def click_all_visible_dropdown_options(self, frame: Frame) -> list[str]:
        clicked: list[str] = []
        for _ in range(20):
            options = frame.locator(
                ".ant-select-dropdown:not(.ant-select-dropdown-hidden) "
                ".ant-select-item-option:not(.ant-select-item-option-selected):not(.ant-select-item-option-disabled)"
            )
            if options.count() == 0:
                break
            option = options.first
            text = option.inner_text(timeout=5000).strip()
            if not text or text == "No data":
                break
            option.click(force=True)
            clicked.append(text)
            self.page.wait_for_timeout(800)
        return clicked

    def select_dispatch_config_multi_option(self, frame: Frame, select_index: int, option_text: str) -> None:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        expect(selectors.nth(select_index)).to_be_visible(timeout=5000)
        if option_text in selectors.nth(select_index).inner_text(timeout=5000):
            return
        for _ in range(2):
            selector = selectors.nth(select_index)
            selector.click(force=True)
            self.page.wait_for_timeout(800)
            available_options = self.visible_dropdown_options(frame)
            option = frame.locator(
                ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
            ).filter(has_text=option_text).first
            try:
                expect(option).to_be_visible(timeout=2000)
            except Exception:
                search_input = selector.locator("input[role='combobox']").first
                if search_input.count():
                    search_input.fill(option_text)
                    self.page.wait_for_timeout(1000)
                available_options = self.visible_dropdown_options(frame)
                option = frame.locator(
                    ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
                ).filter(has_text=option_text).first
                try:
                    expect(option).to_be_visible(timeout=5000)
                except Exception as exc:
                    raise AssertionError(
                        f"授权商户号下拉中未找到「{option_text}」，"
                        f"当前可见选项：{available_options}"
                    ) from exc
            option.click(force=True)
            self.page.wait_for_timeout(1000)
            selected_text = selectors.nth(select_index).inner_text(timeout=5000)
            if option_text in selected_text:
                return
        selected_text = selectors.nth(select_index).inner_text(timeout=5000)
        raise AssertionError(
            f"第 {select_index + 1} 个下拉未能多选「{option_text}」，当前选择结果：{selected_text}"
        )

    def visible_dropdown_options(self, frame: Frame) -> list[str]:
        return list(
            frame.evaluate(
                """() => Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option'
                )).map(el => (el.innerText || el.textContent || '').trim()).filter(Boolean)"""
            )
        )

    def click_visible_dropdown_option(self, frame: Frame, option_text: str) -> None:
        clicked = frame.evaluate(
            """optionText => {
                const options = Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content'
                )).map(el => ({
                    el,
                    text: (el.innerText || el.textContent || '').trim(),
                    rect: el.getBoundingClientRect(),
                })).filter(item => item.rect.width > 0 && item.rect.height > 0);
                const exact = options.find(item => item.text === optionText);
                const includes = options.find(item => item.text.includes(optionText));
                const target = exact || includes;
                if (!target) return false;
                const optionNode = target.el.closest('.ant-select-item-option') || target.el;
                optionNode.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
                optionNode.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
                optionNode.click();
                return true;
            }""",
            option_text,
        )
        if not clicked:
            options_text = frame.evaluate(
                """() => Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content'
                )).map(el => (el.innerText || el.textContent || '').trim()).filter(Boolean)"""
            )
            raise AssertionError(f"当前打开下拉中未找到选项「{option_text}」，可选项：{options_text}")

    def assert_dispatch_combobox_contains(self, frame: Frame, combobox_index: int, expected_text: str) -> None:
        text = frame.locator(".ant-modal .ant-select-selector").nth(combobox_index).inner_text(timeout=5000)
        assert expected_text in text, (
            f"第 {combobox_index + 1} 个下拉选择结果不正确，期望包含「{expected_text}」，实际为「{text}」"
        )

    def wait_dispatch_combobox_enabled(self, frame: Frame, combobox_index: int) -> None:
        combobox = frame.locator(".ant-modal").last.get_by_role("combobox").nth(combobox_index)
        try:
            expect(combobox).to_be_enabled(timeout=8000)
        except Exception as exc:
            modal_text = frame.locator(".ant-modal").last.inner_text(timeout=5000)
            raise AssertionError(
                f"选择供应链后，第 {combobox_index + 1} 个下拉框仍不可用，"
                f"当前弹窗文本：{modal_text[:1500]}"
            ) from exc

    def select_dispatch_config_combobox_first_available_option(self, frame: Frame, combobox_index: int) -> None:
        combobox = frame.locator(".ant-modal").last.get_by_role("combobox").nth(combobox_index)
        expect(combobox).to_be_enabled(timeout=5000)
        combobox.click(force=True)
        self.page.wait_for_timeout(800)
        clicked = frame.evaluate(
            """() => {
                const options = Array.from(document.querySelectorAll(
                    '.ant-select-dropdown:not(.ant-select-dropdown-hidden) ' +
                    '.ant-select-item-option:not(.ant-select-item-option-disabled) .ant-select-item-option-content'
                )).map(el => {
                    const option = el.closest('.ant-select-item-option') || el;
                    const text = (el.innerText || el.textContent || '').trim();
                    const r = el.getBoundingClientRect();
                    return { option, text, r };
                }).filter(item => item.r.width > 0 && item.r.height > 0)
                  .filter(item => item.text && item.text !== 'No data' && !item.text.includes('请选择'))
                const target = options.find(item => !/^\\d+$/.test(item.text)) || options[0];
                if (!target) return false;
                target.option.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
                target.option.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
                target.option.click();
                return true;
            }"""
        )
        if not clicked:
            options_text = self.visible_dropdown_options(frame)
            raise AssertionError(f"服务类型下拉没有可选项，当前可见选项：{options_text}")
        self.page.wait_for_timeout(800)

    def select_dispatch_config_combobox_by_keyboard(self, frame: Frame, combobox_index: int) -> None:
        combobox = frame.locator(".ant-modal").last.get_by_role("combobox").nth(combobox_index)
        expect(combobox).to_be_enabled(timeout=5000)
        combobox.click(force=True)
        self.page.wait_for_timeout(500)
        combobox.press("ArrowDown")
        self.page.wait_for_timeout(200)
        combobox.press("Enter")
        self.page.wait_for_timeout(800)

    def select_dispatch_config_option(self, frame: Frame, select_index: int, option_text: str) -> None:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        expect(selectors.nth(select_index)).to_be_visible(timeout=5000)
        for _ in range(2):
            selectors.nth(select_index).click(force=True)
            self.page.wait_for_timeout(800)
            option = frame.locator(
                ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
            ).filter(has_text=option_text).first
            expect(option).to_be_visible(timeout=5000)
            option.click(force=True)
            self.page.wait_for_timeout(1000)
            selected_text = selectors.nth(select_index).inner_text(timeout=5000)
            if option_text in selected_text:
                return
        selected_text = selectors.nth(select_index).inner_text(timeout=5000)
        raise AssertionError(
            f"第 {select_index + 1} 个下拉未能选择「{option_text}」，当前选择结果：{selected_text}"
        )

    def select_dispatch_config_first_available_option(self, frame: Frame, select_index: int) -> None:
        selectors = frame.locator(".ant-modal .ant-select-selector")
        selectors.nth(select_index).click()
        self.page.wait_for_timeout(800)
        option = frame.locator(
            ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content"
        ).filter(has_not_text="请选择").first
        expect(option).to_be_visible(timeout=5000)
        option.click()
        self.page.wait_for_timeout(800)

    def click_frame_button_by_js(self, frame: Frame, text: str) -> None:
        clicked = frame.evaluate(
            """text => {
                const buttons = Array.from(document.querySelectorAll('button'));
                const target = buttons.find(el => {
                    const value = (el.innerText || el.textContent || '').trim();
                    return value === text || value.includes(text.replace(/\\s+/g, '')) || value.replace(/\\s+/g, '').includes(text.replace(/\\s+/g, ''));
                });
                if (!target) return false;
                target.click();
                return true;
            }""",
            text,
        )
        if not clicked:
            body_text = frame.locator("body").inner_text(timeout=5000)
            raise AssertionError(f"未点击到 iframe 按钮 {text}，当前文本：{body_text[:1000]}")
