"""Tests for League of Legends Live Client Data API adapter.""" import threading import pytest from ledgrab.core.game_integration.adapters.lol_adapter import ( LoLAdapter, LoLPoller, ) def _make_lol_payload( *, current_health: float = 1000.0, max_health: float = 1000.0, resource_value: float = 500.0, resource_max: float = 500.0, level: int = 10, summoner_name: str = "TestPlayer", current_gold: float | None = None, ) -> dict: """Build a realistic LoL Live Client Data payload.""" payload: dict = { "activePlayer": { "summonerName": summoner_name, "level": level, "currentHealth": current_health, "championStats": { "currentHealth": current_health, "maxHealth": max_health, "resourceValue": resource_value, "resourceMax": resource_max, "attackDamage": 80.0, "abilityPower": 0.0, "armor": 60.0, "magicResist": 40.0, "moveSpeed": 345.0, }, }, "allPlayers": [ { "summonerName": summoner_name, "championName": "Jinx", "team": "ORDER", "scores": {"kills": 5, "deaths": 2, "assists": 7}, }, ], "gameData": { "gameMode": "CLASSIC", "gameTime": 1200.5, "mapName": "Map11", "mapNumber": 11, "mapTerrain": "Default", }, } if current_gold is not None: payload["allPlayers"][0]["currentGold"] = current_gold return payload class TestLoLContinuousEvents: def test_health_normalized(self) -> None: payload = _make_lol_payload(current_health=500.0, max_health=1000.0) events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {}) hp = [e for e in events if e.event_type == "health"] assert len(hp) == 1 assert hp[0].value == pytest.approx(0.5) def test_mana_normalized(self) -> None: payload = _make_lol_payload(resource_value=300.0, resource_max=600.0) events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {}) mp = [e for e in events if e.event_type == "mana"] assert len(mp) == 1 assert mp[0].value == pytest.approx(0.5) def test_level_normalized(self) -> None: payload = _make_lol_payload(level=9) events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {}) lvl = [e for e in events if e.event_type == "speed"] assert len(lvl) == 1 assert lvl[0].value == pytest.approx(9.0 / 18.0) def test_level_max(self) -> None: payload = _make_lol_payload(level=18) events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {}) lvl = [e for e in events if e.event_type == "speed"] assert lvl[0].value == pytest.approx(1.0) def test_gold(self) -> None: payload = _make_lol_payload(current_gold=15000.0) events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {}) gold = [e for e in events if e.event_type == "gold"] assert len(gold) == 1 assert gold[0].value == pytest.approx(15000.0 / 30000.0) class TestLoLDeathRespawn: def test_death_detected_when_health_drops_to_zero(self) -> None: prev = {"alive": True} payload = _make_lol_payload(current_health=0.0, max_health=1000.0) events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev) deaths = [e for e in events if e.event_type == "death"] assert len(deaths) == 1 assert state["alive"] is False def test_no_death_when_already_dead(self) -> None: prev = {"alive": False} payload = _make_lol_payload(current_health=0.0, max_health=1000.0) events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev) deaths = [e for e in events if e.event_type == "death"] assert len(deaths) == 0 def test_respawn_detected(self) -> None: prev = {"alive": False} payload = _make_lol_payload(current_health=800.0, max_health=1000.0) events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev) respawns = [e for e in events if e.event_type == "objective_progress"] assert len(respawns) == 1 assert state["alive"] is True def test_no_respawn_when_already_alive(self) -> None: prev = {"alive": True} payload = _make_lol_payload(current_health=800.0, max_health=1000.0) events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev) respawns = [e for e in events if e.event_type == "objective_progress"] assert len(respawns) == 0 class TestLoLAuth: def test_always_accepts(self) -> None: assert LoLAdapter.validate_auth({}, {}, {}) is True assert LoLAdapter.validate_auth({"X-Custom": "val"}, {}, {"auth_token": "x"}) is True class TestLoLMetadata: def test_adapter_type(self) -> None: assert LoLAdapter.ADAPTER_TYPE == "lol" def test_supported_events(self) -> None: assert "health" in LoLAdapter.SUPPORTED_EVENTS assert "mana" in LoLAdapter.SUPPORTED_EVENTS assert "death" in LoLAdapter.SUPPORTED_EVENTS def test_config_schema(self) -> None: schema = LoLAdapter.get_config_schema() assert "poll_interval_ms" in schema["properties"] def test_setup_instructions(self) -> None: instructions = LoLAdapter.get_setup_instructions() assert "League of Legends" in instructions class TestLoLPoller: def test_start_stop(self) -> None: """Test that the poller starts and stops cleanly.""" called = threading.Event() def callback(data: dict) -> None: called.set() poller = LoLPoller({"poll_interval_ms": 100}, callback) assert not poller.is_running poller.start() assert poller.is_running poller.stop() assert not poller.is_running def test_double_start_no_crash(self) -> None: """Starting twice should not create duplicate threads.""" poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None) poller.start() poller.start() # should warn but not crash poller.stop() def test_stop_without_start(self) -> None: """Stopping without starting should not crash.""" poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None) poller.stop() # no-op