"""AutoPOE2 - Endless Multi-Map Loop Integration Test Suite (Doc 69 / SSoT 2026).
================================================================================
[MÔ PHỎNG THUẬT TOÁN HEADLESS - KHÔNG PHẢI LIVE GAME] (Rule 5 Compliance)

Domain: QA & Governance Sentinel (Sư đoàn 4)
Kiểm thử tích hợp khép kín vòng lặp vô tận (Endless Multi-Map Farming Loop):
Stage 0 (Stash) -> Stage 1 (Waystone) -> Stage 2 (Map Device) -> Stage 3 (Activate)
-> Stage 4 (Enter) -> Stage 5 (Clear 4-Gate) -> Stage 6 (Hideout) -> Lặp lại chu kỳ mới.

Đặc biệt kiểm định và rà soát:
1. INV-MAP-LEGITIMATE-CLEAR: Thẩm định 4 cổng (90s-240s, Boss Slain, Fog 85%, Distance 1200u, Quiet 25s).
   Triệt tiêu hoàn toàn bug "monsters <= 30 sau 15s".
2. INV-FOCUS-SAFETY-SEPARATION: Bảo đảm không rò rỉ bất kỳ input chuột/phím ra desktop.
   KeyDown/MouseDown bị chặn khi unfocused; KeyUp/MouseUp luôn giải phóng phần cứng KMBox;
   Tự động ReleaseAllKeys() khi mất focus.
3. INV-WASD-NO-BLIND-LMB: Tiếp cận Stash và Map Device dùng WASD tiếp cận <= 18u (200px)
   trước khi Click LMB có chủ đích; cấm click mù xuống đất hoặc chân nhân vật (960, 540).
4. Tính toán và xác minh Autonomy Readiness Score (ARS) đạt 100.0%.
"""

from __future__ import annotations

import math
import os
import shutil
import tempfile
import time
from typing import Any, Dict, List, Optional, Tuple
from unittest.mock import MagicMock, call, patch

import pytest

from src.agentic_engine.master_invariants_sentinel import (
    MasterInvariantsSentinel,
    InvariantStatus,
)
from src.assistant_tool.navigation.map_clear_evaluator import MapClearEvaluator
from src.assistant_tool.stash.stash_constants import StashAction, StashConfig
from tools.autonomous_mapping_coordinator import AutonomousMappingCoordinator


@pytest.fixture(autouse=True)
def mock_sync_ram():
    """Tránh quét bộ nhớ thật trong môi trường giả lập headless."""
    with patch("src.assistant_tool.screen_capturer.SyncMemorySnapshotManager"):
        yield


@pytest.fixture
def coordinator_env():
    """Tạo môi trường giả lập cô lập cho AutonomousMappingCoordinator."""
    temp_dir = tempfile.mkdtemp(prefix="endless_loop_test_")
    capture_dir = os.path.join(temp_dir, "captures")
    log_file = os.path.join(temp_dir, "Client.txt")

    with open(log_file, "w", encoding="utf-8") as f:
        f.write("2026/09/19 01:00:00 [INFO Client] You have entered Shoreline Hideout.\n")

    coordinator = AutonomousMappingCoordinator(
        screen_w=2560,
        screen_h=1440,
        capture_dir=capture_dir,
        map_duration_sec=120,
        min_map_duration_sec=90.0,
        client_log_path=log_file,
        is_simulated=True,
    )

    # Cấu hình mock cho an toàn tiền cảnh
    coordinator.ensure_focus = MagicMock(return_value=True)
    coordinator.dismiss_modals = MagicMock()
    coordinator.recover_from_client_crash_if_needed = MagicMock(return_value=True)
    coordinator.wait_for_loading_screen_to_finish = MagicMock(return_value=True)
    coordinator.start_3s_sentinel = MagicMock()
    coordinator.stop_3s_sentinel = MagicMock()

    yield {
        "temp_dir": temp_dir,
        "capture_dir": capture_dir,
        "log_file": log_file,
        "coordinator": coordinator,
    }

    shutil.rmtree(temp_dir, ignore_errors=True)


# =============================================================================
# 1. TEST SUITE: ENDLESS MULTI-MAP CONTINUOUS PIPELINE (STAGES 0 -> 6 -> REPEAT)
# =============================================================================

class TestEndlessMultiMapContinuousLoop:
    """Kiểm thử vòng lặp vô tận đa bản đồ kết nối liên hoàn toàn diện 7 Stages."""

    def test_continuous_multi_map_loop_executes_all_7_stages_per_cycle(self, coordinator_env):
        """[MÔ PHỎNG THUẬT TOÁN HEADLESS - KHÔNG PHẢI LIVE GAME]
        Xác minh vòng lặp 3 bản đồ liên tục thực thi đầy đủ chuỗi:
        Stage 0 (Stash) -> Stage 1 (Waystone) -> Stage 2 (Map Device) -> Stage 3 (Activate)
        -> Stage 4 (Enter) -> Stage 5 (Clear 4-Gate) -> Stage 6 (Hideout) -> Stage 0 Post-Stash
        -> Lặp lại chu kỳ mới cho từng bản đồ.
        """
        coordinator: AutonomousMappingCoordinator = coordinator_env["coordinator"]

        # Giả lập phản hồi cho từng Stage
        coordinator.execute_stage0_stash_routine = MagicMock(return_value={
            "status": "SUCCESS",
            "stash_opened": True,
            "items_deposited": 4,
            "tabs_detected": 4,
            "waystone_withdrawn": False,
        })
        coordinator.execute_stage1_precheck = MagicMock(return_value={
            "status": "SUCCESS",
            "selected_slot": (0, 0, 1731, 822, 100.0),
            "waystones_found": [(0, 0, 1731, 822, 100.0), (1, 0, 1801, 822, 95.0)],
            "occupied_slots": 15,
            "is_full": False,
        })
        coordinator.execute_stage2_map_device = MagicMock(return_value={
            "status": "SUCCESS",
            "portal_already_open": False,
            "approached": True,
        })
        coordinator.execute_stage3_atlas_activation = MagicMock(return_value={
            "status": "SUCCESS",
            "node_id": "map_node_simulated",
        })
        coordinator.execute_stage4_map_entrance = MagicMock(return_value={
            "status": "SUCCESS",
            "entered_zone": "Sulphur Vents",
        })
        coordinator.execute_stage5_map_clear = MagicMock(return_value={
            "status": "SUCCESS",
            "runner_instance": None,
            "runner_result": {
                "status": "COMPLETED",
                "death_detected": False,
                "respawn_count": 0,
                "clear_reason": "Map Boss đã bị tiêu diệt sau 110s",
            },
        })
        coordinator.execute_stage6_portal_return = MagicMock(return_value={
            "status": "SUCCESS",
            "returned_to_hideout": True,
        })

        # Thực thi vòng lặp 3 bản đồ liên tục với bật cất đồ (enable_stashing=True)
        with patch("time.sleep"):
            loop_res = coordinator.run_multi_map_loop(
                max_maps=3,
                enable_stashing=True,
                force_new_map=True,
            )

        # 1. Xác nhận vòng lặp hoàn thành trọn vẹn 3/3 maps
        assert loop_res["status"] == "COMPLETED"
        assert loop_res["total_maps_requested"] == 3
        assert loop_res["maps_completed"] == 3
        assert loop_res["exhausted"] is False
        assert len(loop_res["runs"]) == 3

        # 2. Xác nhận trong mỗi lượt chạy bản đồ, toàn bộ 7 stages đều được thi hành đúng thứ tự
        expected_stages = [
            "STAGE0_STASH",
            "STAGE1_PRECHECK",
            "STAGE2_MAP_DEVICE",
            "STAGE3_ATLAS_ACTIVATION",
            "STAGE4_MAP_ENTRANCE",
            "STAGE5_MAP_CLEAR",
            "STAGE6_PORTAL_RETURN",
            "STAGE0_POST_STASH",
        ]
        for run_info in loop_res["runs"]:
            p_res = run_info["pipeline_result"]
            assert p_res["status"] == "SUCCESS"
            assert p_res["stages_completed"] == expected_stages

        # 3. Xác nhận số lần gọi các Stage:
        # 3 maps * 2 stash (pre + post) = 6 stash calls
        assert coordinator.execute_stage0_stash_routine.call_count == 6
        assert coordinator.execute_stage1_precheck.call_count == 3
        assert coordinator.execute_stage2_map_device.call_count == 3
        assert coordinator.execute_stage3_atlas_activation.call_count == 3
        assert coordinator.execute_stage4_map_entrance.call_count == 3
        assert coordinator.execute_stage5_map_clear.call_count == 3
        assert coordinator.execute_stage6_portal_return.call_count == 3

    def test_fail_closed_at_any_stage_aborts_cycle_and_prevents_damage(self, coordinator_env):
        """[MÔ PHỎNG THUẬT TOÁN HEADLESS - KHÔNG PHẢI LIVE GAME]
        Nguyên tắc Fail-Closed: Nếu Stage 2 (Map Device) thất bại, không kích hoạt Stage 3, 4, 5, 6.
        """
        coordinator: AutonomousMappingCoordinator = coordinator_env["coordinator"]

        coordinator.execute_stage0_stash_routine = MagicMock(return_value={"status": "SUCCESS"})
        coordinator.execute_stage1_precheck = MagicMock(return_value={
            "status": "SUCCESS",
            "selected_slot": (0, 0, 1731, 822, 100.0),
        })
        # Giả lập Stage 2 thất bại (UI chưa mở / timeout)
        coordinator.execute_stage2_map_device = MagicMock(return_value={
            "status": "FAILED",
            "error": "Atlas UI not verified after LMB click (INV-MAPDEVICE-VERIFY-UI)",
        })
        coordinator.execute_stage3_atlas_activation = MagicMock()
        coordinator.execute_stage4_map_entrance = MagicMock()
        coordinator.execute_stage5_map_clear = MagicMock()
        coordinator.execute_stage6_portal_return = MagicMock()

        with patch("time.sleep"):
            report = coordinator.execute_pipeline(target_slot_idx=0, enable_stashing=True)

        assert report["status"] == "FAILED"
        assert "Stage 2 thất bại" in report["error"]
        assert "STAGE2_MAP_DEVICE" not in report["stages_completed"]
        # Các stages sau tuyệt đối không được gọi
        assert coordinator.execute_stage3_atlas_activation.call_count == 0
        assert coordinator.execute_stage4_map_entrance.call_count == 0
        assert coordinator.execute_stage5_map_clear.call_count == 0
        assert coordinator.execute_stage6_portal_return.call_count == 0


# =============================================================================
# 2. TEST SUITE: INV-MAP-LEGITIMATE-CLEAR (4 CỔNG ĐÁNH GIÁ BẢN ĐỒ 90s - 240s)
# =============================================================================

class TestInvMapLegitimateClear4Gates:
    """Kiểm tra toàn diện bất biến INV-MAP-LEGITIMATE-CLEAR với 4 cổng thẩm định."""

    def test_gate_duration_strictly_rejects_early_clear_under_90s(self):
        """Rào chắn thời lượng tối thiểu: CẤM kết luận hoàn tất khi elapsed < 90s (90s - 240s)."""
        evaluator = MapClearEvaluator(min_duration_sec=90.0, target_coverage=0.85, min_traverse_units=1200.0)

        # Cập nhật số quái vật = 0 ở giây thứ 15 (bug cũ "monsters <= 30 sau 15s")
        evaluator.update_telemetry(pos_x=100.0, pos_y=100.0, monsters_near=0, coverage_ratio=0.10, now_sec=15.0)
        is_cleared, reason = evaluator.evaluate_clear(elapsed_sec=15.0, now_sec=15.0)
        assert is_cleared is False
        assert reason == ""

        # Dù boss đã chết nhưng chưa đủ 90s (ví dụ 45s) -> Vẫn từ chối hoàn tất sớm
        evaluator.update_telemetry(pos_x=200.0, pos_y=200.0, monsters_near=0, boss_slain=True, now_sec=45.0)
        is_cleared, reason = evaluator.evaluate_clear(elapsed_sec=45.0, now_sec=45.0)
        assert is_cleared is False
        assert reason == ""

    def test_gate1_boss_slain_qualifies_map_after_90s(self):
        """Cổng 1: Map Boss Slain + elapsed >= 90s + traverse >= 1200u (INV-MAP-LEGITIMATE-CLEAR-V2)."""
        evaluator = MapClearEvaluator(min_duration_sec=90.0, target_coverage=0.80, min_traverse_units=1200.0)

        # Di chuyển tích lũy qua các cụm tọa độ để đạt >= 1200u
        positions = [
            (100.0, 100.0), (350.0, 100.0), (600.0, 100.0),
            (850.0, 100.0), (1100.0, 100.0), (1350.0, 100.0),
        ]
        for px, py in positions:
            evaluator.update_telemetry(pos_x=px, pos_y=py, monsters_near=2)

        evaluator.update_telemetry(pos_x=1400.0, pos_y=100.0, monsters_near=1, boss_slain=True, now_sec=95.0)
        is_cleared, reason = evaluator.evaluate_clear(elapsed_sec=95.0, now_sec=95.0)

        assert is_cleared is True
        assert "Map Boss đã bị tiêu diệt" in reason

    def test_gate2_fog_coverage_reaches_85_percent_after_90s(self):
        """Cổng 2: Minimap Fog Exploration >= 85% + elapsed >= 90s + traverse >= 1200u."""
        evaluator = MapClearEvaluator(min_duration_sec=90.0, target_coverage=0.85, min_traverse_units=1200.0)

        # Di chuyển tích lũy qua các điểm đạt >= 1200u
        positions = [
            (100.0, 100.0), (350.0, 100.0), (600.0, 100.0),
            (850.0, 100.0), (1100.0, 100.0), (1350.0, 100.0),
        ]
        for px, py in positions:
            evaluator.update_telemetry(pos_x=px, pos_y=py, monsters_near=3)

        # 60% sau 95s -> Chưa đạt
        evaluator.update_telemetry(pos_x=1360.0, pos_y=100.0, monsters_near=5, coverage_ratio=0.60, now_sec=95.0)
        is_cleared, _ = evaluator.evaluate_clear(elapsed_sec=95.0, now_sec=95.0)
        assert is_cleared is False

        # 86.5% sau 140s -> Đạt chuẩn cổng 2
        evaluator.update_telemetry(pos_x=1370.0, pos_y=100.0, monsters_near=2, coverage_ratio=0.865, now_sec=140.0)
        is_cleared, reason = evaluator.evaluate_clear(elapsed_sec=140.0, now_sec=140.0)
        assert is_cleared is True
        assert "86.5%" in reason

    def test_gate3_traversed_distance_and_core_signal_after_90s(self):
        """Cổng 3: Quãng đường tuần tra >= 1200u + C++ Core State 9 + Boss Slain."""
        evaluator = MapClearEvaluator(min_duration_sec=90.0, min_traverse_units=1200.0)

        # Di chuyển tích lũy qua các điểm
        positions = [
            (100.0, 100.0), (350.0, 100.0), (600.0, 100.0),
            (850.0, 100.0), (1100.0, 100.0), (1350.0, 100.0),
        ]
        for px, py in positions:
            evaluator.update_telemetry(pos_x=px, pos_y=py, monsters_near=3)

        assert evaluator.total_distance_traversed >= 1200.0

        # Boss bị hạ gục và Core Engine gửi Opcode/State 9
        evaluator.update_telemetry(pos_x=1400.0, pos_y=100.0, monsters_near=0, boss_slain=True)
        is_cleared, reason = evaluator.evaluate_clear(elapsed_sec=105.0, core_lifecycle_state=9)
        assert is_cleared is True
        assert "Map Boss đã bị tiêu diệt" in reason
        assert "State=9" in reason

    def test_gate4_quiet_area_after_extensive_patrol(self):
        """Cổng 4: Quãng đường >= 1200u + 25s yên ắng liên tục + sương mở >= 80%."""
        evaluator = MapClearEvaluator(min_duration_sec=90.0, min_traverse_units=1200.0)

        # Di chuyển tích lũy 1320u (6 bước * 220u)
        for step in range(7):
            evaluator.update_telemetry(pos_x=100.0 + step * 220.0, pos_y=100.0, monsters_near=4)
        assert evaluator.total_distance_traversed >= 1200.0

        # Bắt đầu yên ắng lúc t=100s, sương mở 70% (chưa đạt 80%)
        evaluator.update_telemetry(pos_x=1450.0, pos_y=100.0, monsters_near=0, coverage_ratio=0.70, now_sec=100.0)

        # Tại t=115s (sương 70% < 80%, mới 15s yên ắng < 25s) -> Chưa clear
        is_cleared, _ = evaluator.evaluate_clear(elapsed_sec=115.0, now_sec=115.0)
        assert is_cleared is False

        # Tại t=126s (đã 26s yên ắng >= 25s và sương đạt 82% >= 80%) -> Đạt cổng 4
        evaluator.update_telemetry(pos_x=1460.0, pos_y=100.0, monsters_near=0, coverage_ratio=0.82, now_sec=126.0)
        is_cleared, reason = evaluator.evaluate_clear(elapsed_sec=126.0, now_sec=126.0)
        assert is_cleared is True
        assert "hoàn toàn yên ắng" in reason



# =============================================================================
# 3. TEST SUITE: INV-WASD-NO-BLIND-LMB (STASH & MAP DEVICE APPROACH <= 18u)
# =============================================================================

class TestInvWasdNoBlindLmbProximityInterlock:
    """Kiểm tra bất biến INV-WASD-NO-BLIND-LMB: Tiếp cận Stash và Map Device dùng WASD <= 18u."""

    def test_stash_approach_uses_wasd_when_distance_over_200px(self):
        """StashOrchestrator: Khi nhân vật cách Stash > 200px, bắt buộc dùng WASD tiếp cận trước."""
        from src.assistant_tool.stash.stash_orchestrator import StashOrchestrator
        from PIL import Image

        mock_img = Image.new("RGB", (2560, 1440), color=(20, 20, 20))
        orchestrator = StashOrchestrator(
            screen_w=2560,
            screen_h=1440,
            capture_func=lambda: mock_img,
        )
        orchestrator._ensure_focus = MagicMock(return_value=True)
        orchestrator._click = MagicMock()

        # Mock nhãn Stash ở vị trí xa tâm (1800, 300), khoảng cách tới tâm (1280, 720) là ~660px > 200px
        with patch("src.assistant_tool.stash.stash_orchestrator.find_button_by_text", return_value=(1800, 300)):
            with patch("src.common.win32_input.send_wasd_direction") as mock_wasd:
                with patch.object(orchestrator.ui_scanner, "is_stash_open", side_effect=[False, True]):
                    with patch("time.sleep"):
                        opened = orchestrator.open_stash(img_current=mock_img, timeout_sec=1.0)
                        assert opened is True
                        # Bắt buộc gọi send_wasd_direction tiếp cận trước
                        assert mock_wasd.call_count >= 1
                        # Gọi click LMB mở hòm sau khi tiếp cận
                        assert orchestrator._click.call_count >= 1

    def test_map_device_approach_uses_wasd_when_world_distance_over_18u(self, coordinator_env):
        """MapDevice: Khi nhân vật cách Map Device > 18.0u, bắt buộc dùng WASD tiếp cận <= 18u."""
        coordinator: AutonomousMappingCoordinator = coordinator_env["coordinator"]

        # Mock tọa độ người chơi (100, 100) và Map Device (135, 100) -> khoảng cách 35.0u > 18.0u
        coordinator.get_player_world_pos = MagicMock(return_value=(100.0, 100.0))
        coordinator.find_map_device_world_pos = MagicMock(return_value=(135.0, 100.0))
        coordinator.navigate_towards_with_tangent_sliding = MagicMock(return_value=True)
        coordinator._core_owns_map_device_loop = MagicMock(return_value=False)
        coordinator.is_atlas_ui_open = MagicMock(return_value=True)
        coordinator.find_map_device_screen_pos = MagicMock(return_value=(1200, 600))
        coordinator.hover = MagicMock()
        coordinator.click = MagicMock()
        coordinator.dynamic_camera_zoom = MagicMock()

        with patch("time.sleep"):
            res = coordinator.execute_stage2_map_device()

        assert res["status"] == "SUCCESS"
        assert res["approached"] is True
        # Xác nhận gọi tiếp cận điều hướng bằng WASD
        coordinator.navigate_towards_with_tangent_sliding.assert_called_once_with(
            100.0, 100.0, 135.0, 100.0, interact_radius=18.0, max_duration_sec=8.0
        )

    def test_master_sentinel_audits_wasd_no_blind_lmb_for_both_stash_and_map_device(self):
        """MasterInvariantsSentinel xác nhận cự ly tiếp cận cả Stash và Map Device đều <= 18.0u."""
        sentinel = MasterInvariantsSentinel()

        # Kịch bản 1: Cả hai đều đạt <= 18u -> PASSED
        res_ok = sentinel.audit_inv_wasd_no_blind_lmb(
            approach_dist=12.5,
            stash_approach_dist=14.0,
            map_device_approach_dist=12.5,
            blind_lmb_prevented=True,
        )
        assert res_ok.status == InvariantStatus.PASSED
        assert "Stash=14.0u <= 18u" in res_ok.evidence
        assert "MapDevice=12.5u <= 18u" in res_ok.evidence

        # Kịch bản 2: Stash ở cự ly 22u > 18u -> BLOCKED
        res_fail_stash = sentinel.audit_inv_wasd_no_blind_lmb(
            approach_dist=22.0,
            stash_approach_dist=22.0,
            map_device_approach_dist=12.0,
        )
        assert res_fail_stash.status == InvariantStatus.BLOCKED

        # Kịch bản 3: Map Device ở cự ly 25u > 18u -> BLOCKED
        res_fail_dev = sentinel.audit_inv_wasd_no_blind_lmb(
            approach_dist=12.0,
            stash_approach_dist=12.0,
            map_device_approach_dist=25.0,
        )
        assert res_fail_dev.status == InvariantStatus.BLOCKED


# =============================================================================
# 4. TEST SUITE: INV-FOCUS-SAFETY-SEPARATION (ZERO DESKTOP INPUT LEAK)
# =============================================================================

class TestInvFocusSafetySeparation:
    """Kiểm tra bất biến INV-FOCUS-SAFETY-SEPARATION: Bảo đảm không rò rỉ bất kỳ input chuột/phím ra desktop."""

    def test_hardware_focus_interlock_blocks_keydown_and_allows_keyup_when_unfocused(self):
        """Chỉ chặn KeyDown/MouseDown khi mất tiêu điểm; KeyUp/MouseUp luôn được gửi để chống kẹt phím."""
        sentinel = MasterInvariantsSentinel()

        res = sentinel.audit_inv_focus_safety_separation(
            keydown_blocked_unfocused=True,
            keyup_allowed_unfocused=True,
            release_all_on_unfocus=True,
            two_factor_window_check=True,
        )
        assert res.status == InvariantStatus.PASSED
        assert res.code == "INV-FOCUS-SAFETY-SEPARATION"
        assert "Không rò rỉ bất kỳ KeyDown/MouseDown ra desktop" in res.evidence

    def test_hardware_focus_interlock_blocks_when_safety_criteria_violated(self):
        """Khi vi phạm bất kỳ tiêu chí an toàn nào (ví dụ rò rỉ KeyDown) -> Lập tức BLOCKED."""
        sentinel = MasterInvariantsSentinel()

        # Giả lập rò rỉ KeyDown khi mất focus
        res_leak = sentinel.audit_inv_focus_safety_separation(
            keydown_blocked_unfocused=False,
        )
        assert res_leak.status == InvariantStatus.BLOCKED


# =============================================================================
# 5. TEST SUITE: AUTONOMY READINESS SCORE (ARS) CALCULATION & MATRIX SENTINEL
# =============================================================================

class TestAutonomyReadinessScoreAndMatrixSentinel:
    """Kiểm tra tính toán định lượng Autonomy Readiness Score (ARS) và thẩm định 19 Invariants."""

    def test_autonomy_readiness_score_reaches_100_percent_when_all_invariants_pass(self):
        """Khi toàn bộ 20 Master Invariants đều PASSED -> ARS đạt 100.0% và cleared_for_autonomous=True."""
        sentinel = MasterInvariantsSentinel()
        report = sentinel.audit_all_invariants()

        assert report.is_cleared_for_autonomous is True
        assert report.total_invariants == 20
        assert report.passed_count == 20
        assert report.blocked_count == 0
        assert report.warning_count == 0
        assert report.autonomy_readiness_score == 100.0
        assert "100% VERIFIED" in report.summary

    def test_fail_closed_zero_ars_on_any_blocked_invariant(self):
        """Fail-Closed: Bất kỳ bất biến nào bị BLOCKED (ví dụ phím F bị rò rỉ) -> ARS về 0.0%."""
        sentinel = MasterInvariantsSentinel()

        report = sentinel.audit_all_19_invariants(
            custom_overrides={"INV-KEY-NO-F-FOR-INTERACT": False}
        )
        assert report.is_cleared_for_autonomous is False
        assert report.blocked_count == 1
        assert report.autonomy_readiness_score == 0.0
        assert "BLOCKED" in report.summary

    def test_map_legitimate_clear_audit_standalone(self):
        """Kiểm định phương thức audit_inv_map_legitimate_clear độc lập."""
        sentinel = MasterInvariantsSentinel()

        res_ok = sentinel.audit_inv_map_legitimate_clear(
            min_duration_sec=95.0,
            max_duration_sec=240.0,
            boss_slain=True,
            fog_coverage_ratio=0.88,
            traversed_distance=1350.0,
            early_clear_rejected=True,
        )
        assert res_ok.status == InvariantStatus.PASSED
        assert res_ok.code == "INV-MAP-LEGITIMATE-CLEAR"

        # Dọn sớm < 90s -> BLOCKED
        res_early = sentinel.audit_inv_map_legitimate_clear(
            min_duration_sec=30.0,
        )
        assert res_early.status == InvariantStatus.BLOCKED


# =============================================================================
# 6. TEST SUITE: INV-CI-0MS-BURST-WATCHDOG & REFLEX FALLBACK LADDER
# =============================================================================

class TestInvCi0msBurstWatchdogWithReflexFallbackLadder:
    """Kiểm tra bất biến INV-CI-0MS-BURST-WATCHDOG và Thang phản xạ đa tầng (Reflex Fallback Ladder)."""

    def test_ci_burst_watchdog_triggers_0ms_dodge_when_off_cooldown(self):
        """Khi Shock ES >= 50% và Dodge Roll không hồi chiêu -> Kích hoạt ngay 0ms Dodge Roll (I-frame)."""
        sentinel = MasterInvariantsSentinel()
        res = sentinel.audit_inv_ci_0ms_burst_watchdog(
            shock_ratio=0.55,
            dodge_on_cooldown=False,
            human_latency_bypassed=True,
        )
        assert res.status == InvariantStatus.PASSED
        assert "Bypass hoàn toàn human latency" in res.evidence
        assert "0ms I-frame Dodge Roll thành công" in res.evidence

    def test_ci_burst_watchdog_falls_back_to_guard_when_dodge_on_cooldown(self):
        """Khi Dodge Roll đang hồi chiêu (CD), mạch 0ms chuyển sang Defensive Guard / Logout, không kẹt Spacebar."""
        sentinel = MasterInvariantsSentinel()
        res = sentinel.audit_inv_ci_0ms_burst_watchdog(
            shock_ratio=0.60,
            dodge_on_cooldown=True,
            fallback_guard_triggered=True,
            human_latency_bypassed=True,
        )
        assert res.status == InvariantStatus.PASSED
        assert "Reflex Fallback Ladder" in res.evidence
        assert "không kẹt Spacebar" in res.evidence

    def test_ci_burst_watchdog_blocks_if_no_fallback_when_dodge_on_cooldown(self):
        """Vi phạm: Dodge Roll đang hồi chiêu nhưng không có fallback guard -> BLOCKED."""
        sentinel = MasterInvariantsSentinel()
        res = sentinel.audit_inv_ci_0ms_burst_watchdog(
            shock_ratio=0.60,
            dodge_on_cooldown=True,
            fallback_guard_triggered=False,
        )
        assert res.status == InvariantStatus.BLOCKED
        assert "spam Spacebar vô dụng" in res.evidence


# =============================================================================
# 7. TEST SUITE: INV-DESYNC-DETECT-RESYNC & BODY-BLOCK BREAKOUT
# =============================================================================

class TestInvDesyncDetectResyncWithBodyBlockBranching:
    """Kiểm tra bất biến INV-DESYNC-DETECT-RESYNC và phân nhánh Quái vật vây hãm (Body-Block vs Mesh Snag)."""

    def test_desync_mesh_snag_without_monsters_triggers_3tier_mesh_resync(self):
        """Kẹt địa hình tự do (0 quái vật, V_net < 0.8u/s) -> Kích hoạt 3-Tier Mesh Resync."""
        sentinel = MasterInvariantsSentinel()
        res = sentinel.audit_inv_desync_detect_resync(
            spatial_divergence=18.0,
            movement_dwell_ms=400.0,
            net_velocity=0.1,
            monsters_near_15u=0,
            mesh_resync_triggered=True,
        )
        assert res.status == InvariantStatus.PASSED
        assert "3-Tier Mesh Resync" in res.evidence

    def test_desync_body_block_siege_triggers_aoe_clear_breakout(self):
        """Bị bao vây bởi >= 3 quái (Body-Block) -> Từ chối trượt 90°, kích hoạt AOE Clear phá vây."""
        sentinel = MasterInvariantsSentinel()
        res = sentinel.audit_inv_desync_detect_resync(
            spatial_divergence=0.0,
            movement_dwell_ms=450.0,
            net_velocity=0.1,
            monsters_near_15u=5,
            aoe_breakout_triggered=True,
        )
        assert res.status == InvariantStatus.PASSED
        assert "Từ chối trượt tiếp tuyến 90°" in res.evidence
        assert "Combat AOE Clear / Phasing Skill phá vây" in res.evidence

    def test_desync_body_block_blocks_if_aoe_breakout_fails(self):
        """Vi phạm: Bị quái vây chặt nhưng không kích hoạt AOE phá vây -> BLOCKED."""
        sentinel = MasterInvariantsSentinel()
        res = sentinel.audit_inv_desync_detect_resync(
            spatial_divergence=0.0,
            movement_dwell_ms=450.0,
            net_velocity=0.1,
            monsters_near_15u=4,
            aoe_breakout_triggered=False,
        )
        assert res.status == InvariantStatus.BLOCKED

