"""test_mesh_comms.py -- Unit tests for Issue #171 robot mesh comms."""

from __future__ import annotations
import json, os, time
from typing import Dict
import pytest


def _pkg_root():
    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def _read_src(rel_path):
    with open(os.path.join(_pkg_root(), rel_path)) as f:
        return f.read()


# ── Message definitions ───────────────────────────────────────────────────────

class TestMeshPeerMsg:
    @pytest.fixture(scope="class")
    def src(self):
        return _read_src("../saltybot_social_msgs/msg/MeshPeer.msg")

    def test_robot_id(self, src):      assert "string  robot_id" in src
    def test_namespace(self, src):     assert "string  namespace" in src
    def test_social_state(self, src):  assert "string  social_state" in src
    def test_active_ids(self, src):    assert "int32[] active_person_ids" in src
    def test_greeted(self, src):       assert "string[] greeted_person_names" in src
    def test_battery(self, src):       assert "float32 battery_pct" in src
    def test_header(self, src):        assert "std_msgs/Header" in src
    def test_issue_tag(self, src):     assert "171" in src


class TestMeshHandoffMsg:
    @pytest.fixture(scope="class")
    def src(self):
        return _read_src("../saltybot_social_msgs/msg/MeshHandoff.msg")

    def test_from_robot(self, src):    assert "from_robot_id" in src
    def test_to_robot(self, src):      assert "to_robot_id" in src
    def test_person_id(self, src):     assert "string  person_id" in src
    def test_person_name(self, src):   assert "string  person_name" in src
    def test_face_id(self, src):       assert "int32   face_id" in src
    def test_speaker_id(self, src):    assert "string  speaker_id" in src
    def test_bearing(self, src):       assert "bearing_to_target_deg" in src
    def test_distance(self, src):      assert "distance_m" in src
    def test_engagement(self, src):    assert "engagement_score" in src
    def test_summary(self, src):       assert "conversation_summary" in src
    def test_last_utterance(self, src):assert "last_utterance" in src
    def test_last_response(self, src): assert "last_response" in src
    def test_language(self, src):      assert "string  language" in src
    def test_header(self, src):        assert "std_msgs/Header" in src


# ── CMakeLists includes new msgs ──────────────────────────────────────────────

class TestCMakeLists:
    @pytest.fixture(scope="class")
    def src(self):
        return _read_src("../saltybot_social_msgs/CMakeLists.txt")

    def test_mesh_peer(self, src):    assert '"msg/MeshPeer.msg"' in src
    def test_mesh_handoff(self, src): assert '"msg/MeshHandoff.msg"' in src
    def test_issue_tag(self, src):    assert "Issue #171" in src


# ── Node source structure ─────────────────────────────────────────────────────

class TestMeshCommsNodeSrc:
    @pytest.fixture(scope="class")
    def src(self):
        return _read_src("saltybot_social/mesh_comms_node.py")

    def test_class_defined(self, src):        assert "class MeshCommsNode" in src
    def test_robot_id_param(self, src):       assert '"robot_id"' in src
    def test_announce_rate_param(self, src):  assert '"announce_rate_hz"' in src
    def test_peer_timeout_param(self, src):   assert '"peer_timeout_s"' in src
    def test_handoff_dist_param(self, src):   assert '"handoff_distance_m"' in src
    def test_greeted_memory_param(self, src): assert '"greeted_memory_s"' in src
    def test_announce_pub(self, src):         assert "/social/mesh/announce" in src
    def test_handoff_pub(self, src):          assert "/social/mesh/handoff" in src
    def test_peers_pub(self, src):            assert "/social/mesh/peers" in src
    def test_greeted_pub(self, src):          assert "/social/mesh/greeted" in src
    def test_person_states_sub(self, src):    assert "/social/person_states" in src
    def test_orch_state_sub(self, src):       assert "/social/orchestrator/state" in src
    def test_on_announce(self, src):          assert "_on_announce" in src
    def test_on_handoff(self, src):           assert "_on_handoff" in src
    def test_on_person_states(self, src):     assert "_on_person_states" in src
    def test_trigger_handoff(self, src):      assert "_trigger_handoff" in src
    def test_publish_announce(self, src):     assert "_publish_announce" in src
    def test_cleanup(self, src):              assert "_cleanup" in src
    def test_leaving_constant(self, src):     assert "_LEAVING" in src
    def test_engaged_constant(self, src):     assert "_ENGAGED" in src
    def test_talking_constant(self, src):     assert "_TALKING" in src
    def test_main(self, src):                 assert "def main" in src
    def test_no_self_announce(self, src):
        assert "self._robot_id" in src  # ignores own announcements
    def test_mesh_qos_best_effort(self, src): assert "BEST_EFFORT" in src
    def test_issue_tag(self, src):            assert "171" in src


# ── setup.py entry point ──────────────────────────────────────────────────────

class TestSetupPy:
    @pytest.fixture(scope="class")
    def src(self):
        return _read_src("setup.py")

    def test_mesh_entry_point(self, src):
        assert "mesh_comms_node = saltybot_social.mesh_comms_node:main" in src


# ── Config file ───────────────────────────────────────────────────────────────

class TestMeshConfig:
    @pytest.fixture(scope="class")
    def src(self):
        return _read_src("config/mesh_params.yaml")

    def test_node_name(self, src):       assert "mesh_comms_node:" in src
    def test_robot_id(self, src):        assert "robot_id:" in src
    def test_announce_rate(self, src):   assert "announce_rate_hz:" in src
    def test_peer_timeout(self, src):    assert "peer_timeout_s:" in src
    def test_handoff_dist(self, src):    assert "handoff_distance_m:" in src
    def test_greeted_memory(self, src):  assert "greeted_memory_s:" in src


# ── Peer tracking logic (pure Python, no ROS) ────────────────────────────────

class TestPeerTracking:
    def _make_peers(self):
        return {}

    def _add_peer(self, peers, rid, state, active, greeted):
        peers[rid] = {
            "last_seen": time.time(),
            "msg": type("M", (), {
                "social_state": state,
                "active_person_ids": active,
                "greeted_person_names": greeted,
            })(),
        }

    def _cleanup(self, peers, timeout=5.0):
        now = time.time()
        return {rid: d for rid, d in peers.items()
                if now - d["last_seen"] <= timeout}

    def test_add_peer(self):
        peers = self._make_peers()
        self._add_peer(peers, "bot2", "idle", [], [])
        assert "bot2" in peers

    def test_ignore_self(self):
        peers = self._make_peers()
        robot_id = "saltybot_1"
        incoming_id = "saltybot_1"
        if incoming_id != robot_id:
            self._add_peer(peers, incoming_id, "idle", [], [])
        assert "saltybot_1" not in peers

    def test_peer_replaced_on_update(self):
        peers = self._make_peers()
        self._add_peer(peers, "bot2", "idle", [], [])
        self._add_peer(peers, "bot2", "speaking", [42], [])
        assert peers["bot2"]["msg"].social_state == "speaking"

    def test_cleanup_removes_stale(self):
        peers = self._make_peers()
        peers["bot3"] = {"last_seen": time.time() - 10, "msg": None}
        self._add_peer(peers, "bot4", "idle", [], [])
        cleaned = self._cleanup(peers, timeout=5.0)
        assert "bot3" not in cleaned
        assert "bot4" in cleaned

    def test_multiple_peers(self):
        peers = self._make_peers()
        for i in range(5):
            self._add_peer(peers, f"bot{i}", "idle", [], [])
        assert len(peers) == 5


# ── Greeting deduplication logic ─────────────────────────────────────────────

class TestGreetingDedup:
    def test_new_name_added(self):
        greeted: Dict[str, float] = {}
        name = "Alice"
        if name not in greeted:
            greeted[name] = time.time()
        assert "Alice" in greeted

    def test_existing_name_not_overwritten(self):
        greeted = {"Alice": 1000.0}
        t_before = greeted["Alice"]
        if "Alice" not in greeted:
            greeted["Alice"] = time.time()
        assert greeted["Alice"] == t_before

    def test_peer_names_merged(self):
        greeted: Dict[str, float] = {}
        peer_names = ["Alice", "Bob"]
        for n in peer_names:
            if n and n not in greeted:
                greeted[n] = time.time()
        assert "Alice" in greeted and "Bob" in greeted

    def test_expire_old_names(self):
        greeted = {"Alice": time.time() - 400, "Bob": time.time()}
        memory_s = 300.0
        fresh = {n: t for n, t in greeted.items() if time.time() - t < memory_s}
        assert "Alice" not in fresh and "Bob" in fresh


# ── Handoff trigger logic ─────────────────────────────────────────────────────

class TestHandoffTrigger:
    def _should_handoff(self, prev_state, new_state, pid, sent):
        _LEAVING = 4
        return (prev_state != _LEAVING
                and new_state == _LEAVING
                and str(pid) not in sent)

    def test_triggers_on_leaving(self):
        assert self._should_handoff(2, 4, 1, set())

    def test_no_duplicate_handoff(self):
        sent = {"1"}
        assert not self._should_handoff(2, 4, 1, sent)

    def test_no_trigger_if_already_leaving(self):
        assert not self._should_handoff(4, 4, 1, set())

    def test_triggers_for_different_person(self):
        sent = {"1"}
        assert self._should_handoff(2, 4, 2, sent)

    def test_handoff_sent_cleared_when_person_gone(self):
        sent = {"1", "2", "3"}
        active_pids = {"2", "3"}
        sent &= active_pids
        assert "1" not in sent and "2" in sent


# ── JSON status blob ──────────────────────────────────────────────────────────

class TestPeersBlob:
    def test_peers_json(self):
        peers = {
            "bot2": {
                "last_seen": time.time(),
                "msg": type("M", (), {
                    "social_state": "idle",
                    "active_person_ids": [],
                })(),
            }
        }
        entries = [
            {
                "robot_id":       rid,
                "social_state":   d["msg"].social_state,
                "active_persons": len(d["msg"].active_person_ids),
                "age_s":          round(time.time() - d["last_seen"], 1),
            }
            for rid, d in peers.items()
        ]
        blob = json.dumps({"peers": entries, "ts": time.time()})
        parsed = json.loads(blob)
        assert len(parsed["peers"]) == 1
        assert parsed["peers"][0]["robot_id"] == "bot2"

    def test_greeted_json(self):
        greeted = {"Alice": time.time(), "Bob": time.time()}
        names = list(greeted.keys())
        blob = json.dumps({"greeted": names, "ts": time.time()})
        parsed = json.loads(blob)
        assert "Alice" in parsed["greeted"] and "Bob" in parsed["greeted"]
