#!/usr/bin/env python3
"""
BIND-03 离线回归：Agent 公网主服有序回退候选构建 + 健康探测。
不依赖真机/WS；仅校验 _build_server_candidates 与 _probe_ws_base 纯逻辑。
真机寻服 E2E（断 LAN 自动连公网）见 §八 V3，仍须真机留痕。
"""
import os
import sys
import socket
import threading
import importlib.util

AGENT_PATH = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "agent", "agent.py",
)


def _load_agent_module():
    # 直接按文件路径加载，避免 import 触发 u2/websockets 之外的副作用
    spec = importlib.util.spec_from_file_location("wp_agent_under_test", AGENT_PATH)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def test_build_candidates_dedup_and_fill():
    mod = _load_agent_module()
    Agent = mod.WorkPhoneAgent
    primary = "ws://192.168.110.251:8899/ws/device/dev1"
    publics = [
        "ws://sdk.quwanzhi.com:8899",            # 缺 /ws/device + device_id
        "ws://sdk.quwanzhi.com:8899/ws/device",  # 缺 device_id
        "ws://192.168.110.251:8899/ws/device/dev1",  # 与主连接重复
        "ws://backup.quwanzhi.com:8899/ws/device/dev1",  # 已完整
    ]
    cands = Agent._build_server_candidates(primary, publics, "dev1")
    assert cands[0] == primary
    assert "ws://sdk.quwanzhi.com:8899/ws/device/dev1" in cands
    assert "ws://backup.quwanzhi.com:8899/ws/device/dev1" in cands
    # 去重：主连接与重复公网只出现一次
    assert cands.count(primary) == 1
    assert len(cands) == 3, f"应去重为 3 个候选，实际 {cands}"


def test_build_candidates_empty_public():
    mod = _load_agent_module()
    Agent = mod.WorkPhoneAgent
    primary = "ws://127.0.0.1:8899/ws/device/x"
    assert Agent._build_server_candidates(primary, [], "x") == [primary]
    assert Agent._build_server_candidates(primary, None, "x") == [primary]


def test_probe_unreachable():
    mod = _load_agent_module()
    Agent = mod.WorkPhoneAgent
    # 保留端口 9（discard）本机通常不监听，确定性不可达
    assert Agent._probe_ws_base("ws://127.0.0.1:9/ws/device/x", timeout=0.5) is False


def test_probe_reachable_local_socket():
    mod = _load_agent_module()
    Agent = mod.WorkPhoneAgent
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(("127.0.0.1", 0))
    srv.listen(1)
    port = srv.getsockname()[1]
    stop = threading.Event()

    def _accept():
        srv.settimeout(2)
        try:
            c, _ = srv.accept()
            c.close()
        except Exception:
            pass

    t = threading.Thread(target=_accept, daemon=True)
    t.start()
    try:
        url = f"ws://127.0.0.1:{port}/ws/device/x"
        assert Agent._probe_ws_base(url, timeout=1.0) is True
    finally:
        stop.set()
        srv.close()


class _FakeAgent:
    """最小 duck-typed self，避免重型构造（u2/frida/anti_ban）。"""
    VERSION = "test"
    def __init__(self, stage):
        self.d = None
        self.connect_stage = stage
        self.device_id = "dev1"
        self.project_id = "cunkebao"
        self.server_url = "ws://x:8899/ws/device/dev1"
        self.server_candidates = ["ws://x:8899/ws/device/dev1", "ws://y:8899/ws/device/dev1"]


def test_quick_status_includes_connect_stage():
    mod = _load_agent_module()
    fake = _FakeAgent("public")
    st = mod.WorkPhoneAgent._get_quick_status(fake)
    assert st["online"] is True
    assert st["connect_stage"] == "public", f"心跳须带 connect_stage，实际 {st}"


def test_device_info_includes_connect_stage():
    mod = _load_agent_module()
    fake = _FakeAgent("retry")
    info = mod.WorkPhoneAgent._get_device_info(fake)
    assert info["connect_stage"] == "retry"
    assert info["server_url"] == "ws://x:8899/ws/device/dev1"
    assert info["server_candidate_count"] == 2


if __name__ == "__main__":
    test_build_candidates_dedup_and_fill()
    test_build_candidates_empty_public()
    test_probe_unreachable()
    test_probe_reachable_local_socket()
    test_quick_status_includes_connect_stage()
    test_device_info_includes_connect_stage()
    print("✅ BIND-03/07 候选回退+寻服阶段上报离线回归全部通过")
