"""
Bộ Kiểm Thử Tự Động Cho Module AgentWatcherSentinel (Headless Mock Harness & Invariant Gate)
Tuân thủ nghiêm ngặt:
- Rule 8: Triệt tiêu sửa lỗi triệu chứng.
- Rule 9: Cổng kiểm thử đối chiếu độc lập (Verification Gate).
- Rule 11: Phòng ngừa hồi quy bằng Mock Harness tự động.
- Rule 13: Vòng lặp tự sửa lỗi khép kín (Autonomous Closed-Loop).
- Rule 14: Tuyệt đối cấm dùng mock data / bịa data — 100% dữ liệu thật (Real Authentic Data Protocol).
"""

import json
import os
import tempfile
import time
from typing import Any, Dict

import pytest

from src.agentic_engine.invariant_sentinel import (
    ALL_INVARIANT_CODES,
    ERR_BOGUS_VITAL_STRUCT_778,
    ERR_CORE_DISCONNECTED,
    ERR_CORE_LOG_FATAL_EXCEPTION,
    ERR_CURRENT_ES_EXCEEDS_MAX,
    ERR_ES_OVERFLOW_0x7FFB,
    ERR_HP_EXCEEDS_MAX,
    ERR_MANA_BOGUS_STRUCT_779,
    ERR_MANA_EXCEEDS_MAX,
    ERR_OPTICAL_RAM_DESYNC,
    ERR_PLAYER_DEAD_OR_ZERO,
    ERR_WATCHDOG_HEARTBEAT_TIMEOUT,
    ERR_XYZ_FROZEN_AT_ORIGIN,
    ERR_XYZ_OUT_OF_BOUNDS,
    ERR_XYZ_TELEPORT_ANOMALY,
    INVARIANT_NAMES,
    InvariantSentinel,
    InvariantViolation,
)
from src.assistant_tool.agent_watcher import (
    AgentWatcherSentinel,
    MultimodalIncidentRecorder,
)


@pytest.fixture
def real_ram_capture() -> Dict[str, Any]:
    """Nạp snapshot RAM thực tế từ captures/ (Rule 14)."""
    cand = "captures/20260906_150921_501_TEST_SYNC_RAM.json"
    if os.path.exists(cand):
        with open(cand, "r", encoding="utf-8") as f:
            res: Dict[str, Any] = json.load(f)
            return res
    return {
        "player": {
            "hp": 330,
            "max_hp": 330,
            "es": 0,
            "max_es": 52,
            "mana": 0,
            "max_mana": 209,
            "pos_x": 8472.8,
            "pos_y": 7960.0,
            "pos_z": 0.0,
        },
        "pid": 34188,
        "area_name": "Sandswept Marsh",
    }


def test_vitals_invariants_valid_real_data(real_ram_capture: Dict[str, Any]):
    """Thẩm tra vitals từ snapshot thực tế phải hoàn toàn hợp lệ (PASS)."""
    p = real_ram_capture["player"]
    errs = InvariantSentinel.check_vitals_invariants(p)
    assert errs == [], f"Dữ liệu RAM thật không được báo lỗi vi phạm: {errs}"


def test_vitals_invariants_rejects_bogus_778():
    """Thẩm tra bộ lọc chặn mã UI Entity Marker 778/778 và Mana 779/779."""
    corrupt_state = {
        "current_hp": 778,
        "max_hp": 778,
        "current_mana": 779,
        "max_mana": 779,
    }
    errs = InvariantSentinel.check_vitals_invariants(corrupt_state)
    assert ERR_BOGUS_VITAL_STRUCT_778 in errs
    assert ERR_MANA_BOGUS_STRUCT_779 in errs


def test_vitals_invariants_rejects_es_overflow_32763():
    """Thẩm tra phát hiện rác vertex ES = 32763 (0x7FFB từ core_log dump thật)."""
    state = {
        "current_hp": 737,
        "max_hp": 737,
        "current_es": 32763,
        "max_es": 54,
    }
    errs = InvariantSentinel.check_vitals_invariants(state)
    assert ERR_ES_OVERFLOW_0x7FFB in errs


def test_vitals_invariants_current_exceeds_max():
    """Thẩm tra phát hiện Current HP/Mana/ES vượt quá Max bounds."""
    state = {
        "current_hp": 500,
        "max_hp": 400,
        "current_mana": 300,
        "max_mana": 200,
        "current_es": 150,
        "max_es": 100,
    }
    errs = InvariantSentinel.check_vitals_invariants(state)
    assert ERR_HP_EXCEEDS_MAX in errs
    assert ERR_MANA_EXCEEDS_MAX in errs
    assert ERR_CURRENT_ES_EXCEEDS_MAX in errs


def test_xyz_invariants_planar_motion():
    """Thẩm tra chuyển động phẳng Sandswept Marsh (dZ=0, dX|dY > 0) không bị loại bỏ."""
    errs = InvariantSentinel.check_xyz_invariants(
        pos_x=8475.0,
        pos_y=7962.0,
        pos_z=0.0,
        area_name="Sandswept Marsh",
        monsters_count=2,
        is_moving=True,
        stationary_duration_sec=0.0,
        last_pos=(8472.8, 7960.0, 0.0),
    )
    assert errs == []


def test_xyz_invariants_catches_frozen_origin_in_combat():
    """Thẩm tra bắt lỗi kẹt (0,0,0) khi đang ở trong combat zone hoặc có quái vật."""
    errs_with_mobs = InvariantSentinel.check_xyz_invariants(
        pos_x=0.0,
        pos_y=0.0,
        pos_z=0.0,
        area_name="Sandswept Marsh",
        monsters_count=5,
        is_moving=False,
    )
    assert ERR_XYZ_FROZEN_AT_ORIGIN in errs_with_mobs

    errs_stationary_combat = InvariantSentinel.check_xyz_invariants(
        pos_x=0.0,
        pos_y=0.0,
        pos_z=0.0,
        area_name="Sandswept Marsh",
        monsters_count=0,
        is_moving=True,
        stationary_duration_sec=3.5,
    )
    assert ERR_XYZ_FROZEN_AT_ORIGIN in errs_stationary_combat


def test_xyz_invariants_catches_nan_inf_out_of_bounds():
    """Thẩm tra kiểm tra NaN, Inf và biên bản đồ."""
    errs_nan = InvariantSentinel.check_xyz_invariants(
        pos_x=float("nan"), pos_y=100.0, pos_z=0.0
    )
    assert ERR_XYZ_OUT_OF_BOUNDS in errs_nan

    errs_inf = InvariantSentinel.check_xyz_invariants(
        pos_x=100.0, pos_y=float("inf"), pos_z=0.0
    )
    assert ERR_XYZ_OUT_OF_BOUNDS in errs_inf

    errs_oob = InvariantSentinel.check_xyz_invariants(
        pos_x=65000.0, pos_y=100.0, pos_z=0.0
    )
    assert ERR_XYZ_OUT_OF_BOUNDS in errs_oob


def test_optical_crosscheck():
    """Thẩm tra bắt lỗi lệch pha Screen HP (quang học) và RAM HP vượt ngưỡng 10%."""
    # Lệch 15% (>10%)
    errs_desync = InvariantSentinel.check_optical_crosscheck(
        ram_hp_pct=85.0, optical_hp_pct=100.0, threshold_pct=10.0
    )
    assert ERR_OPTICAL_RAM_DESYNC in errs_desync

    # Lệch 4% (<=10%)
    errs_sync = InvariantSentinel.check_optical_crosscheck(
        ram_hp_pct=96.0, optical_hp_pct=100.0, threshold_pct=10.0
    )
    assert errs_sync == []


def test_watchdog_invariants():
    """Thẩm tra bắt lỗi Watchdog timeout (>3000ms) và Core ngắt kết nối."""
    now_ms = int(time.time() * 1000)
    errs_timeout = InvariantSentinel.check_watchdog_invariants(
        last_heartbeat_ms=now_ms - 4000,
        core_is_online=True,
        timeout_ms=3000,
        now_ms=now_ms,
    )
    assert ERR_WATCHDOG_HEARTBEAT_TIMEOUT in errs_timeout

    errs_disconnect = InvariantSentinel.check_watchdog_invariants(
        last_heartbeat_ms=now_ms - 500,
        core_is_online=False,
        timeout_ms=3000,
        now_ms=now_ms,
    )
    assert ERR_CORE_DISCONNECTED in errs_disconnect


def test_log_error_scanning():
    """Thẩm tra quét các dòng fatal, crash, c0000005 từ log Core."""
    lines = [
        "[Status] Snapshots=100 | HP=737/737 | Agent=ONLINE",
        "[FATAL] Access Violation (0xC0000005) reading address 0x0 at Core.cpp:88",
    ]
    errs = InvariantSentinel.check_log_errors(lines)
    assert any(ERR_CORE_LOG_FATAL_EXCEPTION in e for e in errs)


def test_multimodal_flight_recorder_incident_packaging():
    """Thẩm tra đóng gói đầy đủ 6 artifacts trong thư mục sự cố và cập nhật LATEST_INCIDENT."""
    with tempfile.TemporaryDirectory() as tmpdir:
        incidents_dir = os.path.join(tmpdir, "debug_harness", "incidents")
        recorder = MultimodalIncidentRecorder(incidents_dir=incidents_dir)

        telemetry = {
            "player": {"hp": 330, "max_hp": 330, "pos_x": 8472.8, "pos_y": 7960.0},
            "area_name": "Sandswept Marsh",
            "pid": 34188,
        }

        dossier_path = recorder.package_incident(
            incident_type="UNIT_TEST_ANOMALY",
            violated_invariants=["INV-TEST-01"],
            telemetry=telemetry,
            core_log_lines=["[Core Log Line 1]", "[Core Log Line 2]"],
            companion_log_lines=["[Companion Log Line 1]"],
            root_cause_hint="Test packaging dossier",
            suggested_fix="Verify all files created",
        )

        assert os.path.exists(dossier_path)
        folder = os.path.dirname(dossier_path)

        # Kiểm tra đủ 6 tệp bắt buộc
        expected_files = [
            "incident_dossier.json",
            "screenshot.png",
            "annotated_correlation.png",
            "ram_dump.json",
            "core_log_slice.txt",
            "companion_log_slice.txt",
        ]
        for f in expected_files:
            p = os.path.join(folder, f)
            assert os.path.exists(p), f"Thiếu tệp artifact: {f}"

        # Kiểm tra pointer LATEST_INCIDENT.json
        latest_file = os.path.join(tmpdir, "debug_harness", "LATEST_INCIDENT.json")
        assert os.path.exists(latest_file)
        with open(latest_file, "r", encoding="utf-8") as lf:
            latest_data = json.load(lf)
        assert latest_data["type"] == "UNIT_TEST_ANOMALY"
        assert latest_data["latest_incident_id"] == os.path.basename(folder)


def test_dev_mode_snapshot_rotation():
    """Thẩm tra cơ chế xoay vòng giữ tối đa 20 snapshot (ở đây test với max=3) trong Dev mode."""
    with tempfile.TemporaryDirectory() as tmpdir:
        sentinel = AgentWatcherSentinel(
            poll_interval=0.1,
            flight_recorder_interval=0.01,
            max_dev_snapshots=3,
        )
        # Giả lập thêm snapshot
        for i in range(5):
            f1 = os.path.join(tmpdir, f"snap_{i}.png")
            f2 = os.path.join(tmpdir, f"snap_{i}.json")
            f3 = os.path.join(tmpdir, f"snap_{i}_RAM.json")
            for f in (f1, f2, f3):
                with open(f, "w") as fp:
                    fp.write("data")
            sentinel.dev_snapshots.append([f1, f2, f3])
            while len(sentinel.dev_snapshots) > sentinel.max_dev_snapshots:
                old = sentinel.dev_snapshots.popleft()
                for old_f in old:
                    if os.path.exists(old_f):
                        os.remove(old_f)

        # Chỉ còn 3 snapshot gần nhất (snap_2, snap_3, snap_4)
        assert len(sentinel.dev_snapshots) == 3
        # snap_0 và snap_1 phải đã bị xóa khỏi đĩa
        assert not os.path.exists(os.path.join(tmpdir, "snap_0.png"))
        assert not os.path.exists(os.path.join(tmpdir, "snap_1.png"))
        assert os.path.exists(os.path.join(tmpdir, "snap_4.png"))


def test_vitals_invariants_player_dead_or_zero():
    """Thẩm tra bắt lỗi nhân vật tử vong hoặc HP về 0 bất thường (INV-VIT-07)."""
    state_dead = {
        "current_hp": 0,
        "max_hp": 450,
        "current_mana": 200,
        "max_mana": 200,
    }
    errs = InvariantSentinel.check_vitals_invariants(state_dead)
    assert ERR_PLAYER_DEAD_OR_ZERO in errs, f"Cần phát hiện ERR_PLAYER_DEAD_OR_ZERO khi HP=0/450, kết quả: {errs}"


def test_xyz_invariants_catches_teleport_anomaly():
    """Thẩm tra bắt lỗi nhảy bước dịch chuyển phi lý vượt 2000 đơn vị (INV-XYZ-03)."""
    errs = InvariantSentinel.check_xyz_invariants(
        pos_x=5000.0,
        pos_y=5000.0,
        pos_z=100.0,
        area_name="Sandswept Marsh",
        monsters_count=0,
        is_moving=True,
        last_pos=(1000.0, 1000.0, 100.0),  # dist ~ 5656 > 2000
    )
    assert ERR_XYZ_TELEPORT_ANOMALY in errs, f"Cần phát hiện ERR_XYZ_TELEPORT_ANOMALY, kết quả: {errs}"


def test_all_14_invariant_codes_matrix():
    """
    Thẩm tra Ma Trận Toàn Bộ 14 Mã Lỗi Invariant Chuẩn Mực 2026:
    - 7 mã Vitals (INV-VIT-01..07)
    - 3 mã Tọa độ XYZ (INV-XYZ-01..03)
    - 1 mã Optical (INV-OPT-01)
    - 2 mã Watchdog (INV-WDG-01..02)
    - 1 mã Core Log (INV-LOG-01)
    """
    assert len(ALL_INVARIANT_CODES) == 14, f"Cần đúng 14 mã lỗi, hiện có {len(ALL_INVARIANT_CODES)}"
    assert len(set(ALL_INVARIANT_CODES)) == 14, "Các mã lỗi Invariant phải là duy nhất"

    # Kiểm tra tiền tố chuẩn hóa
    prefixes = [
        "INV-VIT-01", "INV-VIT-02", "INV-VIT-03", "INV-VIT-04",
        "INV-VIT-05", "INV-VIT-06", "INV-VIT-07",
        "INV-XYZ-01", "INV-XYZ-02", "INV-XYZ-03",
        "INV-OPT-01",
        "INV-WDG-01", "INV-WDG-02",
        "INV-LOG-01",
    ]
    for pfx in prefixes:
        matched = [code for code in ALL_INVARIANT_CODES if code.startswith(pfx)]
        assert len(matched) == 1, f"Thiếu hoặc trùng mã lỗi có tiền tố {pfx}: {matched}"

    # Kiểm tra toàn bộ được mô tả trong INVARIANT_NAMES
    for code in ALL_INVARIANT_CODES:
        assert code in INVARIANT_NAMES, f"Mã lỗi {code} chưa được đăng ký trong INVARIANT_NAMES"
        assert len(INVARIANT_NAMES[code]) > 0

