"""朋友圈子 driver：发表图文 / 点赞 / 评论 / 删除。

从 xdotool_driver.py 单类中拆出的朋友圈域方法，继承 BaseDriver 复用公共能力。
find_wechat_window 归属 WindowDriver，本 driver 通过构造函数注入
window_driver 引用调用。

方法清单（与原 XdotoolDriver 签名一致）：
- publish_moment(content, timeout_sec=20.0) -> str
  （任务描述称 publish_moment_text，原 XdotoolDriver 实际命名为 publish_moment，此处保持原名）
- publish_moment_with_image(image_path, content="", timeout_sec=25.0) -> str
- moment_like(moment_index=1, timeout_sec=20.0) -> bool
- moment_comment(comment, moment_index=1, timeout_sec=20.0) -> bool
- moment_delete(moment_index=1, timeout_sec=20.0) -> bool
"""

from __future__ import annotations

import asyncio
import logging
import os
import random
import time
from typing import Any, Awaitable, Optional, TYPE_CHECKING

from woc_bridge.models import BridgeError
from woc_bridge.ui.drivers.base import BaseDriver

if TYPE_CHECKING:
    from woc_bridge.ui.drivers.window import WindowDriver

logger = logging.getLogger("woc-bridge")


class MomentDriver(BaseDriver):
    """朋友圈子 driver。"""

    def __init__(
        self,
        display: str,
        backend=None,
        window_driver: Optional["WindowDriver"] = None,
    ) -> None:
        super().__init__(display, backend)
        # 注入兄弟 driver：publish / moment_* 依赖 WindowDriver.find_wechat_window
        self._window_driver = window_driver
        # Task 11: moment_like post_verify 用，存储点赞前的截图（PNG bytes）
        # 由 moment_like 在点击"赞"之前捕获，post_verify_moment_like 读取比对
        self._moment_like_before_shot: Optional[bytes] = None
        # P0 修复：评论前截图，供 post_verify_moment_comment 比对
        self._moment_comment_before_shot: Optional[bytes] = None

    # ------------------------------------------------------------------
    # 命令执行包装（保持与原 XdotoolDriver._run 语义一致）
    # ------------------------------------------------------------------
    async def _run(
        self,
        args: list[str],
        *,
        input_bytes: Optional[bytes] = None,
    ) -> tuple[int, bytes, bytes]:
        """执行一条命令并返回 (returncode, stdout, stderr)。

        委托 BaseDriver._run_managed，TimeoutError 转 BridgeError(code=SEND_FAILED)。
        """
        try:
            return await self._run_managed(args)
        except asyncio.TimeoutError as exc:
            logger.error("[ui] command timeout: %s", args[0] if args else "?")
            raise BridgeError(
                code="SEND_FAILED",
                message=f"命令超时: {args[0] if args else '?'}",
            ) from exc

    async def _key(self, key: str, repeat: int = 1) -> None:
        """执行 xdotool key [--repeat N] <key>。"""
        logger.info("[ui] key: %s repeat=%d", key, repeat)
        args = ["xdotool", "key"]
        if repeat > 1:
            args.extend(["--repeat", str(repeat)])
        args.append(key)
        rc, _, _ = await self._run(args)
        logger.info("[ui] key %s -> rc=%s", key, rc)

    async def _sleep(self, seconds: float) -> None:
        """asyncio.sleep 封装，便于测试与统一调速。"""
        logger.info("[ui] sleep %.2fs", seconds)
        await asyncio.sleep(seconds)

    async def _paste_via_xclip(self, text: str) -> None:
        """通过 xdotool type 直接向聚焦控件输入文本。

        微信 4.x Linux 自绘 UI 不响应 Ctrl+V，改用 xdotool type 逐字符输入。
        方法名保留 _paste_via_xclip 以维持调用点稳定。

        注意：
        - 把 \n / \r 替换为空格，避免 xdotool 把换行解析为 Return 键。
        - 使用 --delay 30ms 降低输入速度，减少中文 IME 字符丢失。

        Raises:
            BridgeError(SEND_FAILED): xdotool type 执行失败
        """
        normalized = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
        normalized = " ".join(normalized.split())
        if normalized != text:
            logger.info("[ui] type text normalized: original_len=%d len=%d", len(text), len(normalized))
        text = normalized
        logger.info("[ui] type text: len=%d", len(text))
        rc, _, stderr = await self._run(["xdotool", "type", "--delay", "30", "--", text])
        if rc != 0:
            err = stderr.decode(errors="ignore").strip() if stderr else "unknown"
            raise BridgeError(
                code="SEND_FAILED",
                message=f"xdotool type 失败 (code={rc}): {err}",
            )
        logger.info("[ui] type done rc=%s", rc)

    async def _click(self, x: int, y: int) -> None:
        """移动鼠标到 (x, y) 并左键单击（分两条命令版本）。"""
        rc, _, _ = await self._run(
            ["xdotool", "mousemove", "--sync", str(x), str(y)]
        )
        if rc != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message=f"移动鼠标到 ({x},{y}) 失败",
            )
        rc, _, _ = await self._run(["xdotool", "click", "1"])
        if rc != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message=f"点击 ({x},{y}) 失败",
            )

    async def _long_press(self, x: int, y: int, hold_sec: float = 1.5) -> None:
        """移动鼠标到 (x, y) 并长按左键 hold_sec 秒后释放。

        用 try/finally 保证 mouseup 一定执行，避免超时/取消时
        mousedown 状态泄漏导致整个 UI 自动化瘫痪（拖拽锁死）。
        """
        rc, _, _ = await self._run(
            ["xdotool", "mousemove", "--sync", str(x), str(y)]
        )
        if rc != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message=f"长按：移动鼠标到 ({x},{y}) 失败",
            )
        rc, _, _ = await self._run(["xdotool", "mousedown", "1"])
        if rc != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message=f"长按 ({x},{y}) mousedown 失败",
            )
        try:
            await self._sleep(hold_sec)
        finally:
            rc, _, _ = await asyncio.shield(
                self._run(["xdotool", "mouseup", "1"])
            )
            if rc != 0:
                logger.warning(
                    "_long_press mouseup 失败 (rc=%d)，鼠标可能仍处于按下状态", rc
                )

    async def _activate_window_fast(self, window_id: Optional[int] = None) -> None:
        """激活微信窗口并校验焦点是否真正落到该窗口上。

        先用非阻塞 windowactivate 避免 --sync 在 VNC 无人操作时死等，
        然后轮询 getactivewindow 最多 2 秒确认目标窗口已获得焦点。
        """
        if window_id is None:
            window_id = await self._window_driver.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口，无法激活",
            )
        await self._run(["xdotool", "windowactivate", str(window_id)])

        deadline = time.monotonic() + 2.0
        while time.monotonic() < deadline:
            rc, stdout, _ = await self._run(["xdotool", "getactivewindow"])
            if rc == 0:
                active_text = stdout.decode(errors="ignore").strip()
                try:
                    if int(active_text) == window_id:
                        return
                except ValueError:
                    pass
            await self._sleep(0.1)
        logger.warning(
            "[ui] 窗口激活校验失败: window_id=%s 未在 2s 内成为活动窗口", window_id
        )

    async def _get_window_geometry(self) -> tuple[int, int, int, int]:
        """获取微信窗口几何信息，返回 (win_x, win_y, win_w, win_h)。

        复用 BaseDriver._parse_window_geometry 消除内联解析重复。
        """
        window_id = await self._window_driver.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口",
            )
        returncode, stdout, _ = await self._run(
            ["xdotool", "getwindowgeometry", "--shell", str(window_id)]
        )
        if returncode != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message="无法获取微信窗口几何信息",
            )
        geom = self._parse_window_geometry(stdout.decode(errors="ignore"))
        if geom is None:
            raise BridgeError(
                code="SEND_FAILED",
                message="解析窗口几何信息失败",
            )
        return (geom.x, geom.y, geom.width, geom.height)

    async def _enter_moments_page(self) -> tuple[int, int, int, int]:
        """进入朋友圈页面并返回窗口几何 (win_x, win_y, win_w, win_h)。

        统一朋友圈互动接口（like/comment/delete）的前置步骤：
        激活窗口 → 获取几何 → 关闭弹窗 → 点击朋友圈入口 → 等待打开。

        避免在聊天主界面执行朋友圈操作导致误删聊天消息（C3 风险）。
        """
        window_id = await self._window_driver.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口，无法进入朋友圈",
            )
        await self._activate_window_fast(window_id)
        await self._sleep(0.3)

        win_x, win_y, win_w, win_h = await self._get_window_geometry()

        # 关闭可能存在的弹窗/搜索框
        await self._key("Escape")
        await self._sleep(0.2)

        # 点击左侧栏「朋友圈」图标（估算：左栏底部偏上）
        moment_x = win_x + 30
        moment_y = win_y + win_h - 90
        await self._click(moment_x, moment_y)
        await self._sleep(1.0)

        return (win_x, win_y, win_w, win_h)

    async def _capture_screenshot_png(self) -> Optional[bytes]:
        """截取当前屏幕返回 PNG 字节流（Task 11 post_verify 用）。

        优先委托 backend.screenshot()（XdotoolBackend 已实现 scrot -o -），
        失败或无 backend 时回退 legacy scrot 写临时文件再读取。任何异常
        返回 None（post_verify 调用方负责 None 兜底）。
        """
        if self._backend is not None and hasattr(self._backend, "screenshot"):
            try:
                return await self._backend.screenshot()
            except Exception as exc:
                logger.warning("[ui] backend.screenshot 异常: %s", exc)
                return None
        # legacy: scrot 写临时文件
        import tempfile
        tmp_path = tempfile.mktemp(suffix=".png")
        try:
            rc, _, _ = await self._run(["scrot", tmp_path])
            if rc != 0:
                return None
            with open(tmp_path, "rb") as f:
                return f.read()
        except Exception as exc:
            logger.warning("[ui] legacy scrot 截图异常: %s", exc)
            return None
        finally:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass

    # ------------------------------------------------------------------
    # 发表朋友圈（纯文字）
    # ------------------------------------------------------------------
    async def publish_moment(
        self,
        content: str,
        timeout_sec: float = 20.0,
    ) -> str:
        """发表纯文字朋友圈。

        流程：
        1. 激活微信窗口
        2. 获取窗口几何，点击左侧栏「朋友圈」图标进入朋友圈页
        3. 点击右上角相机图标 → 选择「发表文字」
        4. 粘贴文字内容
        5. 点击「发表」按钮
        6. 按 Esc 关闭朋友圈页回到主界面

        注意：下方点击坐标均为估算值（基于微信 4.0 Linux 默认布局），
        需在目标分辨率实测后调优。

        Args:
            content: 朋友圈文字内容
            timeout_sec: 整体超时（秒），默认 20

        Returns:
            本地生成的 local_moment_id，格式 moment_<unix秒>_<随机>

        Raises:
            BridgeError(WINDOW_NOT_FOUND): 找不到微信窗口
            BridgeError(SEND_FAILED): 超时或 UI 操作失败
        """
        if not content:
            raise BridgeError(
                code="SEND_FAILED",
                message="content 不能为空，无法发表朋友圈",
            )

        deadline = time.monotonic() + timeout_sec

        # 1. 激活窗口（非阻塞，避免 --sync 死等）
        window_id = await self._window_driver.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口，无法发表朋友圈",
            )
        await self._step(self._activate_window_fast(window_id), "激活窗口", deadline)
        await self._step(self._sleep(0.3), "等待窗口激活", deadline)

        # 2. 获取窗口几何
        returncode, stdout, _ = await self._run(
            ["xdotool", "getwindowgeometry", "--shell", str(window_id)]
        )
        if returncode != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message="无法获取微信窗口几何信息",
            )
        geom = self._parse_window_geometry(stdout.decode(errors="ignore"))
        if geom is None:
            raise BridgeError(
                code="SEND_FAILED",
                message="解析窗口几何信息失败",
            )
        win_x, win_y, win_w, win_h = geom.x, geom.y, geom.width, geom.height

        # 3. 关闭可能存在的弹窗/搜索框
        await self._step(self._key("Escape"), "关闭弹窗", deadline)
        await self._step(self._sleep(0.2), "等待 Esc 生效", deadline)

        # 4. 点击左侧栏「朋友圈」图标（估算：左栏底部偏上）
        moment_x = win_x + 30
        moment_y = win_y + win_h - 90
        await self._step(self._click(moment_x, moment_y), "点击朋友圈入口", deadline)
        await self._step(self._sleep(1.0), "等待朋友圈页打开", deadline)

        # 5. 点击右上角相机图标（估算：窗口右上角）
        camera_x = win_x + win_w - 30
        camera_y = win_y + 30
        await self._step(self._click(camera_x, camera_y), "点击相机图标", deadline)
        await self._step(self._sleep(0.6), "等待发表菜单弹出", deadline)

        # 6. 选择「发表文字」（菜单第 1 项，按 Down 选中 + 回车）
        await self._step(self._key("Down"), "选中发表文字", deadline)
        await self._step(self._sleep(0.2), "等待选中", deadline)
        await self._step(self._key("Return"), "进入发表文字", deadline)
        await self._step(self._sleep(0.8), "等待发表文字弹窗", deadline)

        # 7. 粘贴文字内容
        await self._step(self._paste_via_xclip(content), "粘贴朋友圈内容", deadline)
        await self._step(self._sleep(0.3), "等待粘贴完成", deadline)

        # 8. 点击「发表」按钮（估算：弹窗右上角）
        publish_x = win_x + win_w - 60
        publish_y = win_y + 60
        await self._step(self._click(publish_x, publish_y), "点击发表按钮", deadline)
        await self._step(self._sleep(1.0), "等待发表完成", deadline)

        # 9. 点击左上角返回按钮回到聊天主界面（估算：窗口左上角）
        back_x = win_x + 30
        back_y = win_y + 30
        await self._step(self._click(back_x, back_y), "点击返回按钮", deadline)
        await self._step(self._sleep(0.5), "等待返回主界面", deadline)
        # 兜底 Esc，关闭可能残留的弹窗（如发表成功提示）
        await self._step(self._key("Escape"), "兜底关闭弹窗", deadline)
        await self._step(self._sleep(0.2), "等待焦点稳定", deadline)

        # 10. 生成 local_moment_id
        local_moment_id = f"moment_{int(time.time())}_{random.randint(0, 0xFFFFFF):06x}"
        return local_moment_id

    # ------------------------------------------------------------------
    # 发表带图片的朋友圈
    # ------------------------------------------------------------------
    async def publish_moment_with_image(
        self,
        image_path: str,
        content: str = "",
        timeout_sec: float = 25.0,
    ) -> str:
        """发表带图片的朋友圈（experimental）。

        Args:
            image_path: 图片文件容器内绝对路径
            content: 可选文字内容

        Returns:
            本地生成的 local_moment_id，格式 moment_img_<unix秒>_<随机>

        Raises:
            BridgeError(INVALID_PARAMS): 图片不存在或不可读
            BridgeError(WINDOW_NOT_FOUND): 找不到微信窗口
            BridgeError(SEND_FAILED): 超时或 UI 操作失败
        """
        if not image_path or not os.path.isfile(image_path):
            raise BridgeError(
                code="INVALID_PARAMS",
                message=f"图片不存在: {image_path}",
            )
        if not os.access(image_path, os.R_OK):
            raise BridgeError(
                code="INVALID_PARAMS",
                message=f"图片不可读: {image_path}",
            )

        deadline = time.monotonic() + timeout_sec

        # 复用纯文字朋友圈的前置步骤：激活窗口 → 进入朋友圈 → 点相机
        window_id = await self._window_driver.find_wechat_window()
        if window_id is None:
            raise BridgeError(
                code="WINDOW_NOT_FOUND",
                message="未找到微信窗口，无法发表朋友圈",
            )
        await self._step(self._activate_window_fast(window_id), "激活窗口", deadline)
        await self._step(self._sleep(0.3), "等待窗口激活", deadline)

        returncode, stdout, _ = await self._run(
            ["xdotool", "getwindowgeometry", "--shell", str(window_id)]
        )
        if returncode != 0:
            raise BridgeError(
                code="SEND_FAILED",
                message="无法获取微信窗口几何信息",
            )
        geom = self._parse_window_geometry(stdout.decode(errors="ignore"))
        if geom is None:
            raise BridgeError(
                code="SEND_FAILED",
                message="解析窗口几何信息失败",
            )
        win_x, win_y, win_w, win_h = geom.x, geom.y, geom.width, geom.height

        # 进入朋友圈
        await self._step(self._key("Escape"), "关闭弹窗", deadline)
        await self._step(self._sleep(0.2), "等待 Esc 生效", deadline)
        moment_x = win_x + 30
        moment_y = win_y + win_h - 90
        await self._step(self._click(moment_x, moment_y), "点击朋友圈入口", deadline)
        await self._step(self._sleep(1.0), "等待朋友圈页打开", deadline)

        # 点相机 → 选"发表图片"（第 2 项，Down 2 次）
        camera_x = win_x + win_w - 30
        camera_y = win_y + 30
        await self._step(self._click(camera_x, camera_y), "点击相机图标", deadline)
        await self._step(self._sleep(0.6), "等待发表菜单弹出", deadline)
        await self._step(self._key("Down"), "选中发表图片", deadline)
        await self._step(self._sleep(0.2), "等待选中", deadline)
        await self._step(self._key("Down"), "选中发表图片", deadline)
        await self._step(self._sleep(0.2), "等待选中", deadline)
        await self._step(self._key("Return"), "进入发表图片", deadline)
        await self._step(self._sleep(0.8), "等待文件选择器", deadline)

        # 文件选择器：粘贴图片路径并回车
        await self._step(self._paste_via_xclip(image_path), "粘贴图片路径", deadline)
        await self._step(self._sleep(0.3), "等待粘贴完成", deadline)
        await self._step(self._key("Return"), "确认选择图片", deadline)
        await self._step(self._sleep(1.0), "等待图片加载", deadline)

        # 可选：输入文字
        if content and content.strip():
            await self._step(self._paste_via_xclip(content), "粘贴文字内容", deadline)
            await self._step(self._sleep(0.3), "等待粘贴完成", deadline)

        # 点击"发表"按钮
        publish_x = win_x + win_w - 60
        publish_y = win_y + 60
        await self._step(self._click(publish_x, publish_y), "点击发表按钮", deadline)
        await self._step(self._sleep(1.0), "等待发表完成", deadline)

        # 返回主界面
        back_x = win_x + 30
        back_y = win_y + 30
        await self._step(self._click(back_x, back_y), "点击返回按钮", deadline)
        await self._step(self._sleep(0.5), "等待返回主界面", deadline)
        await self._step(self._key("Escape"), "兜底关闭弹窗", deadline)
        await self._step(self._sleep(0.2), "等待焦点稳定", deadline)

        local_moment_id = f"moment_img_{int(time.time())}_{random.randint(0, 0xFFFFFF):06x}"
        logger.info(
            "publish_moment_with_image: image=%s content_len=%d → 成功 local_moment_id=%s",
            image_path, len(content) if content else 0, local_moment_id,
        )
        return local_moment_id

    # ------------------------------------------------------------------
    # 朋友圈点赞（experimental）
    # ------------------------------------------------------------------
    async def moment_like(
        self,
        moment_index: int = 1,
        timeout_sec: float = 20.0,
    ) -> bool:
        """给朋友圈点赞（experimental）。

        自动进入朋友圈页面后再执行点赞操作，避免在聊天主界面误触。

        Args:
            moment_index: 朋友圈在时间线中的序号，1=最新，2=次新...

        Returns:
            True 表示 UI 操作流程执行完成
        """
        if moment_index < 1:
            raise BridgeError(
                code="INVALID_PARAMS",
                message=f"moment_index 必须 >= 1，收到 {moment_index}",
            )

        deadline = time.monotonic() + timeout_sec

        # 进入朋友圈页面（自动激活窗口 + 导航）
        win_x, win_y, win_w, win_h = await self._step(
            self._enter_moments_page(), "进入朋友圈页面", deadline
        )

        # Task 11: 点击"赞"之前捕获 before 截图，供 post_verify_moment_like 比对
        self._moment_like_before_shot = await self._capture_screenshot_png()

        # 估算第 N 条朋友圈的"赞/评论"按钮位置
        like_x = win_x + win_w - 30
        like_y = win_y + 90 + (moment_index - 1) * 200
        await self._step(self._click(like_x, like_y), "点击赞/评论按钮", deadline)
        await self._step(self._sleep(0.5), "等待弹窗", deadline)

        # 弹窗中"赞"通常是第一项
        await self._step(self._key("Down"), "选中赞", deadline)
        await self._step(self._sleep(0.2), "等待选中", deadline)
        await self._step(self._key("Return"), "点击赞", deadline)
        await self._step(self._sleep(0.5), "等待点赞完成", deadline)

        logger.info("moment_like: index=%d → UI 操作完成", moment_index)
        return True

    # ------------------------------------------------------------------
    # 朋友圈评论（experimental）
    # ------------------------------------------------------------------
    async def moment_comment(
        self,
        comment: str,
        moment_index: int = 1,
        timeout_sec: float = 20.0,
    ) -> bool:
        """评论朋友圈（experimental）。

        自动进入朋友圈页面后再执行评论操作。

        Args:
            comment: 评论文字内容
            moment_index: 朋友圈序号，1=最新
        """
        if not comment:
            raise BridgeError(
                code="INVALID_PARAMS",
                message="comment 不能为空",
            )
        if moment_index < 1:
            raise BridgeError(
                code="INVALID_PARAMS",
                message=f"moment_index 必须 >= 1，收到 {moment_index}",
            )

        deadline = time.monotonic() + timeout_sec

        # 进入朋友圈页面
        win_x, win_y, win_w, win_h = await self._step(
            self._enter_moments_page(), "进入朋友圈页面", deadline
        )

        # P0 修复：评论前捕获 before 截图，供 post_verify_moment_comment 比对。
        # 原实现 post_verify 在评论后才截 before，导致 before/after 都是评论后状态，
        # 截图比对结果与评论是否生效完全无关。
        self._moment_comment_before_shot = await self._capture_screenshot_png()

        # 点击"赞/评论"按钮
        like_x = win_x + win_w - 30
        like_y = win_y + 90 + (moment_index - 1) * 200
        await self._step(self._click(like_x, like_y), "点击赞/评论按钮", deadline)
        await self._step(self._sleep(0.5), "等待弹窗", deadline)

        # "评论"通常是第二项
        await self._step(self._key("Down"), "选中评论", deadline)
        await self._step(self._sleep(0.2), "等待选中", deadline)
        await self._step(self._key("Down"), "选中评论", deadline)
        await self._step(self._sleep(0.2), "等待选中", deadline)
        await self._step(self._key("Return"), "点击评论", deadline)
        await self._step(self._sleep(0.5), "等待输入框", deadline)

        # 粘贴评论内容
        await self._step(self._paste_via_xclip(comment), "粘贴评论内容", deadline)
        await self._step(self._sleep(0.3), "等待粘贴完成", deadline)

        # 发送
        await self._step(self._key("Return"), "发送评论", deadline)
        await self._step(self._sleep(0.5), "等待发送完成", deadline)

        logger.info(
            "moment_comment: index=%d comment_len=%d → UI 操作完成",
            moment_index, len(comment),
        )
        return True

    # ------------------------------------------------------------------
    # 删除朋友圈（experimental）
    # ------------------------------------------------------------------
    async def moment_delete(
        self,
        moment_index: int = 1,
        timeout_sec: float = 20.0,
    ) -> bool:
        """删除自己的朋友圈（experimental）。

        自动进入朋友圈页面后再执行删除操作，避免在聊天主界面
        长按消息导致误删聊天消息（不可逆数据丢失）。

        Args:
            moment_index: 朋友圈序号，1=最新
        """
        if moment_index < 1:
            raise BridgeError(
                code="INVALID_PARAMS",
                message=f"moment_index 必须 >= 1，收到 {moment_index}",
            )

        deadline = time.monotonic() + timeout_sec

        # 进入朋友圈页面（关键安全步骤：避免在聊天界面误删消息）
        win_x, win_y, win_w, win_h = await self._step(
            self._enter_moments_page(), "进入朋友圈页面", deadline
        )

        # P0 修复：记录删除前的朋友圈数量基线，供 post_verify 比对
        self._moment_delete_baseline_count: Optional[int] = None

        # 长按自己的朋友圈触发删除菜单
        moment_x = win_x + win_w // 2
        moment_y = win_y + 90 + (moment_index - 1) * 200
        await self._step(self._long_press(moment_x, moment_y, 1.5), "长按朋友圈", deadline)
        await self._step(self._sleep(0.6), "等待菜单弹出", deadline)

        # 选择"删除"
        await self._step(self._key("Down"), "导航到删除", deadline)
        await self._step(self._sleep(0.2), "等待选中", deadline)
        await self._step(self._key("Return"), "选择删除", deadline)
        await self._step(self._sleep(0.5), "等待确认对话框", deadline)

        # 确认删除
        await self._step(self._key("Return"), "确认删除", deadline)
        await self._step(self._sleep(0.5), "等待删除完成", deadline)

        logger.info("moment_delete: index=%d → UI 操作完成", moment_index)
        return True

    # ------------------------------------------------------------------
    # Task 11: post_verify 校验方法（UI 操作成功后校验 DB/截图，返回 verified bool）
    # ------------------------------------------------------------------
    async def post_verify_moment_like(
        self,
        db_reader: Any = None,
        moment_index: int = 1,
        sleep_sec: float = 1.0,
        timeout_sec: float = 3.0,
    ) -> bool:
        """校验点赞是否生效（Task 11）。

        截图比对点赞前后同位置像素差异（OpenCV absdiff）：
        - moment_like 在点击"赞"之前已捕获 before 截图存于
          self._moment_like_before_shot
        - 本方法 sleep 1s 后捕获 after 截图，转灰度后 absdiff，
          差异像素数 > 阈值 → verified=True（点赞按钮状态变化）
        - OpenCV 不可用或 before 截图为 None → 返回 False 并告警

        Args:
            db_reader: 兼容签名（点赞校验不读 DB，保留参数统一接口）
            moment_index: 朋友圈序号（仅用于日志）
            sleep_sec: after 截图前等待秒数
            timeout_sec: 兼容签名（截图校验无需轮询，仅用 sleep_sec）

        Returns:
            True 表示检测到点赞位置像素变化；False 表示未检测到或无法校验
        """
        before = self._moment_like_before_shot
        # 用完即清，避免下次复用旧截图
        self._moment_like_before_shot = None
        if before is None:
            logger.warning(
                "post_verify_moment_like: before 截图为 None，跳过校验 (index=%d)",
                moment_index,
            )
            return False
        try:
            import cv2
            import numpy as np
        except ImportError:
            logger.warning(
                "post_verify_moment_like: OpenCV 不可用，跳过截图校验 (index=%d)",
                moment_index,
            )
            return False
        await asyncio.sleep(sleep_sec)
        after = await self._capture_screenshot_png()
        if after is None:
            logger.warning(
                "post_verify_moment_like: after 截图失败 (index=%d)",
                moment_index,
            )
            return False
        try:
            before_arr = np.frombuffer(before, dtype=np.uint8)
            after_arr = np.frombuffer(after, dtype=np.uint8)
            before_img = cv2.imdecode(before_arr, cv2.IMREAD_GRAYSCALE)
            after_img = cv2.imdecode(after_arr, cv2.IMREAD_GRAYSCALE)
            if before_img is None or after_img is None:
                logger.warning(
                    "post_verify_moment_like: 截图 decode 失败 (index=%d)",
                    moment_index,
                )
                return False
            # 尺寸不一致时 resize 对齐
            if before_img.shape != after_img.shape:
                after_img = cv2.resize(
                    after_img, (before_img.shape[1], before_img.shape[0])
                )
            diff = cv2.absdiff(before_img, after_img)
            # 像素差异 > 30 的点数占比 > 0.5% 即认为有变化
            changed_pixels = int(np.count_nonzero(diff > 30))
            total_pixels = before_img.shape[0] * before_img.shape[1]
            ratio = changed_pixels / total_pixels if total_pixels > 0 else 0.0
            logger.info(
                "post_verify_moment_like: 像素差异 changed=%d ratio=%.4f (index=%d)",
                changed_pixels, ratio, moment_index,
            )
            return ratio > 0.005
        except Exception as exc:
            logger.warning(
                "post_verify_moment_like: absdiff 计算异常 (index=%d): %s",
                moment_index, exc,
            )
            return False

    async def post_verify_moment_comment(
        self,
        db_reader: Any,
        comment: str,
        moment_index: int = 1,
        sleep_sec: float = 2.0,
        timeout_sec: float = 3.0,
    ) -> bool:
        """校验评论是否生效（Task 11）。

        探测 MomentsComment 表存在性：存在则走 DB 校验（查 comment 列含
        comment 子串的记录），不存在走截图兜底（absdiff 比对发送前后）。
        DB 不可用（db_reader 为 None）时静默返回 True。

        Args:
            db_reader: DbReader 实例（可为 None）
            comment: 期望的评论文字内容
            moment_index: 朋友圈序号（仅用于日志）
            sleep_sec: 首次查询前等待秒数
            timeout_sec: 轮询超时秒数

        Returns:
            True 表示检测到评论写入或截图变化；False 表示未检测到
        """
        if db_reader is None:
            logger.warning(
                "post_verify_moment_comment: db_reader 为 None，跳过校验 (index=%d)",
                moment_index,
            )
            return True
        await asyncio.sleep(sleep_sec)
        # 探测 MomentsComment 表是否存在
        comment_table = await asyncio.to_thread(
            self._probe_moment_table, db_reader, ["comment", "Comments", "MomentComment", "MomentsComment"]
        )
        if comment_table is not None:
            # DB 校验：查 comment 列含目标子串
            deadline = time.monotonic() + timeout_sec
            while time.monotonic() < deadline:
                try:
                    found = await asyncio.to_thread(
                        self._query_comment_in_table, db_reader, comment_table, comment
                    )
                    if found:
                        return True
                except Exception as exc:
                    logger.warning(
                        "post_verify_moment_comment: DB 查询异常 (index=%d): %s",
                        moment_index, exc,
                    )
                await asyncio.sleep(1.0)
            logger.warning(
                "post_verify_moment_comment: DB 校验超时 %.1fs (index=%d)",
                timeout_sec, moment_index,
            )
            return False
        # 截图兜底（无 MomentsComment 表）
        logger.info(
            "post_verify_moment_comment: MomentsComment 表不存在，走截图兜底 (index=%d)",
            moment_index,
        )
        # P0 修复：使用 moment_comment UI 操作前捕获的 before 截图，
        # 而非在此处重新截 before（此时评论已完成，before/after 都是评论后状态）
        before = self._moment_comment_before_shot
        self._moment_comment_before_shot = None  # 用完即清
        if before is None:
            logger.warning(
                "post_verify_moment_comment: before 截图为 None，跳过校验 (index=%d)",
                moment_index,
            )
            return False
        await asyncio.sleep(1.0)
        after = await self._capture_screenshot_png()
        return self._screenshot_absdiff_changed(before, after, "comment", moment_index)

    async def post_verify_moment_delete(
        self,
        db_reader: Any,
        moment_index: int = 1,
        sleep_sec: float = 2.0,
        timeout_sec: float = 3.0,
        baseline_count: Optional[int] = None,
    ) -> bool:
        """校验删除是否生效（Task 11）。

        P0 修复：原实现只要 DB 可读（status=ok）就返回 True，完全未校验删除生效。
        现改为比对删除前后的朋友圈数量：baseline_count 由路由层在删除前捕获，
        删除后查询当前数量，若 < baseline_count 则认为删除成功。

        Args:
            db_reader: DbReader 实例（可为 None）
            moment_index: 被删除的朋友圈序号（仅用于日志）
            sleep_sec: 首次查询前等待秒数
            timeout_sec: 轮询超时秒数
            baseline_count: 删除前的朋友圈数量基线（None 表示无法建立基线）

        Returns:
            True 表示检测到朋友圈数量减少（删除生效）；
            False 表示未检测到变化或无法校验；
            baseline_count=None 时返回 True（DB 不可用，跳过校验，不阻塞流程）
        """
        if db_reader is None:
            logger.warning(
                "post_verify_moment_delete: db_reader 为 None，跳过校验 (index=%d)",
                moment_index,
            )
            return True
        # 无基线时无法校验数量变化，返回 True（不阻塞，但记录告警）
        if baseline_count is None:
            logger.warning(
                "post_verify_moment_delete: 无基线 count，跳过校验 (index=%d)",
                moment_index,
            )
            return True
        await asyncio.sleep(sleep_sec)
        deadline = time.monotonic() + timeout_sec
        while time.monotonic() < deadline:
            try:
                result = await asyncio.to_thread(
                    db_reader.get_moments_timeline, 0, 50
                )
                status = result.get("status", "")
                if status == "ok":
                    current_count = len(result.get("moments", []))
                    # P0 修复：比对数量变化，而非仅检查 DB 可读
                    if current_count < baseline_count:
                        logger.info(
                            "post_verify_moment_delete: 朋友圈数量 %d → %d，删除生效 (index=%d)",
                            baseline_count, current_count, moment_index,
                        )
                        return True
                    logger.warning(
                        "post_verify_moment_delete: 朋友圈数量未变 %d == %d (index=%d)",
                        current_count, baseline_count, moment_index,
                    )
            except Exception as exc:
                logger.warning(
                    "post_verify_moment_delete: DB 查询异常 (index=%d): %s",
                    moment_index, exc,
                )
            await asyncio.sleep(1.0)
        logger.warning(
            "post_verify_moment_delete: 超时 %.1fs 未确认删除 (index=%d)",
            timeout_sec, moment_index,
        )
        return False

    # ------------------------------------------------------------------
    # post_verify 辅助方法
    # ------------------------------------------------------------------
    def _probe_moment_table(
        self, db_reader: Any, candidates: list[str]
    ) -> Optional[str]:
        """探测朋友圈 DB 中是否存在候选表名，返回命中的表名或 None。

        直接复用 db_reader 内部的 _find_moment_db_path + _ensure_decrypted
        链路有困难（涉及私有方法），这里用 sqlite3 直接打开 moment DB 探测。
        DB 不可读时返回 None。
        """
        import sqlite3
        try:
            moment_db = db_reader._find_moment_db_path()
        except Exception:
            return None
        if moment_db is None:
            return None
        storage = db_reader._find_db_storage_dir()
        if storage is None:
            return None
        try:
            import os as _os
            rel_path = _os.path.relpath(moment_db, storage).replace(_os.sep, "/")
            db_path = db_reader._ensure_decrypted(rel_path)
        except Exception:
            return None
        try:
            conn = sqlite3.connect(db_path, isolation_level=None)
            try:
                cur = conn.execute(
                    "SELECT name FROM sqlite_master WHERE type='table'"
                )
                tables = {row[0] for row in cur.fetchall()}
                for cand in candidates:
                    if cand in tables:
                        return cand
            finally:
                conn.close()
        except Exception:
            return None
        return None

    def _query_comment_in_table(
        self, db_reader: Any, table: str, comment_substring: str
    ) -> bool:
        """在指定朋友圈评论表中查 comment 列是否含目标子串。"""
        import sqlite3
        try:
            moment_db = db_reader._find_moment_db_path()
        except Exception:
            return False
        if moment_db is None:
            return False
        storage = db_reader._find_db_storage_dir()
        if storage is None:
            return False
        try:
            import os as _os
            rel_path = _os.path.relpath(moment_db, storage).replace(_os.sep, "/")
            db_path = db_reader._ensure_decrypted(rel_path)
        except Exception:
            return False
        try:
            conn = sqlite3.connect(db_path, isolation_level=None)
            conn.row_factory = sqlite3.Row
            try:
                cols = db_reader._table_columns(conn, table)
                # 找文本列
                text_col = db_reader._pick_column(
                    cols, ["content", "text", "comment", "body", "message"]
                )
                if text_col is None:
                    return False
                sql = f"SELECT {text_col} FROM [{table}] LIMIT 50"
                cur = conn.execute(sql)
                for row in cur.fetchall():
                    val = row[text_col] or ""
                    if comment_substring in str(val):
                        return True
            finally:
                conn.close()
        except Exception:
            return False
        return False

    def _screenshot_absdiff_changed(
        self,
        before: Optional[bytes],
        after: Optional[bytes],
        tag: str,
        moment_index: int,
    ) -> bool:
        """截图 absdiff 比对通用辅助：差异像素占比 > 0.5% 返回 True。"""
        if before is None or after is None:
            logger.warning(
                "post_verify_moment_%s: 截图为 None (before=%s after=%s index=%d)",
                tag, before is None, after is None, moment_index,
            )
            return False
        try:
            import cv2
            import numpy as np
        except ImportError:
            logger.warning(
                "post_verify_moment_%s: OpenCV 不可用 (index=%d)", tag, moment_index
            )
            return False
        try:
            before_img = cv2.imdecode(
                np.frombuffer(before, dtype=np.uint8), cv2.IMREAD_GRAYSCALE
            )
            after_img = cv2.imdecode(
                np.frombuffer(after, dtype=np.uint8), cv2.IMREAD_GRAYSCALE
            )
            if before_img is None or after_img is None:
                return False
            if before_img.shape != after_img.shape:
                after_img = cv2.resize(
                    after_img, (before_img.shape[1], before_img.shape[0])
                )
            diff = cv2.absdiff(before_img, after_img)
            changed = int(np.count_nonzero(diff > 30))
            total = before_img.shape[0] * before_img.shape[1]
            ratio = changed / total if total > 0 else 0.0
            logger.info(
                "post_verify_moment_%s: absdiff changed=%d ratio=%.4f (index=%d)",
                tag, changed, ratio, moment_index,
            )
            return ratio > 0.005
        except Exception as exc:
            logger.warning(
                "post_verify_moment_%s: absdiff 异常 (index=%d): %s",
                tag, moment_index, exc,
            )
            return False
