"""
Kiểm thử Bộ Điều Phối Vòng Đời Tự Hành (Autonomous Lifecycle Manager)
Kiểm tra logic 2 giai đoạn:
- Giai đoạn 1 (Chưa End Game): Campaign Grind (Cày cuốc & Làm quest)
- Giai đoạn 2 (Đã Đạt End Game): Endgame Earning (Farm Map & Arbitrage)
"""

import os
import sys
import time
import unittest
from unittest.mock import MagicMock

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from src.assistant_tool.autonomous_lifecycle import (
    AutonomousLifecycleManager,
    LifecyclePhase,
    EndgameMappingSubState,
    CharacterLifecycleState,
)


class TestAutonomousLifecycle(unittest.TestCase):
    def setUp(self):
        self.mock_bridge = MagicMock()
        self.mock_bridge.send_trigger_world_map_travel.return_value = True
        self.mock_controller = MagicMock()
        self.mock_controller.is_running.return_value = True

        self.lifecycle = AutonomousLifecycleManager(
            core_bridge=self.mock_bridge,
            core_controller=self.mock_controller,
            config={"auto_launch_client": False}
        )

    def test_evaluate_phase_campaign_grind(self):
        """Nhân vật Level < 65 và ở Act bình thường phải ở pha CAMPAIGN_GRIND."""
        self.lifecycle.player_level = 15
        self.lifecycle.current_area_code = "G1_2"
        self.lifecycle.current_area_name = "The Ogham Farmlands"

        phase = self.lifecycle.evaluate_phase()
        self.assertEqual(phase, LifecyclePhase.CAMPAIGN_GRIND)

    def test_evaluate_phase_endgame_earning(self):
        """Nhân vật Level >= 65 hoặc ở Map Atlas phải chuyển sang pha ENDGAME_EARNING."""
        self.lifecycle.player_level = 68
        self.lifecycle.current_area_code = "MapTier1"
        self.lifecycle.current_area_name = "Strand Map Tier 1"

        phase = self.lifecycle.evaluate_phase()
        self.assertEqual(phase, LifecyclePhase.ENDGAME_EARNING)

    def test_init_state_is_in_town_false(self):
        """Khởi tạo: is_in_town phải là False và has_real_map_log phải là False."""
        self.assertFalse(self.lifecycle.is_in_town)
        self.assertFalse(self.lifecycle.has_real_map_log)
        self.assertEqual(self.lifecycle.current_area_name, "")

    def test_town_travel_trigger(self):
        """Khi ở Làng trong pha Campaign, hệ thống chỉ gửi lệnh phím U dịch chuyển nếu sang map khác."""
        self.lifecycle.player_level = 10
        self.lifecycle.current_area_code = "G1_town"
        self.lifecycle.current_area_name = "Clearfell Encampment"
        self.lifecycle.is_in_town = True
        self.lifecycle.has_real_map_log = True  # Đã có log map thật từ game
        self.lifecycle.phase = LifecyclePhase.CAMPAIGN_GRIND
        self.lifecycle.last_travel_time = 0.0

        # 1. Khi mục tiêu nằm ngay tại thị trấn hiện tại (Clearfell Encampment) -> KHÔNG gửi lệnh travel vô nghĩa
        self.lifecycle._execute_campaign_grind_step()
        self.assertFalse(self.mock_bridge.send_trigger_world_map_travel.called)

        # 2. Khi hoàn thành quest tại làng và mục tiêu chuyển sang map chiến đấu khác (The Ogham Farmlands) -> Gửi lệnh phím U
        self.lifecycle.quester.record_objective_completed("Una")
        self.lifecycle._execute_campaign_grind_step()
        self.assertTrue(self.mock_bridge.send_trigger_world_map_travel.called)

        # 3. Khi ở Map chiến đấu (is_in_town = False) -> TUYỆT ĐỐI KHÔNG gửi send_trigger_world_map_travel
        self.mock_bridge.send_trigger_world_map_travel.reset_mock()
        self.lifecycle.is_in_town = False
        self.lifecycle.current_area_code = "G3_1"
        self.lifecycle.current_area_name = "Sandswept Marsh"
        self.lifecycle._execute_campaign_grind_step()
        self.assertFalse(self.mock_bridge.send_trigger_world_map_travel.called)

    def test_town_travel_requires_real_map_log(self):
        """Khi chưa có log map thật từ game (has_real_map_log = False) -> KHÔNG gửi send_trigger_world_map_travel."""
        self.lifecycle.player_level = 10
        self.lifecycle.current_area_code = "G1_town"
        self.lifecycle.current_area_name = "Clearfell Encampment"
        self.lifecycle.is_in_town = True
        self.lifecycle.has_real_map_log = False  # Chưa có log thật
        self.lifecycle.phase = LifecyclePhase.CAMPAIGN_GRIND
        self.lifecycle.last_travel_time = 0.0

        self.lifecycle.quester.record_objective_completed("Una")
        self.lifecycle._execute_campaign_grind_step()
        self.assertFalse(self.mock_bridge.send_trigger_world_map_travel.called)

    def test_town_travel_rejects_ui_scenes(self):
        """Khi tên map hiện tại là UI scene overlay (Act 1..4, Atlas...) -> KHÔNG gửi send_trigger_world_map_travel."""
        ui_scenes = ["Act 1", "Act 2", "Act 3", "Act 4", "Atlas", "(null)", "(unknown)"]
        self.lifecycle.player_level = 10
        self.lifecycle.is_in_town = True
        self.lifecycle.has_real_map_log = True
        self.lifecycle.phase = LifecyclePhase.CAMPAIGN_GRIND
        self.lifecycle.last_travel_time = 0.0

        for scene in ui_scenes:
            self.mock_bridge.send_trigger_world_map_travel.reset_mock()
            self.lifecycle.current_area_name = scene
            self.lifecycle._execute_campaign_grind_step()
            self.assertFalse(
                self.mock_bridge.send_trigger_world_map_travel.called,
                f"Không được gửi travel khi current_area_name là UI scene: {scene}"
            )

    def test_on_area_changed_filters_ui_scenes(self):
        """on_area_changed phải bỏ qua các UI scenes và chỉ chấp nhận map thật."""
        # 1. UI scene bị bỏ qua
        self.lifecycle.on_area_changed("", "Act 1")
        self.assertFalse(self.lifecycle.has_real_map_log)
        self.assertEqual(self.lifecycle.current_area_name, "")

        self.lifecycle.on_area_changed("", "Atlas")
        self.assertFalse(self.lifecycle.has_real_map_log)
        self.assertEqual(self.lifecycle.current_area_name, "")

        # 2. Map thật được chấp nhận và cập nhật has_real_map_log = True
        self.lifecycle.on_area_changed("G1_town", "Clearfell Encampment")
        self.assertTrue(self.lifecycle.has_real_map_log)
        self.assertEqual(self.lifecycle.current_area_name, "Clearfell Encampment")
        self.assertTrue(self.lifecycle.is_in_town)

    def test_telemetry_verifies_hive_fortress_without_travel(self):
        """SHM area_name Hive Fortress (Client.txt 10/09/2026 18:29) xác thực map, không mở World Map."""
        snap = MagicMock()
        snap.is_online = True
        snap.area_name = "Hive Fortress"
        snap.area_code = "ChayulaLeague_Tower"
        snap.player.level = 1

        self.lifecycle.on_telemetry_snapshot(snap)
        self.assertTrue(self.lifecycle.has_real_map_log)
        self.assertEqual(self.lifecycle.current_area_name, "Hive Fortress")
        self.assertEqual(self.lifecycle.current_area_code, "ChayulaLeague_Tower")
        self.assertFalse(self.lifecycle.is_in_town)

        texts = []
        self.lifecycle.register_listener(lambda _phase, text: texts.append(text))
        self.lifecycle.phase = LifecyclePhase.CAMPAIGN_GRIND
        self.lifecycle._execute_campaign_grind_step()
        self.assertFalse(self.mock_bridge.send_trigger_world_map_travel.called)
        self.assertTrue(any("CHIẾN ĐẤU" in t for t in texts))
        self.assertFalse(any("chờ xác thực" in t.lower() for t in texts))

    def test_telemetry_empty_or_ui_scene_does_not_verify(self):
        """area_name rỗng / Act 2 / Atlas không được coi là xác thực bản đồ."""
        snap = MagicMock()
        snap.is_online = True
        snap.player.level = 10

        snap.area_name = ""
        snap.area_code = ""
        self.lifecycle.on_telemetry_snapshot(snap)
        self.assertFalse(self.lifecycle.has_real_map_log)
        self.assertEqual(self.lifecycle.current_area_name, "")

        for ui_name in ("Act 2", "Atlas", "(null)", "[DNT] World Map"):
            snap.area_name = ui_name
            self.lifecycle.on_telemetry_snapshot(snap)
            self.assertFalse(
                self.lifecycle.has_real_map_log,
                f"UI scene {ui_name} không được xác thực",
            )
            self.assertEqual(self.lifecycle.current_area_name, "")

        texts = []
        self.lifecycle.register_listener(lambda _phase, text: texts.append(text))
        self.lifecycle.phase = LifecyclePhase.CAMPAIGN_GRIND
        self.lifecycle._execute_campaign_grind_step()
        self.assertTrue(any("chờ xác thực vị trí bản đồ" in t for t in texts))
        self.assertFalse(self.mock_bridge.send_trigger_world_map_travel.called)

    def test_telemetry_shoreline_hideout_is_verified_town(self):
        """Shoreline Hideout từ SHM là thị trấn đã xác thực (được phép travel sau cooldown)."""
        snap = MagicMock()
        snap.is_online = True
        snap.area_name = "Shoreline Hideout"
        snap.area_code = "HideoutShoreline"
        snap.player.level = 65
        self.lifecycle.on_telemetry_snapshot(snap)
        self.assertTrue(self.lifecycle.has_real_map_log)
        self.assertTrue(self.lifecycle.is_in_town)
        self.assertEqual(self.lifecycle.current_area_name, "Shoreline Hideout")

    def test_is_ui_scene_dnt_prefix(self):
        self.assertTrue(self.lifecycle.is_ui_scene("[DNT] World Map"))
        self.assertTrue(self.lifecycle.is_ui_scene(""))
        self.assertFalse(self.lifecycle.is_ui_scene("Hive Fortress"))
        self.assertFalse(self.lifecycle.is_ui_scene("Shoreline Hideout"))

    def test_listeners_notification(self):
        """Kiểm tra việc phát thông báo cập nhật giao diện."""
        received_phases = []
        received_texts = []

        def sample_listener(phase, text):
            received_phases.append(phase)
            received_texts.append(text)

        self.lifecycle.register_listener(sample_listener)
        self.lifecycle.phase = LifecyclePhase.CAMPAIGN_GRIND
        self.lifecycle._notify_listeners("Test Message")

        self.assertEqual(len(received_phases), 1)
        self.assertEqual(received_phases[0], LifecyclePhase.CAMPAIGN_GRIND)
        self.assertIn("Test Message", received_texts[0])

    def test_endgame_earning_state_transitions_with_character_lifecycle_state(self):
        """Kiểm thử chu trình EndgameMappingSubState đồng bộ với CharacterLifecycleState (Doc 58)."""
        import time
        from src.assistant_tool.autonomous_lifecycle import EndgameMappingSubState, CharacterLifecycleState

        self.mock_bridge.send_map_device.return_value = True
        self.lifecycle.inventory_scanner.scan_row1_waystones = MagicMock(return_value=[(0, 1731, 822, 70.0)])
        self.lifecycle.max_maps = 2
        self.lifecycle.maps_completed = 0
        self.lifecycle.is_in_town = True
        self.lifecycle.phase = LifecyclePhase.ENDGAME_EARNING
        self.lifecycle.lifecycle_state = CharacterLifecycleState.HIDEOUT_IDLE

        # 1. IDLE -> PRECHECK_INVENTORY
        self.lifecycle._execute_endgame_earning_step()
        self.assertEqual(self.lifecycle.mapping_substate, EndgameMappingSubState.PRECHECK_INVENTORY)

        # 2. PRECHECK_INVENTORY -> SELECT_WAYSTONE
        self.lifecycle._execute_endgame_earning_step()
        self.assertEqual(self.lifecycle.mapping_substate, EndgameMappingSubState.SELECT_WAYSTONE)

        # 3. SELECT_WAYSTONE -> DISPATCH_MAP_DEVICE
        self.lifecycle._execute_endgame_earning_step()
        self.assertEqual(self.lifecycle.mapping_substate, EndgameMappingSubState.DISPATCH_MAP_DEVICE)

        # 4. DISPATCH_MAP_DEVICE -> Gửi Opcode 7 send_map_device -> WAIT_ZONE_ENTER
        self.lifecycle._execute_endgame_earning_step()
        self.assertTrue(self.mock_bridge.send_map_device.called)
        self.assertEqual(self.lifecycle.mapping_substate, EndgameMappingSubState.WAIT_ZONE_ENTER)

        # 4. WAIT_ZONE_ENTER + Telemetry chuyển sang MAP_LOADING_GRACE -> IN_MAP_MONITORING
        snap = MagicMock()
        snap.is_online = True
        snap.area_name = "Sandspit"
        snap.area_code = "Map_Sandspit"
        snap.lifecycle_state = CharacterLifecycleState.MAP_LOADING_GRACE
        self.lifecycle.on_telemetry_snapshot(snap)

        self.lifecycle._execute_endgame_earning_step()
        self.assertEqual(self.lifecycle.mapping_substate, EndgameMappingSubState.IN_MAP_MONITORING)

        # 5. IN_MAP_MONITORING + Telemetry quay về Hideout (HIDEOUT_IDLE) -> MAP_COMPLETED
        self.lifecycle.map_start_time = time.time() - 10.0  # Duration > 5.0s
        snap_return = MagicMock()
        snap_return.is_online = True
        snap_return.area_name = "Shoreline Hideout"
        snap_return.area_code = "HideoutShoreline"
        snap_return.lifecycle_state = CharacterLifecycleState.HIDEOUT_IDLE
        self.lifecycle.on_telemetry_snapshot(snap_return)

        self.lifecycle._execute_endgame_earning_step()
        self.assertEqual(self.lifecycle.mapping_substate, EndgameMappingSubState.MAP_COMPLETED)

        # 6. Thực thi bước MAP_COMPLETED -> maps_completed tăng lên 1
        self.lifecycle._execute_endgame_earning_step()
        self.assertEqual(self.lifecycle.maps_completed, 1)

    def test_in_map_clear_triggers_town_portal_exit(self):
        """Khi ở trong Map và thỏa mãn điều kiện hoàn thành (quái sạch/timeout/core báo), phải tự động gửi lệnh Town Portal (Key . / Opcode 4)."""
        self.mock_bridge.send_cast_emergency_portal = MagicMock(return_value=True)
        self.lifecycle.phase = LifecyclePhase.ENDGAME_EARNING
        self.lifecycle.mapping_substate = EndgameMappingSubState.IN_MAP_MONITORING
        self.lifecycle.map_start_time = time.time() - 200.0  # Duration > 180s timeout
        self.lifecycle.current_farming_map_name = "Creek Tier 15"
        self.lifecycle.is_in_town = False
        self.lifecycle.portal_exit_triggered = False

        self.lifecycle._execute_endgame_earning_step()
        self.assertTrue(self.mock_bridge.send_cast_emergency_portal.called)
        self.assertTrue(self.lifecycle.portal_exit_triggered)

    def test_hideout_map_device_interlock_prevents_opcode7_spam(self):
        """Khi C++ Core đang trong chu trình Map Device (APPROACH/ACTIVATE/PORTAL), không được gửi lại Opcode 7."""
        self.mock_bridge.send_map_device = MagicMock(return_value=True)
        self.lifecycle.phase = LifecyclePhase.ENDGAME_EARNING
        self.lifecycle.mapping_substate = EndgameMappingSubState.DISPATCH_MAP_DEVICE
        self.lifecycle.lifecycle_state = CharacterLifecycleState.HIDEOUT_MAP_DEVICE_APPROACH

        self.lifecycle._execute_endgame_earning_step()
        # Không được spam send_map_device
        self.assertFalse(self.mock_bridge.send_map_device.called)
        # Tự động chuyển sang WAIT_ZONE_ENTER
        self.assertEqual(self.lifecycle.mapping_substate, EndgameMappingSubState.WAIT_ZONE_ENTER)

    def test_get_map_device_distance_from_telemetry(self):
        """Kiểm tra trích xuất khoảng cách Map Device từ danh sách thực thể RAM."""
        snap = MagicMock()
        snap.is_online = True
        snap.entities = [
            {"name": "Map Device", "type": 3, "dist": 14.5},
            {"name": "Zombie", "type": 1, "dist": 30.0},
        ]
        self.mock_bridge.last_snapshot = snap
    def test_hideout_automatically_detected_as_endgame_earning(self):
        """Nhân vật tại Shoreline Hideout kể cả khi player_level=1 phải luôn được đánh giá là ENDGAME_EARNING."""
        self.lifecycle.player_level = 1
        self.lifecycle.current_area_code = "Hideout"
        self.lifecycle.current_area_name = "Shoreline Hideout"

        phase = self.lifecycle.evaluate_phase()
        self.assertEqual(phase, LifecyclePhase.ENDGAME_EARNING)
        self.assertGreaterEqual(self.lifecycle.player_level, 65)

    def test_hideout_prevents_world_map_travel_to_act1(self):
        """Đang ở Hideout thì tuyệt đối cấm phát lệnh send_trigger_world_map_travel tới Act 1."""
        self.lifecycle.player_level = 1
        self.lifecycle.current_area_code = "Hideout"
        self.lifecycle.current_area_name = "Shoreline Hideout"
        self.lifecycle.is_in_town = True
        self.lifecycle.has_real_map_log = True
        self.lifecycle.phase = LifecyclePhase.CAMPAIGN_GRIND

        self.lifecycle._execute_campaign_grind_step()
        self.assertFalse(self.mock_bridge.send_trigger_world_map_travel.called)
        self.assertEqual(self.lifecycle.phase, LifecyclePhase.ENDGAME_EARNING)

    def test_auto_quester_hideout_objective_not_campaign(self):
        """AutoQuester nhận diện Hideout là Endgame Map Device chứ không phải Act 1 The Riverbank."""
        obj = self.lifecycle.quester.resolve_active_objective("Hideout", "Shoreline Hideout", area_level=1)
        self.assertEqual(obj.action_type, "MAP_DEVICE")
        self.assertIn("Map Device", obj.target_name)
        self.assertNotEqual(obj.area_name, "The Riverbank")


if __name__ == "__main__":
    unittest.main()


