"""AutoPOE2 - Unit Tests cho Live Run Sentinel (Mock Harness & Invariant Gate)
Domain: QA & Governance Sentinel (Sư đoàn 4)
Tuân thủ:
- Rule 4: Tự động khóa chặn hồi quy.
- Rule 5: Cấm mock ảo, 100% dữ liệu cấu trúc thực nghiệm.
- Rule 8: Kiểm toán toàn diện Master Invariant Matrix.
"""

import os
import shutil
import tempfile
import time
from unittest.mock import MagicMock, patch

import pytest

from src.assistant_tool.live_sentinel import (
    ERR_CLIENT_LOG_PORTAL_DISABLED,
    ERR_FOCUS_LOST_WHILE_AUTONOMOUS,
    ERR_WATCHDOG_HEARTBEAT_LAG,
    LiveRunSentinel,
    LiveSentinelStatus,
)
from src.agentic_engine.invariant_sentinel import (
    ERR_BOGUS_VITAL_STRUCT_778,
    ERR_CORE_LOG_FATAL_EXCEPTION,
    ERR_WATCHDOG_HEARTBEAT_TIMEOUT,
    ERR_XYZ_OUT_OF_BOUNDS,
)


@pytest.fixture
def temp_env():
    """Tạo thư mục tạm thời phục vụ kiểm thử cô lập."""
    temp_dir = tempfile.mkdtemp(prefix="sentinel_test_")
    log_file = os.path.join(temp_dir, "Client.txt")
    incidents_dir = os.path.join(temp_dir, "incidents")

    with open(log_file, "w", encoding="utf-8") as f:
        f.write("2026/09/18 01:00:00 12345678 [INFO Client 22632] Connected to server.\n")

    yield {
        "temp_dir": temp_dir,
        "log_file": log_file,
        "incidents_dir": incidents_dir,
    }

    shutil.rmtree(temp_dir, ignore_errors=True)


class TestLiveRunSentinelZeroInput:
    """Xác nhận nguyên tắc thụ động tuyệt đối (Zero Input Guarantee)."""

    def test_sentinel_never_calls_win32_input_apis(self, temp_env):
        """Khẳng định LiveRunSentinel không phát bất kỳ phím bấm hay click chuột nào ra màn hình."""
        with patch("ctypes.windll.user32.keybd_event") as mock_keybd:
            with patch("ctypes.windll.user32.mouse_event") as mock_mouse:
                with patch("ctypes.windll.user32.PostMessageW") as mock_post:
                    sentinel = LiveRunSentinel(
                        target_pid=22632,
                        log_path=temp_env["log_file"],
                        incidents_dir=temp_env["incidents_dir"],
                    )
                    # Chạy 10 ticks liên tiếp
                    for _ in range(10):
                        sentinel.process_tick()

                    # Tuyệt đối 0 lệnh phát phím/chuột
                    assert mock_keybd.call_count == 0, "VI PHẠM NGUYÊN TẮC THỤ ĐỘNG: keybd_event bị gọi!"
                    assert mock_mouse.call_count == 0, "VI PHẠM NGUYÊN TẮC THỤ ĐỘNG: mouse_event bị gọi!"
                    assert mock_post.call_count == 0, "VI PHẠM NGUYÊN TẮC THỤ ĐỘNG: PostMessageW bị gọi!"


class TestFocusLossAndAutonomousGuard:
    """Kiểm tra bắt lỗi mất focus khi đang bật chế độ tự hành F8."""

    def test_focus_loss_while_autonomous_triggers_incident(self, temp_env):
        """Khi đang tự hành (F8 ON) mà game mất focus, Sentinel phải báo lỗi và đóng gói dossier."""
        sentinel = LiveRunSentinel(
            target_pid=22632,
            log_path=temp_env["log_file"],
            incidents_dir=temp_env["incidents_dir"],
        )
        sentinel.set_autonomous_mode(True)

        # 1. Game đang Focused -> Không có lỗi
        with patch.object(sentinel, "is_game_window_focused", return_value=True):
            anomalies = sentinel.process_tick()
            assert ERR_FOCUS_LOST_WHILE_AUTONOMOUS not in anomalies
            assert sentinel.incidents_recorded == 0

        # 2. Game bị mất focus (Alt-Tab) -> Lập tức bắt lỗi và đóng gói dossier
        with patch.object(sentinel, "is_game_window_focused", return_value=False):
            anomalies = sentinel.process_tick()
            assert ERR_FOCUS_LOST_WHILE_AUTONOMOUS in anomalies
            assert sentinel.incidents_recorded == 1

            # Xác minh thư mục incident đã được tạo
            incident_folders = os.listdir(temp_env["incidents_dir"])
            assert len(incident_folders) == 1
            assert "INV_F8_01" in incident_folders[0]


class TestClientLogWatcherChannel:
    """Kiểm tra giám sát tệp nhật ký Client.txt."""

    def test_detect_zone_transition(self, temp_env):
        """Kiểm tra nhận diện chuyển cảnh khu vực từ [SCENE] Set Source."""
        sentinel = LiveRunSentinel(
            target_pid=22632,
            log_path=temp_env["log_file"],
            incidents_dir=temp_env["incidents_dir"],
        )

        with open(temp_env["log_file"], "a", encoding="utf-8") as f:
            f.write("2026/09/18 01:10:05 12345678 [SCENE] Set Source [Shoreline Hideout]\n")

        sentinel.process_tick()
        assert sentinel.current_area == "Shoreline Hideout"

    def test_detect_portal_disabled_in_hideout(self, temp_env):
        """Kiểm tra phát hiện lỗi 'Portals are disabled here' (phá bất biến INV-KEY-PORTAL)."""
        sentinel = LiveRunSentinel(
            target_pid=22632,
            log_path=temp_env["log_file"],
            incidents_dir=temp_env["incidents_dir"],
        )

        with open(temp_env["log_file"], "a", encoding="utf-8") as f:
            f.write("2026/09/18 01:12:30 12345678 [INFO Client 22632] : Portals are disabled here.\n")

        anomalies = sentinel.process_tick()
        assert ERR_CLIENT_LOG_PORTAL_DISABLED in anomalies
        assert sentinel.incidents_recorded == 1

    def test_detect_fatal_crash_in_log(self, temp_env):
        """Kiểm tra bắt lỗi [FATAL] hoặc Access Violation và đóng gói hồ sơ sự cố."""
        sentinel = LiveRunSentinel(
            target_pid=22632,
            log_path=temp_env["log_file"],
            incidents_dir=temp_env["incidents_dir"],
        )

        with open(temp_env["log_file"], "a", encoding="utf-8") as f:
            f.write("2026/09/18 01:15:00 12345678 [FATAL] Access Violation (0xC0000005) at 0x7FFF1234\n")

        anomalies = sentinel.process_tick()
        assert any(ERR_CORE_LOG_FATAL_EXCEPTION in a for a in anomalies)
        assert sentinel.incidents_recorded == 1


class TestLiveInvariantsChannel:
    """Kiểm tra giám sát Telemetry sống (HP/ES/XYZ/Watchdog)."""

    def test_vitals_bogus_marker_778_packaged(self, temp_env):
        """Struct 778/778 phải bị bắt và đóng gói incident dossier ngay lập tức."""
        sentinel = LiveRunSentinel(
            target_pid=22632,
            log_path=temp_env["log_file"],
            incidents_dir=temp_env["incidents_dir"],
        )

        telemetry = {
            "current_hp": 778,
            "max_hp": 778,
            "current_es": 0,
            "max_es": 0,
            "pos_x": 100.0,
            "pos_y": 200.0,
            "pos_z": 0.0,
            "timestamp_ms": int(time.time() * 1000),
        }
        sentinel.update_telemetry(telemetry)
        anomalies = sentinel.process_tick()

        assert ERR_BOGUS_VITAL_STRUCT_778 in anomalies
        assert sentinel.incidents_recorded == 1

    def test_xyz_out_of_bounds_packaged(self, temp_env):
        """Tọa độ bay vượt ngưỡng biên bản đồ phải bị bắt lỗi."""
        sentinel = LiveRunSentinel(
            target_pid=22632,
            log_path=temp_env["log_file"],
            incidents_dir=temp_env["incidents_dir"],
        )

        telemetry = {
            "current_hp": 1,
            "max_hp": 1,
            "current_es": 1500,
            "max_es": 1500,
            "pos_x": 999999.0,  # Out of bounds
            "pos_y": 200.0,
            "pos_z": 0.0,
            "timestamp_ms": int(time.time() * 1000),
        }
        sentinel.update_telemetry(telemetry)
        anomalies = sentinel.process_tick()

        assert ERR_XYZ_OUT_OF_BOUNDS in anomalies
        assert sentinel.incidents_recorded == 1

    def test_watchdog_lag_warning_and_timeout(self, temp_env):
        """Đo lường nhịp tim: Lag > 1500ms báo warning (không đóng gói), > 3000ms báo timeout (đóng gói)."""
        sentinel = LiveRunSentinel(
            target_pid=22632,
            log_path=temp_env["log_file"],
            incidents_dir=temp_env["incidents_dir"],
            heartbeat_warning_ms=1500,
            heartbeat_timeout_ms=3000,
        )

        base_now = int(time.time() * 1000)
        sentinel.last_heartbeat_ms = base_now
        sentinel.has_received_heartbeat = True

        # 1. Heartbeat mới 500ms -> Hoàn toàn bình thường
        anomalies1 = sentinel.process_tick(now_ms=base_now + 500)
        assert anomalies1 == []
        assert sentinel.incidents_recorded == 0

        # 2. Heartbeat trễ 1800ms (> 1500ms) -> Báo warning, KHÔNG đóng gói dossier
        anomalies2 = sentinel.process_tick(now_ms=base_now + 1800)
        assert ERR_WATCHDOG_HEARTBEAT_LAG in anomalies2
        assert sentinel.incidents_recorded == 0

        # 3. Heartbeat trễ 3200ms (> 3000ms) -> Báo critical timeout, TỰ ĐỘNG đóng gói dossier
        anomalies3 = sentinel.process_tick(now_ms=base_now + 3200)
        assert ERR_WATCHDOG_HEARTBEAT_TIMEOUT in anomalies3
        assert sentinel.incidents_recorded == 1
