// ==========================================================
// Domain 5: Brain & FSM Unit Tests
// ==========================================================

#include "test_harness.hpp"

#include "common/protocol.hpp"
#include "ipc/shared_memory.hpp"
#include "input/human_curve.hpp"
#include "sensor/log_sensor.hpp"
#include "memory/imemory_reader.hpp"
#include "memory/rpm_reader.hpp"
#include "memory/aob_scanner.hpp"
#include "memory/offsets_store.hpp"
#include "memory/pe_fingerprint.hpp"
#include "memory/offset_registry.hpp"
#include "memory/simulated_reader.hpp"
#include "memory/entity_manager.hpp"
#include "memory/game_layout.hpp"
#include "memory/player_finder.hpp"
#include "memory/pointer_chain_resolver.hpp"
#include "memory/registry_live_label.hpp"
#include "memory/vitals_fingerprint.hpp"
#include "combat/reflex_manager.hpp"
#include "looting/loot_controller.hpp"
#include "combat/combo_manager.hpp"
#include "combat/kiting_engine.hpp"
#include "navigation/movement_controller.hpp"
#include "navigation/quest_navigator.hpp"
#include "navigation/terrain_grid.hpp"
#include "navigation/pathfinder.hpp"
#include "memory/terrain_reader.hpp"
#include "game_session.hpp"
#include "autologin/auto_login.hpp"
#include "navigation/town_quest_engine.hpp"
#include "common/coordinate_transform.hpp"
#include "common/zone_classifier.hpp"
#include "brain/bot_brain.hpp"
#include "brain/area_events.hpp"
#include "brain/inventory_handler.hpp"
#include "combat/skills_table.hpp"
#include "combat/skill_definition.hpp"
#include "combat/skill_engine.hpp"
#include "brain/unstuck_handler.hpp"
#include "brain/map_device_handler.hpp"
#include "brain/character_fsm.hpp"
#include "brain/npc_interaction_handler.hpp"
#include "common/window_utils.hpp"
#include "common/math2d.hpp"
#include "common/string_utils.hpp"
#include "common/time_utils.hpp"
#include "common/toml_parser.hpp"
#include "common/logger.hpp"

#include <filesystem>
#include <atomic>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
#include <thread>
#include <vector>

namespace {

void InstantAtlasLoadForTests(MapDeviceHandler& md) {
    auto coords = md.Coordinates();
    coords.atlasLoadDelayMs = 0;
    coords.requireUiVerification = false;
    md.SetCoordinates(coords);
}

void PutNearbyMapPortal(TelemetryPacket& pkt, uint32_t id = 501) {
    const uint32_t i = pkt.entityCount;
    if (i >= MAX_ENTITIES_PER_SNAPSHOT) return;
    pkt.entities[i] = {};
    pkt.entities[i].id = id;
    pkt.entities[i].type = 3;
    pkt.entities[i].posX = pkt.player.posX + 4.0f;
    pkt.entities[i].posY = pkt.player.posY;
    pkt.entities[i].extraFlags = 0;
    std::snprintf(pkt.entities[i].name, sizeof(pkt.entities[i].name), "Map Portal");
    pkt.entityCount = i + 1;
}

bool DriveMapDeviceTo(MapDeviceHandler& md, const ActionProposal& prop, TelemetryPacket& pkt,
                      KMBoxNet& kmbox, uint64_t& t, MapDeviceState target, int maxTicks = 24) {
    for (int i = 0; i < maxTicks && md.State() != target; ++i) {
        t += 400;
        md.Execute(prop, pkt, kmbox, t);
    }
    return md.State() == target;
}

} // namespace

// ==========================================================
// 15. LootController (Auto Fast-Loot & Item Filter)
// ==========================================================
void TestLootController() {
    std::cout << "[Test 15] LootController - Auto-Loot & Item Filter..." << std::endl;
    KMBoxNet kmbox;
    kmbox.SetIgnoreWindowFocus(true);

    LootConfig cfg;
    cfg.lootRadius = 30.0f;
    cfg.lootIntervalMs = 100;
    cfg.pickGold = true;
    cfg.pickCurrency = true;
    cfg.minEquipmentRarity = 2; // Rare trá»Ÿ lÃªn

    LootController lc(cfg);

    EntityTelemetryData goldItem{};
    goldItem.type = 2;
    std::snprintf(goldItem.name, sizeof(goldItem.name), "%s", "120 Gold");
    CHECK(lc.ShouldLoot(goldItem), "Gold phai duoc nhat");

    EntityTelemetryData divineOrb{};
    divineOrb.type = 2;
    std::snprintf(divineOrb.name, sizeof(divineOrb.name), "%s", "Divine Orb");
    CHECK(lc.ShouldLoot(divineOrb), "Divine Orb phai duoc nhat");

    EntityTelemetryData whiteSword{};
    whiteSword.type = 2;
    whiteSword.rarity = 0;
    std::snprintf(whiteSword.name, sizeof(whiteSword.name), "%s", "Rusted Sword");
    CHECK(!lc.ShouldLoot(whiteSword), "White sword phai bi bo qua khi minEquipmentRarity=2");

    EntityTelemetryData rareBoots{};
    rareBoots.type = 2;
    rareBoots.rarity = 2;
    std::snprintf(rareBoots.name, sizeof(rareBoots.name), "%s", "Dragon Greaves");
    CHECK(lc.ShouldLoot(rareBoots), "Rare boots phai duoc nhat");

    // Kiá»ƒm tra cÃ¡c danh má»¥c váº­t pháº©m Ä‘áº·c thÃ¹ cá»§a POE2
    EntityTelemetryData waystone{};
    waystone.type = 2;
    std::snprintf(waystone.name, sizeof(waystone.name), "%s", "Waystone (Tier 15)");
    CHECK(lc.ShouldLoot(waystone), "Waystone Tier 15 phai duoc nhat");
    CHECK(lc.GetPriority(waystone) == 92, "Priority cua Waystone phai la 92");

    EntityTelemetryData uncutGem{};
    uncutGem.type = 2;
    std::snprintf(uncutGem.name, sizeof(uncutGem.name), "%s", "Uncut Skill Gem (Level 19)");
    CHECK(lc.ShouldLoot(uncutGem), "Uncut Gem phai duoc nhat");
    CHECK(lc.GetPriority(uncutGem) == 88, "Priority cua Uncut Gem phai la 88");

    EntityTelemetryData runeItem{};
    runeItem.type = 2;
    std::snprintf(runeItem.name, sizeof(runeItem.name), "%s", "Sun Rune");
    CHECK(lc.ShouldLoot(runeItem), "Rune phai duoc nhat");
    CHECK(lc.GetPriority(runeItem) == 85, "Priority cua Rune phai la 85");

    EntityTelemetryData soulCore{};
    soulCore.type = 2;
    std::snprintf(soulCore.name, sizeof(soulCore.name), "%s", "Soul Core of Zalatl");
    CHECK(lc.ShouldLoot(soulCore), "Soul Core phai duoc nhat");

    EntityTelemetryData charmItem{};
    charmItem.type = 2;
    std::snprintf(charmItem.name, sizeof(charmItem.name), "%s", "Antidote Charm");
    CHECK(lc.ShouldLoot(charmItem), "Charm phai duoc nhat");

    // Kiểm tra bất biến INV-LOOT-PROXIMITY-22U: Khi túi đầy, từ chối nhặt trang bị cồng kềnh, chỉ nhặt currency/gems/waystones
    lc.SetInventoryFull(true);
    CHECK(lc.IsInventoryFull(), "Co isInventoryFull phai la true");
    CHECK(!lc.ShouldLoot(rareBoots), "Khi tui day phai tu choi Rare Boots (trang bi cong kenh)");
    CHECK(!lc.ShouldLoot(whiteSword), "Khi tui day phai tu choi White Sword (trang bi cong kenh)");
    CHECK(lc.ShouldLoot(divineOrb), "Khi tui day van phai nhat Divine Orb (currency 1-slot)");
    CHECK(lc.ShouldLoot(waystone), "Khi tui day van phai nhat Waystone (1-slot)");
    CHECK(lc.ShouldLoot(uncutGem), "Khi tui day van phai nhat Uncut Gem (1-slot)");
    lc.SetInventoryFull(false);

    // Kiá»ƒm thá»­ mÃ´ phá»ng nháº·t Ä‘á»“ trong snapshot (Ultra-Fast <5ms)
    TelemetryPacket packet{};
    packet.player.currentHP = 100;
    packet.player.maxHP = 100;
    packet.player.posX = 50.0f;
    packet.player.posY = 50.0f;
    packet.entityCount = 2;

    packet.entities[0] = whiteSword;
    packet.entities[0].id = 101;
    packet.entities[0].posX = 55.0f;
    packet.entities[0].posY = 50.0f;

    packet.entities[1] = divineOrb;
    packet.entities[1].id = 102;
    packet.entities[1].posX = 60.0f;
    packet.entities[1].posY = 50.0f;

    // Tick: pháº£i Æ°u tiÃªn nháº·t Divine Orb (id 102) vÃ  bá» qua white sword
    bool looted = lc.Update(packet, kmbox, 1000);
    CHECK(looted, "Update phai thuc hien hanh dong nhat");
    CHECK(lc.State().totalItemsLooted == 1, "So item nhat phai la 1");
    CHECK(lc.State().lastLootedEntityId == 102, "Divine Orb phai duoc nhat truoc");
    CHECK(lc.IsUltraFast(), "Cháº¿ Ä‘á»™ Ultra-Fast pháº£i Ä‘Æ°á»£c báº­t");
    CHECK(lc.LastPickupDurationMs() < 250, "Pickup Smooth+F phai xong duoi 250ms (khong MoveMouse tuong doi)");

    // Tick tiep theo: id 102 da nhat -> khong duoc spam nhat lai
    bool lootedAgain = lc.Update(packet, kmbox, 1200);
    CHECK(!lootedAgain, "Khong duoc nhat lap lai cung mot vat pham");

    std::cout << "  -> Auto-Loot priority (POE2 Waystones/Uncut/Runes/Gold), Ultra-Fast <5ms, De-duplication OK" << std::endl;
}

// ==========================================================
// 17. QuestNavigator (Waypoints, Portals & NPCs)
// ==========================================================
void TestQuestNavigator() {
    std::cout << "[Test 17] QuestNavigator - Waypoints, Portals & NPCs..." << std::endl;
    KMBoxNet kmbox;
    kmbox.SetIgnoreWindowFocus(true);

    QuestNavConfig cfg;
    cfg.enabled = true;
    cfg.interactRadius = 15.0f;
    cfg.stepIntervalMs = 100;

    QuestNavigator qn(cfg);

    TelemetryPacket packet{};
    packet.player.currentHP = 100;
    packet.player.maxHP = 100;
    packet.player.posX = 0.0f;
    packet.player.posY = 0.0f;

    // Waypoint á»Ÿ xa tá»a Ä‘á»™ (50, 0)
    packet.entityCount = 1;
    packet.entities[0].id = 999;
    packet.entities[0].type = 3; // Portal / Waypoint
    std::snprintf(packet.entities[0].name, sizeof(packet.entities[0].name), "%s", "The Riverbank Waypoint");
    packet.entities[0].posX = 50.0f;
    packet.entities[0].posY = 0.0f;

    // Tick 1: á»ž xa -> Tiáº¿p tá»¥c di chuyá»ƒn vá» phÃ­a má»¥c tiÃªu
    CHECK(qn.Update(packet, kmbox, 1000), "Phai di chuyen ve phia muc tieu");
    CHECK(qn.State().targetEntityId == 999, "Target ID phai khop");
    CHECK(qn.State().questsProgressed == 0, "Chua tuong tac");

    // Di chuyá»ƒn ngÆ°á»i chÆ¡i tá»›i gáº§n (cá»± ly 10 <= 15)
    packet.player.posX = 45.0f;
    CHECK(qn.Update(packet, kmbox, 1200), "Phai tuong tac khi toi gan");
    CHECK(qn.State().questsProgressed == 1, "Nhiem vu phai tang tien do sau khi tuong tac");

    // Kiá»ƒm tra Ä‘a cháº¿ Ä‘á»™: MOUSE (Click-to-Move) vs WASD
    packet.player.posX = 0.0f;
    cfg.moveMode = MovementMode::MOUSE;
    QuestNavigator qnMouse(cfg);
    CHECK(qnMouse.Update(packet, kmbox, 2000), "Phai hoat dong o che do Click-to-Move (MOUSE)");

    cfg.moveMode = MovementMode::WASD;
    QuestNavigator qnWasd(cfg);
    CHECK(qnWasd.Update(packet, kmbox, 2200), "Phai hoat dong o che do Ban Phim (WASD)");

    std::cout << "  -> Waypoint navigation and interaction trigger (MOUSE + WASD) OK" << std::endl;
}

// ==========================================================
// 26. AutoLogin State Machine & Disconnect Recovery
// ==========================================================
void TestAutoLogin() {
    std::cout << "[Test 26] Auto-Login State Machine (Disconnect -> Dismiss -> Login -> CharSelect -> Load)..." << std::endl;

    AutoLoginConfig cfg;
    cfg.enabled = true;
    cfg.reconnectDelayMs = 3000;
    cfg.charSlot = 2;
    cfg.password = "";
    cfg.maxRetries = 3;

    AutoLogin al(cfg);
    CHECK(al.GetState() == AutoLoginState::IDLE && !al.IsActive(), "Khoi tao AutoLogin phai o trang thai IDLE");

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    al.OnDisconnect("SocketException", 1000);
    bool disconnOk = (al.GetState() == AutoLoginState::DISCONNECTED_WAITING) && al.IsActive() &&
                     (al.GetLastDisconnectReason() == "SocketException");

    al.Tick(kmbox, 2500);
    bool waitCdOk = (al.GetState() == AutoLoginState::DISCONNECTED_WAITING);

    al.Tick(kmbox, 4001);
    bool dismissOk = (al.GetState() == AutoLoginState::DISMISS_DIALOG);

    al.Tick(kmbox, 4010);
    bool waitLoginOk = (al.GetState() == AutoLoginState::WAIT_LOGIN_SCREEN);

    al.Tick(kmbox, 5520);
    bool submitOk = (al.GetState() == AutoLoginState::SUBMIT_LOGIN);

    al.Tick(kmbox, 5530);
    bool waitCharOk = (al.GetState() == AutoLoginState::WAIT_CHAR_SELECT);

    al.Tick(kmbox, 8540);
    bool selectOk = (al.GetState() == AutoLoginState::SELECT_CHARACTER);

    al.Tick(kmbox, 8550);
    bool waitLoadOk = (al.GetState() == AutoLoginState::WAIT_GAME_LOAD);

    al.OnLoadingFinished("Hideout", 12000);
    bool finishOk = (al.GetState() == AutoLoginState::IDLE) && !al.IsActive();

    CHECK(disconnOk && waitCdOk && dismissOk && waitLoginOk && submitOk && waitCharOk && selectOk && waitLoadOk && finishOk,
          "Auto-Login state transitions sequence failed");

    al.TriggerManualLogin(20000);
    bool triggerOk = (al.GetState() == AutoLoginState::DISMISS_DIALOG);
    al.Reset();
    bool resetOk = (al.GetState() == AutoLoginState::IDLE);
    CHECK(triggerOk && resetOk, "Manual trigger / Reset failed");

    al.SetStateForTest(AutoLoginState::WAIT_GAME_LOAD, 30000);
    al.Tick(kmbox, 65001);
    CHECK(al.GetRetryCount() == 1 && al.GetState() == AutoLoginState::DISMISS_DIALOG,
          "Timeout and retry at WAIT_GAME_LOAD failed");

    std::cout << "  -> Auto-Login State Machine (Disconnect/Dismiss/Login/Select/Load/Retry) OK" << std::endl;
}

void TestTownQuestEngine() {
    std::cout << "[TEST] Town & World Map Quest Engine (Phím U)..." << std::endl;

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    TownQuestConfig cfg{};
    cfg.mapOpenDelayMs = 300;
    cfg.tabSwitchDelayMs = 200;
    cfg.zoneClickDelayMs = 200;
    cfg.maxLoadWaitMs = 15000;

    TownQuestEngine tqe(cfg);
    CHECK(tqe.GetState() == TownQuestState::IDLE && !tqe.IsActive(), "Ban dau TownQuestEngine phai IDLE");

    // 2. Kích hoạt dịch chuyển World Map tới Act 2, Zone #3 (Dreadnought)
    tqe.StartTravel(2, 3, 1000);
    CHECK(tqe.IsActive() && tqe.GetState() == TownQuestState::PRESS_U &&
          tqe.GetTargetAct() == 2 && tqe.GetTargetZoneIndex() == 3,
          "Sau StartTravel phai IsActive o PRESS_U voi Act 2 Zone 3");

    // Duyệt qua toàn bộ FSM chu trình Act 2
    bool act2FlowOk = tqe.Tick(kmbox, 1000) && tqe.GetState() == TownQuestState::WAIT_MAP_OPEN
                   && !tqe.Tick(kmbox, 1150) && tqe.GetState() == TownQuestState::WAIT_MAP_OPEN
                   && (tqe.Tick(kmbox, 1301), tqe.GetState() == TownQuestState::CLICK_ACT_TAB)
                   && (tqe.Tick(kmbox, 1305), tqe.GetState() == TownQuestState::CLICK_ZONE_NODE)
                   && (tqe.Tick(kmbox, 1510), tqe.GetState() == TownQuestState::CONFIRM_TRAVEL)
                   && (tqe.Tick(kmbox, 1715), tqe.GetState() == TownQuestState::WAIT_ZONE_LOAD);
    CHECK(act2FlowOk, "Chu trinh Act 2 FSM tu PRESS_U toi WAIT_ZONE_LOAD phai dung");

    tqe.OnLoadingFinished("Dreadnought", 2500);
    CHECK(tqe.GetState() == TownQuestState::IDLE && !tqe.IsActive(), "OnLoadingFinished phai reset ve IDLE");

    // 3. Reset thủ công
    tqe.StartTravel(1, 0, 5000);
    tqe.Reset();
    CHECK(tqe.GetState() == TownQuestState::IDLE, "Reset thu cong phai dua ve IDLE");

    // 4. Chu trình Act 3 (Sandswept Marsh & Phrey Jungle)
    tqe.StartTravel(3, 0, 10000);
    tqe.Tick(kmbox, 10000);
    tqe.Tick(kmbox, 10305);
    tqe.Tick(kmbox, 10310);
    tqe.Tick(kmbox, 10520);
    tqe.Tick(kmbox, 10730);
    CHECK(tqe.GetTargetAct() == 3 && tqe.GetTargetZoneIndex() == 0 && tqe.GetState() == TownQuestState::WAIT_ZONE_LOAD,
          "Act 3 Sandswept Marsh travel FSM phai toi WAIT_ZONE_LOAD");

    tqe.OnLoadingFinished("Sandswept Marsh", 11500);
    CHECK(tqe.GetState() == TownQuestState::IDLE, "Vao Sandswept Marsh xong phai ve IDLE");

    // Chống spam phím U: đang ở đúng map thì bỏ qua
    tqe.OnLoadingFinished("Sandswept Marsh", 13000);
    tqe.StartTravel(3, 0, 13100);
    CHECK(!tqe.IsActive(), "StartTravel vao dung map hien tai phai bi bo qua");

    // 5. Timeout WAIT_ZONE_LOAD và CloseMapIfOpen
    tqe.StartTravel(2, 1, 20000);
    tqe.Tick(kmbox, 20000);
    CHECK(tqe.IsMapOpen(), "Sau khi bam U, IsMapOpen phai la true");
    tqe.SetStateForTest(TownQuestState::WAIT_ZONE_LOAD, 20500);
    bool waitStillLoading = !tqe.Tick(kmbox, 20500 + 10000);
    bool timeoutOk = tqe.Tick(kmbox, 20500 + 16000) && tqe.GetState() == TownQuestState::IDLE && !tqe.IsMapOpen();
    CHECK(waitStillLoading && timeoutOk, "Timeout WAIT_ZONE_LOAD phai dong map va dua ve IDLE");

    // Reset khi map dang mo
    tqe.StartTravel(1, 0, 40000);
    tqe.Tick(kmbox, 40000);
    tqe.Reset(kmbox);
    CHECK(tqe.GetState() == TownQuestState::IDLE && !tqe.IsMapOpen(), "Reset khi map dang mo phai dong map");

    std::cout << "  -> Town & World Map Quest Engine OK" << std::endl;
}

// ==========================================================
// 67. Kiểm thử BotBrain Priority Arbiter & Invariants (Doc 29 - Phase P0)
// ==========================================================
void TestBotBrainArbiterInvariants() {
    std::cout << "[Test 67] BotBrain Priority Arbiter & Invariants (Doc 29 P0)..." << std::endl;

    class MockHandler : public IActionHandler {
    public:
        MockHandler(const char* name, ActionProposal proposal)
            : m_name(name), m_proposal(proposal), m_executed(false) {}
        const char* Name() const override { return m_name; }
        ActionProposal Propose(const TelemetryPacket&, uint64_t) override { return m_proposal; }
        bool Execute(const ActionProposal&, const TelemetryPacket&, KMBoxNet&, uint64_t) override {
            m_executed = true;
            return true;
        }
        bool WasExecuted() const { return m_executed; }
        void Reset() { m_executed = false; }
    private:
        const char* m_name;
        ActionProposal m_proposal;
        bool m_executed;
    };

    TelemetryPacket pkt{};
    pkt.player.posX = 100.0f;
    pkt.player.posY = 200.0f;

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    // Kich ban A: Combat 7000 > Pickup 6000 > Explore 4000
    {
        BotBrain brain;
        brain.SetLogEnabled(false);

        ActionProposal pCombat{};
        pCombat.kind = BotActionKind::Combat;
        pCombat.priority = bot_priority::COMBAT;
        pCombat.priorityName = "COMBAT";
        pCombat.distance = 80.0f;
        pCombat.entityId = 101;

        ActionProposal pLoot{};
        pLoot.kind = BotActionKind::Pickup;
        pLoot.priority = bot_priority::PICKUP;
        pLoot.priorityName = "PICKUP";
        pLoot.distance = 20.0f;
        pLoot.entityId = 202;

        ActionProposal pExplore{};
        pExplore.kind = BotActionKind::Explore;
        pExplore.priority = bot_priority::EXPLORE;
        pExplore.priorityName = "EXPLORE";
        pExplore.distance = 150.0f;
        pExplore.entityId = 0;

        MockHandler hCombat("Combat", pCombat);
        MockHandler hLoot("Loot", pLoot);
        MockHandler hExplore("Explore", pExplore);

        brain.AddHandler(&hCombat);
        brain.AddHandler(&hLoot);
        brain.AddHandler(&hExplore);

        const bool res = brain.Process(pkt, kmbox, 1000);
        CHECK(res && hCombat.WasExecuted() && !hLoot.WasExecuted() && !hExplore.WasExecuted() &&
              brain.LastChosen().kind == BotActionKind::Combat,
              "BotBrain must select Combat (7000) over Pickup (6000) and Explore (4000)");
    }

    // Kich ban B: Unstuck 8000 thang Combat 7000
    {
        BotBrain brain;
        brain.SetLogEnabled(false);

        ActionProposal pUnstuck{};
        pUnstuck.kind = BotActionKind::Unstuck;
        pUnstuck.priority = bot_priority::UNSTUCK;
        pUnstuck.priorityName = "UNSTUCK";

        ActionProposal pCombat{};
        pCombat.kind = BotActionKind::Combat;
        pCombat.priority = bot_priority::COMBAT;
        pCombat.priorityName = "COMBAT";

        MockHandler hUnstuck("Unstuck", pUnstuck);
        MockHandler hCombat("Combat", pCombat);

        brain.AddHandler(&hUnstuck);
        brain.AddHandler(&hCombat);

        brain.Process(pkt, kmbox, 1000);
        CHECK(hUnstuck.WasExecuted() && !hCombat.WasExecuted() &&
              brain.LastChosen().kind == BotActionKind::Unstuck,
              "Unstuck (8000) must override Combat (7000)");
    }

    // Kich ban C: Priority tie-break chon handler dung truoc
    {
        BotBrain brain;
        brain.SetLogEnabled(false);

        ActionProposal p1{};
        p1.kind = BotActionKind::Explore;
        p1.priority = 4000;
        p1.priorityName = "EX1";

        ActionProposal p2{};
        p2.kind = BotActionKind::Explore;
        p2.priority = 4000;
        p2.priorityName = "EX2";

        MockHandler h1("H1", p1);
        MockHandler h2("H2", p2);

        brain.AddHandler(&h1);
        brain.AddHandler(&h2);

        brain.Process(pkt, kmbox, 1000);
        CHECK(h1.WasExecuted() && !h2.WasExecuted(),
              "Equal priority must preserve registration order");
    }

    // Kich ban D: Fail-closed khi tat ca handler priority == 0
    {
        BotBrain brain;
        brain.SetLogEnabled(false);

        ActionProposal pZero{};
        pZero.kind = BotActionKind::None;
        pZero.priority = 0;
        pZero.priorityName = "NONE";

        MockHandler hZero("Zero", pZero);
        brain.AddHandler(&hZero);

        const bool res = brain.Process(pkt, kmbox, 1000);
        CHECK(!res && !hZero.WasExecuted() && brain.LastChosen().kind == BotActionKind::Idle,
              "All priority 0 proposals must fail-closed (return false)");
    }

    // Kich ban E: INV-BRAIN-02 Reflex ConsumedExclusiveInput interlock
    {
        ReflexConfig rcfg;
        rcfg.flaskEnabled = true;
        rcfg.humanReactionEnabled = false;
        ReflexManager reflexMgr(rcfg);

        bool initFalse = !reflexMgr.ConsumedExclusiveInput();
        reflexMgr.TriggerDodgeRoll(kmbox, 1000, "Test roll");
        bool trueAfterRoll = reflexMgr.ConsumedExclusiveInput();

        reflexMgr.SetConsumedExclusiveInput(false);
        reflexMgr.TriggerLifeFlask(kmbox, 1001, "Test life");
        bool trueAfterFlask = reflexMgr.ConsumedExclusiveInput();
        CHECK(initFalse && trueAfterRoll && trueAfterFlask,
              "INV-BRAIN-02: Reflex ConsumedExclusiveInput interlock phai khoa doc quyen khi roll/flask");
    }

    std::cout << "  -> BotBrain Priority Arbiter & Invariants P0 OK" << std::endl;
}

void TestAreaEventMemoryAndTaxonomyInvariants() {
    std::cout << "[Test 68] Area Event Memory & Entity Taxonomy Invariants (Doc 29 P1)..." << std::endl;

    static_assert(sizeof(EntityTelemetryData) == 88, "INV-IPC-01: EntityTelemetryData phai dung 88 bytes!");
    CHECK(sizeof(EntityTelemetryData) == 88, "sizeof(EntityTelemetryData) == 88 bytes");

    // 1. Quai vat song (maxHP > 0) tuyet doi khong bi reclassify (INV-SAFE-01)
    uint32_t flagsMonster = 0;
    uint16_t tMob = game_layout::ClassifyEntityType("Wild Rhoa", game_layout::EntityTypeMonster, 340, flagsMonster);
    uint32_t flagsEssMob = 0;
    uint16_t tEssMob = game_layout::ClassifyEntityType("Essence Carrier Behemoth", game_layout::EntityTypeMonster, 1200, flagsEssMob);
    CHECK(tMob == game_layout::EntityTypeMonster && (flagsMonster & game_layout::kEntityFlagInteractable) == 0 &&
          tEssMob == game_layout::EntityTypeMonster,
          "Quai vat song Wild Rhoa va Essence Carrier Behemoth khong duoc bi reclassify");

    // 2. Phan loai cac doi tuong the gioi authentic tu ten
    struct TaxonomyTestCase {
        const char* name;
        uint16_t expectedType;
        uint32_t expectedFlag;
    };
    const TaxonomyTestCase kTaxonomyCases[] = {
        { "Essence of Greed", game_layout::EntityTypeEssence, game_layout::kEntityFlagInteractable },
        { "Arcane Shrine", game_layout::EntityTypeShrine, game_layout::kEntityFlagInteractable },
        { "Arcanist's Strongbox", game_layout::EntityTypeStrongbox, game_layout::kEntityFlagInteractable },
        { "Breach", game_layout::EntityTypeBreach, game_layout::kEntityFlagInteractable },
        { "Ritual Altar", game_layout::EntityTypeRitual, game_layout::kEntityFlagInteractable },
        { "Mirror of Delirium", game_layout::EntityTypeDelirium, game_layout::kEntityFlagInteractable },
        { "Large Chest", game_layout::EntityTypeChest, game_layout::kEntityFlagInteractable },
        { "Waypoint", game_layout::EntityTypeMinimapPoi, game_layout::kEntityFlagMinimapDerived }
    };
    bool allTaxonomyOk = true;
    for (const auto& tc : kTaxonomyCases) {
        uint32_t flags = 0;
        uint16_t t = game_layout::ClassifyEntityType(tc.name, game_layout::EntityTypeUnknown, 0, flags);
        if (t != tc.expectedType || (flags & tc.expectedFlag) == 0) {
            allTaxonomyOk = false;
            break;
        }
    }
    CHECK(allTaxonomyOk, "Phan loai cac doi tuong the gioi va gan flag interactable phai chinh xac");

    // 3. Vong doi su kien Candidate -> Committed -> Completed / Failed
    AreaEventManager eventMgr;
    CHECK(!eventMgr.HasCommittedEvent(), "Ban dau khong co event committed");

    TelemetryPacket pkt{};
    pkt.entityCount = 2;
    pkt.entities[0].id = 101;
    pkt.entities[0].type = game_layout::EntityTypeShrine;
    pkt.entities[0].posX = 30.0f;
    pkt.entities[0].posY = 40.0f;
    std::snprintf(pkt.entities[0].name, sizeof(pkt.entities[0].name), "Arcane Shrine");

    pkt.entities[1].id = 102;
    pkt.entities[1].type = game_layout::EntityTypeChest;
    pkt.entities[1].posX = 10.0f;
    pkt.entities[1].posY = 10.0f;
    std::snprintf(pkt.entities[1].name, sizeof(pkt.entities[1].name), "Golden Chest");

    eventMgr.UpdateCandidates(pkt, 1000);
    bool commitOk = eventMgr.CommitEvent(101, 1000) && eventMgr.HasCommittedEvent();
    CHECK(commitOk, "Commit event 101 phai thanh cong va cap nhat HasCommittedEvent");

    // 4. AreaEventHandler: AREA_EVENT (6500) va OPEN_CHEST (5500)
    AreaEventHandler handler(&eventMgr);
    ActionProposal pEv = handler.Propose(pkt, 1000);
    CHECK(pEv.kind == BotActionKind::AreaEvent && pEv.priority == bot_priority::AREA_EVENT && pEv.entityId == 101,
          "Co committed event -> Propose phai la AreaEvent 6500");

    eventMgr.MarkCompleted(101);

    // OPEN_CHEST an toan vs khi co quai song trong safe_range (INV-SAFE-01)
    pkt.entityCount = 1;
    pkt.entities[0].id = 888;
    pkt.entities[0].type = game_layout::EntityTypeChest;
    pkt.entities[0].posX = 20.0f;
    pkt.entities[0].posY = 20.0f;
    ActionProposal pChestSafe = handler.Propose(pkt, 2000);

    pkt.entityCount = 2;
    pkt.entities[1].id = 999;
    pkt.entities[1].type = game_layout::EntityTypeMonster;
    pkt.entities[1].currentHP = 500;
    pkt.entities[1].maxHP = 500;
    pkt.entities[1].extraFlags = 0;
    pkt.entities[1].posX = 25.0f;
    pkt.entities[1].posY = 0.0f;
    ActionProposal pChestDanger = handler.Propose(pkt, 2000);

    CHECK(pChestSafe.kind == BotActionKind::OpenChest && pChestSafe.priority == bot_priority::OPEN_CHEST &&
          pChestDanger.priority == 0,
          "INV-SAFE-01: An toan thi mo ruong (5500), co quai song trong safe_range thi priority = 0");

    // 5. BotBrain Priority Arbiter Integration: Combat (7000) > AreaEvent (6500) > Pickup (6000)
    BotBrain arbiter;
    LootController lootCtrl;
    ComboManager comboMgr;
    QuestNavigator questNav;
    questNav.SetAreaEventManager(&eventMgr);

    arbiter.AddHandler(&comboMgr);
    arbiter.AddHandler(&lootCtrl);
    arbiter.AddHandler(&handler);
    arbiter.AddHandler(&questNav);

    TelemetryPacket arbPkt{};
    arbPkt.player.posX = 0.0f;
    arbPkt.player.posY = 0.0f;
    arbPkt.player.currentHP = 1000;
    arbPkt.player.maxHP = 1000;
    arbPkt.entityCount = 3;

    arbPkt.entities[0].id = 1;
    arbPkt.entities[0].type = game_layout::EntityTypeItemDrop;
    arbPkt.entities[0].posX = 10.0f;
    std::snprintf(arbPkt.entities[0].name, sizeof(arbPkt.entities[0].name), "Divine Orb");

    arbPkt.entities[1].id = 2;
    arbPkt.entities[1].type = game_layout::EntityTypeChest;
    arbPkt.entities[1].posX = 20.0f;
    std::snprintf(arbPkt.entities[1].name, sizeof(arbPkt.entities[1].name), "Golden Chest");

    arbPkt.entities[2].id = 3;
    arbPkt.entities[2].type = game_layout::EntityTypeMonster;
    arbPkt.entities[2].posX = 100.0f;
    arbPkt.entities[2].currentHP = 800;
    arbPkt.entities[2].maxHP = 800;

    AreaEventRecord ev{};
    ev.entityId = 4;
    ev.type = game_layout::EntityTypeShrine;
    ev.x = 40.0f;
    ev.status = AreaEventStatus::Committed;
    eventMgr.AddOrUpdateEvent(ev);

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    arbiter.Process(arbPkt, kmbox, 1000);
    bool combatWins = (arbiter.LastChosen().kind == BotActionKind::Combat);

    arbPkt.entities[2].currentHP = 0;
    arbPkt.entities[2].extraFlags = game_layout::kEntityFlagDead;
    arbiter.Process(arbPkt, kmbox, 1010);
    bool areaEventWins = (arbiter.LastChosen().kind == BotActionKind::AreaEvent);

    eventMgr.MarkCompleted(4);
    arbiter.Process(arbPkt, kmbox, 1020);
    bool pickupWins = (arbiter.LastChosen().kind == BotActionKind::Pickup);

    CHECK(combatWins && areaEventWins && pickupWins,
          "Priority Arbiter: Combat (7000) > AreaEvent (6500) > Pickup (6000)");

    std::cout << "  -> Area Event Memory & Entity Taxonomy Invariants P1 OK" << std::endl;
}

// ==========================================================
// 69. Test Pickit Filter & Hidden Item Rejection Invariants (Doc 29 P2)
//  - Kiểm tra nạp và parse pickup_filter.json không phụ thuộc thư viện thứ 3.
//  - Bất biến User: Đồ bị filter ẩn đi / không nằm trong filter TUYỆT ĐỐI KHÔNG ĐƯỢC NHẶT.
//  - Đồ nằm trong reject_name_substrings có ShouldLoot = false và GetPriority = 0.
//  - Đồ hợp lệ (Divine, Gold, Waystone, Rare >= minRarity) có ShouldLoot = true và Propose = 6000.
//  - Cơ chế tự động Hot-Reload khi file JSON thay đổi mtime.
// ==========================================================
void TestPickitFilterAndHiddenItemRejectionInvariants() {
    std::cout << "[Test 69] Pickit Filter & Hidden Item Rejection Invariants (Doc 29 P2)..." << std::endl;

    LootConfig cfg;
    cfg.lootRadius = 60.0f;
    cfg.safeRange = 50.0f;
    LootController lootCtrl(cfg);

    std::string testFilterPath = "bin/Release/pickup_filter.json";
    if (GetFileAttributesA(testFilterPath.c_str()) == INVALID_FILE_ATTRIBUTES) {
        testFilterPath = "data/pickup_filter.json";
    }

    CHECK(lootCtrl.LoadFilter(testFilterPath), "LoadFilter pickup_filter.json phai thanh cong");
    CHECK(lootCtrl.Filter().exactTypes.size() > 0, "pickup_filter exactTypes khong duoc rong");

    // Test reject substring
    EntityTelemetryData hiddenTrash{};
    hiddenTrash.id = 10;
    hiddenTrash.type = game_layout::EntityTypeItemDrop;
    std::snprintf(hiddenTrash.name, sizeof(hiddenTrash.name), "Cracked Sash");
    CHECK(!lootCtrl.ShouldLoot(hiddenTrash), "Cracked Sash phai bi reject boi filter");

    // Test valid currency
    EntityTelemetryData divineOrb{};
    divineOrb.id = 11;
    divineOrb.type = game_layout::EntityTypeItemDrop;
    std::snprintf(divineOrb.name, sizeof(divineOrb.name), "Divine Orb");
    CHECK(lootCtrl.ShouldLoot(divineOrb), "Divine Orb phai duoc loot boi filter");

    // Test Propose action proposal
    TelemetryPacket pkt{};
    pkt.player.posX = 0.0f; pkt.player.posY = 0.0f;
    pkt.player.currentHP = 1000; pkt.player.maxHP = 1000;
    pkt.entityCount = 1;
    pkt.entities[0] = hiddenTrash;

    ActionProposal propHidden = lootCtrl.Propose(pkt, 1000);
    CHECK(propHidden.priority == 0 && propHidden.entityId == 0,
          "LootController must propose priority 0 when ground item is rejected");

    pkt.entities[0] = divineOrb;
    ActionProposal propDivine = lootCtrl.Propose(pkt, 1000);
    CHECK(propDivine.priority == bot_priority::PICKUP && propDivine.entityId == 11,
          "LootController must propose priority 6000 for Divine Orb");

    std::cout << "  -> Pickit Filter & Hidden Item Rejection Invariants P2 OK" << std::endl;
}

// ==========================================================
// 70. Test InventoryHandler FSM & Town-Only Invariants (Doc 29 P3)
//  - Kiểm tra nạp và căn chỉnh tọa độ data/ui/coordinates.toml (Scale theo resolution).
//  - Kiểm tra chu trình FSM Stash: Idle -> OpenPanels -> SelectTab -> DepositItems -> ClosePanels -> Completed.
//  - Kiểm tra chu trình FSM Vendor: Idle -> ApproachNpc -> OpenVendor -> SellOrSalvage -> ClosePanels -> Completed.
//  - Bất biến INV-TOWN-01: Chỉ chạy ở Town/Hideout an toàn; nếu có quái vật sống xung quanh -> Fail-closed ngay lập tức.
//  - Kiểm tra ngắt timeout: vượt quá timeoutMs -> Failed.
//  - Kiểm tra tích hợp BotBrain Arbiter: STASH (3500) và VENDOR (3400) thắng EXPLORE (1000) và IDLE (1000).
// ==========================================================
void TestInventoryHandlerFsmAndTownOnlyInvariants() {
    std::cout << "[Test 70] InventoryHandler FSM & Town-Only Invariants (Doc 29 P3)..." << std::endl;

    InventoryHandler inv;
    CHECK(inv.LoadCoordinates("data/ui/coordinates.toml"), "Phai nap thanh cong coordinates.toml");
    const auto& coords = inv.Coordinates();
    bool coordsOk = (coords.refWidth == 1920.0f && coords.refHeight == 1080.0f &&
                     coords.invCols == 12 && coords.invRows == 5 &&
                     coords.stashCols == 12 && coords.stashRows == 12);
    CHECK(coordsOk, "Thong so luoi va do phan giai tham chieu Inventory coordinates.toml phai dung");

    // 1. Tính toán tọa độ và scale tỷ lệ
    UiPoint pCell1080 = coords.GetInventoryCell(0, 0, 1920.0f, 1080.0f);
    UiPoint pCell1440 = coords.GetInventoryCell(0, 0, 2560.0f, 1440.0f);
    float scaleX = 2560.0f / 1920.0f;
    UiPoint pTab1 = coords.GetStashTab(1, 1920.0f, 1080.0f);
    CHECK(std::abs(pCell1080.x - (1272.0f + 26.25f)) < 0.1f &&
          std::abs(pCell1440.x - pCell1080.x * scaleX) < 0.5f &&
          pTab1.x == 180.0f + 70.0f,
          "Toa do cell va StashTab scale chinh xac tren 1080p va 1440p");

    // 2. Chu trình FSM Stash
    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);
    TelemetryPacket townPkt{};
    strcpy_s(townPkt.area.areaCode, "town");
    strcpy_s(townPkt.area.areaName, "Clearfell Encampment");
    townPkt.player.posX = 100.0f;
    townPkt.player.posY = 100.0f;
    townPkt.player.currentHP = 1000;
    townPkt.player.maxHP = 1000;

    CommandPacket stashCmd{};
    stashCmd.commandId = 501;
    stashCmd.opCode = MacroOpCode::EXECUTE_STASH_ROUTINE;
    stashCmd.targetEntityId = 1;
    stashCmd.timeoutMs = 10000;

    inv.SetItemsToTransfer(2);
    CHECK(inv.StartStash(stashCmd, 1000) && inv.CurrentStashState() == StashFsmState::OpenPanels && inv.IsBusy(),
          "StartStash phai thanh cong o OpenPanels va IsBusy");

    ActionProposal pStash = inv.Propose(townPkt, 1000);
    CHECK(pStash.kind == BotActionKind::Stash && pStash.priority == bot_priority::STASH,
          "Propose phai la Stash 3500");

    bool stashFlowOk = inv.Execute(pStash, townPkt, kmbox, 1000 + coords.stepDelayMs + 1) && inv.CurrentStashState() == StashFsmState::SelectTab
                    && inv.Execute(pStash, townPkt, kmbox, 1000 + coords.stepDelayMs * 2 + 10) && inv.CurrentStashState() == StashFsmState::DepositItems
                    && inv.Execute(pStash, townPkt, kmbox, 1000 + coords.stepDelayMs * 2 + coords.itemTransferDelayMs + 20) && inv.ItemsTransferredCount() == 1
                    && inv.Execute(pStash, townPkt, kmbox, 1000 + coords.stepDelayMs * 2 + coords.itemTransferDelayMs * 2 + 30) && inv.ItemsTransferredCount() == 2 && inv.CurrentStashState() == StashFsmState::ClosePanels
                    && inv.Execute(pStash, townPkt, kmbox, 1000 + coords.stepDelayMs * 3 + coords.itemTransferDelayMs * 2 + 40) && inv.CurrentStashState() == StashFsmState::Completed;
    CHECK(stashFlowOk && !inv.IsBusy() && inv.Propose(townPkt, 5000).priority == 0,
          "Chu trinh Stash 5 buoc FSM phai hoan tat chinh xac");

    // 3. Chu trình FSM Vendor
    CommandPacket vendorCmd{};
    vendorCmd.commandId = 601;
    vendorCmd.opCode = MacroOpCode::EXECUTE_VENDOR_ROUTINE;
    vendorCmd.timeoutMs = 8000;

    CHECK(inv.StartVendor(vendorCmd, 5000) && inv.CurrentVendorState() == VendorFsmState::ApproachNpc,
          "StartVendor phai bat dau tai ApproachNpc");
    ActionProposal pVendor = inv.Propose(townPkt, 5000);
    CHECK(pVendor.kind == BotActionKind::Vendor && pVendor.priority == bot_priority::VENDOR,
          "Propose phai la Vendor 3400");

    bool vendorFlowOk = inv.Execute(pVendor, townPkt, kmbox, 5000 + coords.stepDelayMs + 1) && inv.CurrentVendorState() == VendorFsmState::OpenVendor
                     && inv.Execute(pVendor, townPkt, kmbox, 5000 + coords.stepDelayMs * 2 + 10) && inv.CurrentVendorState() == VendorFsmState::SellOrSalvage
                     && inv.Execute(pVendor, townPkt, kmbox, 5000 + coords.stepDelayMs * 3 + 20) && inv.CurrentVendorState() == VendorFsmState::ClosePanels
                     && inv.Execute(pVendor, townPkt, kmbox, 5000 + coords.stepDelayMs * 4 + 30) && inv.CurrentVendorState() == VendorFsmState::Completed;
    CHECK(vendorFlowOk, "Chu trinh Vendor 4 buoc FSM phai hoan tat chinh xac");

    // 4. INV-TOWN-01: Phát hiện quái vật sống -> Fail-Closed & Timeout
    inv.StartStash(stashCmd, 10000);
    TelemetryPacket dangerPkt = townPkt;
    dangerPkt.entityCount = 1;
    dangerPkt.entities[0].id = 99;
    dangerPkt.entities[0].type = 1;
    dangerPkt.entities[0].currentHP = 500;
    dangerPkt.entities[0].maxHP = 500;

    ActionProposal pDanger = inv.Propose(dangerPkt, 10050);
    CHECK(pDanger.priority == 0 && inv.CurrentStashState() == StashFsmState::Failed,
          "INV-TOWN-01: Khi co quai song, FSM phai lap tuc chuyen sang Failed");

    inv.StartStash(stashCmd, 20000);
    ActionProposal pTimeout = inv.Propose(townPkt, 20000 + stashCmd.timeoutMs + 500);
    CHECK(pTimeout.priority == 0 && inv.CurrentStashState() == StashFsmState::Failed,
          "Qua han timeout, FSM phai chuyen sang Failed");

    // 5. BotBrain Arbiter: STASH (3500) > EXPLORE (1000) ở Town
    BotBrain arbiter;
    arbiter.AddHandler(&inv);
    QuestNavConfig qCfg;
    qCfg.enabled = true;
    QuestNavigator qNav(qCfg);
    arbiter.AddHandler(&qNav);

    inv.StartStash(stashCmd, 40000);
    CHECK(arbiter.Process(townPkt, kmbox, 40000) && arbiter.LastChosen().kind == BotActionKind::Stash &&
          arbiter.LastChosen().priority == bot_priority::STASH,
          "STASH (3500) phai thang EXPLORE (1000) tai Town");

    std::cout << "  -> InventoryHandler FSM & Town-Only Invariants P3 OK" << std::endl;
}

// ==========================================================
// Test 72: MapDeviceHandler & Closed Farming Loop Invariants (Doc 29 - SPEC P5)
// ==========================================================
void TestMapDeviceHandlerAndClosedLoopInvariants() {
    std::cout << "[Test 72] MapDeviceHandler & Closed Loop Invariants (Doc 29 P5)..." << std::endl;

    MapDeviceHandler mapDevice;
    CHECK(mapDevice.LoadCoordinates("data/ui/coordinates.toml"), "Phai nap duoc coordinates.toml");

    const auto& coords = mapDevice.Coordinates();
    bool coordsOk = (coords.waystoneSlotX == 420.0f && coords.waystoneSlotY == 480.0f &&
                     std::fabs(coords.activateButtonX - 806.25f) < 0.01f &&
                     std::fabs(coords.activateButtonY - 562.5f) < 0.01f &&
                     std::fabs(coords.traverseButtonX - 806.25f) < 0.01f &&
                     std::fabs(coords.defaultAtlasNodeX - 1057.5f) < 0.01f &&
                     coords.atlasLoadDelayMs == 1100 && coords.activateDelayMs == 2500 &&
                     coords.tabletSlotX == 490.0f && coords.tabletSlotY == 480.0f &&
                     coords.waystoneRow == 0 && coords.waystoneColStart == 0 &&
                     coords.waystoneColEnd == 11 && coords.autoScanFirstRow);
    CHECK(coordsOk, "Toa do UI MapDevice (TRAVERSE + node mac dinh) trong coordinates.toml phai dung");

    // 1. Trạng thái khởi đầu
    CHECK(!mapDevice.IsActive() && mapDevice.State() == MapDeviceState::Idle && mapDevice.CurrentWaystoneCol() == 0,
          "Ban dau mapDevice phai Idle (col=0)");

    TelemetryPacket pkt{};
    pkt.player.posX = 100.0f;
    pkt.player.posY = 100.0f;
    pkt.player.currentHP = 1000;
    pkt.player.maxHP = 1000;
    std::snprintf(pkt.area.areaName, sizeof(pkt.area.areaName), "Hideout");
    std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "Hideout1");

    CHECK(mapDevice.Propose(pkt, 1000).priority == 0, "Khi Idle, Propose phai la 0");

    // 2. Bắt đầu chu trình Map Device
    InstantAtlasLoadForTests(mapDevice);
    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);
    CHECK(mapDevice.StartMapDevice(0, 0, false, 1000) && mapDevice.IsActive() &&
          mapDevice.State() == MapDeviceState::ApproachDevice && mapDevice.AutoScanFirstRow(),
          "StartMapDevice phai active o ApproachDevice va bat autoScanFirstRow");
    CHECK(mapDevice.RequiresNodeSelection(), "StartMapDevice(0,0) phai bat node Atlas mac dinh");

    ActionProposal pActive = mapDevice.Propose(pkt, 1000);
    CHECK(pActive.kind == BotActionKind::SwitchArea && pActive.priority == 3200, "Priority Map Device phai la 3200");

    PutNearbyMapPortal(pkt);
    uint64_t t = 1000;
    bool fsmFlowOk = mapDevice.Execute(pActive, pkt, kmbox, 1400) && mapDevice.State() == MapDeviceState::OpenDevice
                  && DriveMapDeviceTo(mapDevice, pActive, pkt, kmbox, t = 1400, MapDeviceState::SelectAtlasNode)
                  && DriveMapDeviceTo(mapDevice, pActive, pkt, kmbox, t, MapDeviceState::PlaceWaystone)
                  && DriveMapDeviceTo(mapDevice, pActive, pkt, kmbox, t, MapDeviceState::Activate)
                  && mapDevice.CurrentWaystoneCol() == 1
                  && DriveMapDeviceTo(mapDevice, pActive, pkt, kmbox, t, MapDeviceState::WaitForPortals)
                  && DriveMapDeviceTo(mapDevice, pActive, pkt, kmbox, t, MapDeviceState::EnterPortal)
                  && DriveMapDeviceTo(mapDevice, pActive, pkt, kmbox, t, MapDeviceState::Completed);
    CHECK(fsmFlowOk && mapDevice.MapsOpenedCount() == 1 && !mapDevice.IsActive(),
          "Chu trinh FSM MapDevice (node + TRAVERSE + portal) phai hoan tat chinh xac");

    // 3b. Chu trình quét tự động hàng 0 (Cols 0 -> 1 -> 2)
    mapDevice.Reset();
    InstantAtlasLoadForTests(mapDevice);
    mapDevice.StartMapDevice(0, 0, false, 5000);
    ActionProposal pRun2 = mapDevice.Propose(pkt, 5000);
    uint64_t t2 = 5000;
    CHECK(DriveMapDeviceTo(mapDevice, pRun2, pkt, kmbox, t2, MapDeviceState::Activate),
          "Lan 2 phai qua PlaceWaystone toi Activate de tang cot quet");
    CHECK(mapDevice.CurrentWaystoneCol() == 2, "Sau lan 2, currentWaystoneCol phai tinh tien len 2");

    // 3c. Chế độ nhắm ô đơn lẻ cụ thể
    mapDevice.Reset();
    InstantAtlasLoadForTests(mapDevice);
    CHECK(mapDevice.StartMapDevice(5, 0, false, 7000) && !mapDevice.AutoScanFirstRow(),
          "Khi chi dinh o [5, 0], autoScanFirstRow phai tat");
    ActionProposal pTarget = mapDevice.Propose(pkt, 7000);
    uint64_t t3 = 7000;
    DriveMapDeviceTo(mapDevice, pTarget, pkt, kmbox, t3, MapDeviceState::Activate);
    CHECK(mapDevice.CurrentWaystoneCol() == 2, "Dung o don le thi currentWaystoneCol giu nguyen");

    // 4. Bất biến INV-TOWN-01: Phát hiện quái vật sống -> Fail-Closed tức thì
    mapDevice.Reset();
    mapDevice.StartMapDevice(0, 0, false, 10000);

    TelemetryPacket monsterPkt = pkt;
    monsterPkt.entityCount = 1;
    monsterPkt.entities[0].id = 99;
    monsterPkt.entities[0].type = 1;
    monsterPkt.entities[0].currentHP = 500;
    monsterPkt.entities[0].maxHP = 500;
    monsterPkt.entities[0].extraFlags = 0;

    ActionProposal pFailMonster = mapDevice.Propose(monsterPkt, 10100);
    CHECK(pFailMonster.priority == 0 && mapDevice.State() == MapDeviceState::Failed && !mapDevice.IsActive(),
          "INV-TOWN-01: Khi co quai song, MapDevice phai Fail-Closed");

    // 5. Timeout 45s + WaitForPortals fail-closed (khong portal, cam fallback 960,400)
    mapDevice.Reset();
    InstantAtlasLoadForTests(mapDevice);
    mapDevice.StartMapDevice(0, 0, false, 20000);
    ActionProposal pTimeout = mapDevice.Propose(pkt, 70000);
    CHECK(pTimeout.priority == 0 && mapDevice.State() == MapDeviceState::Failed,
          "Qua timeout 45000ms phai chuyen sang Failed");

    mapDevice.Reset();
    InstantAtlasLoadForTests(mapDevice);
    mapDevice.StartMapDevice(0, 0, false, 80000);
    mapDevice.ForceState(MapDeviceState::WaitForPortals);
    TelemetryPacket noPortal{};
    noPortal.player = pkt.player;
    ActionProposal pWait = mapDevice.Propose(noPortal, 83000);
    mapDevice.Execute(pWait, noPortal, kmbox, 83000);
    CHECK(mapDevice.State() == MapDeviceState::WaitForPortals,
          "WaitForPortals tai 3000ms phai dang trong cua so Retry Polling 8000ms");

    // Qua 8000ms retry (80000 + 8500 = 88500ms) khong co portal moi Fail-Closed
    mapDevice.Execute(pWait, noPortal, kmbox, 89000);
    CHECK(mapDevice.State() == MapDeviceState::Failed,
          "WaitForPortals khong co portal type=3 qua 8000ms phai Fail-Closed");

    // 6. Kiểm tra cấu hình data/maps/default.ipd và ignored_nodes.json
    CHECK(std::filesystem::exists("data/maps/default.ipd") && std::filesystem::exists("data/maps/ignored_nodes.json"),
          "Tep data/maps/default.ipd va ignored_nodes.json phai ton tai");

    // 7. Tích hợp BotBrain Arbiter: MapDevice (3200) thắng Town Explore (1000)
    BotBrain brain;
    QuestNavConfig qCfg;
    qCfg.enabled = true;
    QuestNavigator qNav(qCfg);
    brain.AddHandler(&mapDevice);
    brain.AddHandler(&qNav);

    mapDevice.Reset();
    mapDevice.StartMapDevice(0, 0, false, 50000);
    CHECK(brain.Process(pkt, kmbox, 50400) && brain.LastChosen().kind == BotActionKind::SwitchArea && brain.LastChosen().priority == 3200,
          "BotBrain Arbiter: MapDevice (3200) thang Town Explore (1000)");

    std::cout << "  -> MapDeviceHandler & Closed Loop Invariants P5 OK" << std::endl;
}

// ==========================================================
// Test 77: NpcRecognition & Dialogue Invariants (Doc 39)
//  - Kiểm tra phân loại NPC Taxonomy: Identifier (The Hooded One), Faustus, Oswald, Alva...
//  - Nạp cấu hình data/ui/coordinates.toml mục [npc_dialogue]
//  - ScalePoint theo độ phân giải màn hình
//  - FSM chu trình IdentifyAll (The Hooded One: 0 gold, 0 scroll)
//  - FSM chu trình TalkAndSkip (tiến độ quest bằng phím Space)
//  - Bất biến INV-NPC-01: Quái vật sống trong 350u lập tức Fail-Closed
//  - Bất biến Timeout: Hết thời gian chuyển Failed
//  - Tích hợp BotBrain Arbiter: NPC_INTERACT (3300) > MAP_DEVICE (3200) > SWITCH_AREA (3000) > EXPLORE (1000)
// ==========================================================
void TestNpcRecognitionAndDialogueInvariants() {
    std::cout << "[Test 77] NpcRecognition & Dialogue Invariants (Doc 39)..." << std::endl;

    // 1. Phân loại NPC Taxonomy & Role (Table-driven verification)
    struct NpcRoleCase { const char* name; NpcRole expected; };
    const NpcRoleCase kRoleCases[] = {
        {"The Hooded One", NpcRole::Identifier},
        {"Hooded One", NpcRole::Identifier},
        {"the hooded one", NpcRole::Identifier},
        {"Faustus, the Financier", NpcRole::CurrencyExchange},
        {"The Financier", NpcRole::CurrencyExchange},
        {"Oswald", NpcRole::QuestGiver},
        {"Renly", NpcRole::QuestGiver},
        {"Una", NpcRole::QuestGiver},
        {"Finn", NpcRole::QuestGiver},
        {"Alva", NpcRole::Master},
        {"Einhar", NpcRole::Master},
        {"Kirac", NpcRole::Master},
        {"Doryani", NpcRole::Master},
        {"Town Guard", NpcRole::Generic},
        {nullptr, NpcRole::Generic},
        {"", NpcRole::Generic}
    };
    bool allRolesOk = true;
    for (const auto& tc : kRoleCases) {
        if (ClassifyNpcRole(tc.name) != tc.expected) {
            allRolesOk = false;
            break;
        }
    }
    CHECK(allRolesOk, "Tat ca cac role NPC taxonomy phai duoc phan loai chinh xac");

    // 2. Nạp cấu hình toml và kiểm tra tọa độ
    NpcInteractionHandler npcHandler;
    CHECK(npcHandler.LoadCoordinates("data/ui/coordinates.toml"), "Phai nap thanh cong data/ui/coordinates.toml");

    const auto& coords = npcHandler.Coordinates();
    bool coordsOk = (coords.dialogueMenuX == 960.0f && coords.dialogueMenuY == 420.0f &&
                     coords.identifyAllX == 960.0f && coords.identifyAllY == 456.0f &&
                     coords.dialogueOptionHeight == 36.0f);
    CHECK(coordsOk, "Toa do UI NPC dialogue menu phai dung voi coordinates.toml");

    auto p1080 = coords.ScalePoint(coords.identifyAllX, coords.identifyAllY, 1920.0f, 1080.0f);
    auto p1440 = coords.ScalePoint(coords.dialogueMenuX, coords.dialogueMenuY, 2560.0f, 1440.0f);
    CHECK(p1080.x == 960.0f && p1080.y == 456.0f &&
          std::fabs(p1440.x - 1280.0f) < 0.1f && std::fabs(p1440.y - 560.0f) < 0.1f,
          "ScalePoint cho 1080p va 1440p phai dung ti le");

    // 3. Trạng thái khởi đầu
    CHECK(!npcHandler.IsActive() && npcHandler.State() == NpcInteractState::Idle && npcHandler.InteractionsCompletedCount() == 0,
          "Ban dau NPC handler phai o trang thai Idle (count=0)");

    TelemetryPacket pkt{};
    pkt.player.posX = 100.0f;
    pkt.player.posY = 100.0f;
    pkt.player.currentHP = 1000;
    pkt.player.maxHP = 1000;
    std::snprintf(pkt.area.areaName, sizeof(pkt.area.areaName), "Clearfell Encampment");
    std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "Town1");

    CHECK(npcHandler.Propose(pkt, 1000).priority == 0, "Khi Idle, Propose phai la 0");

    // 4. Chu trình Identify All (The Hooded One)
    pkt.entityCount = 1;
    pkt.entities[0].id = 42;
    pkt.entities[0].type = 4; // EntityTypeNPC
    std::snprintf(pkt.entities[0].name, sizeof(pkt.entities[0].name), "The Hooded One");
    pkt.entities[0].posX = 120.0f;
    pkt.entities[0].posY = 100.0f;

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    CHECK(npcHandler.StartInteract(42, NpcInteractMode::IdentifyAll, 1, 1000), "StartInteract IdentifyAll");
    CHECK(npcHandler.IsActive() && npcHandler.State() == NpcInteractState::ApproachNpc && npcHandler.TargetNpcId() == 42,
          "Sau khi StartInteract phai active o ApproachNpc voi TargetNpcId=42");

    ActionProposal prop = npcHandler.Propose(pkt, 1000);
    CHECK(prop.kind == BotActionKind::NpcInteract && prop.priority == bot_priority::NPC_INTERACT,
          "Proposal phai la NpcInteract voi priority 3300");

    // Thực thi tuần tự 4 bước FSM IdentifyAll
    bool identifyFlowOk = npcHandler.Execute(prop, pkt, kmbox, 1400) && npcHandler.State() == NpcInteractState::OpenDialogue
                       && npcHandler.Execute(prop, pkt, kmbox, 1800) && npcHandler.State() == NpcInteractState::SelectOption
                       && npcHandler.Execute(prop, pkt, kmbox, 2200) && npcHandler.State() == NpcInteractState::CloseDialogue
                       && npcHandler.Execute(prop, pkt, kmbox, 2600) && npcHandler.State() == NpcInteractState::Completed;
    CHECK(identifyFlowOk && npcHandler.InteractionsCompletedCount() == 1 && !npcHandler.IsActive(),
          "Chu trinh IdentifyAll 4 buoc FSM phai hoan tat chinh xac");

    // 5. Chu trình TalkAndSkip (Quest progression)
    npcHandler.Reset();
    CHECK(npcHandler.StartInteract(42, NpcInteractMode::TalkAndSkip, 0, 3000), "StartInteract TalkAndSkip");

    prop = npcHandler.Propose(pkt, 3000);
    bool talkFlowOk = npcHandler.Execute(prop, pkt, kmbox, 3400)
                   && npcHandler.Execute(prop, pkt, kmbox, 3800)
                   && npcHandler.Execute(prop, pkt, kmbox, 4200)
                   && npcHandler.Execute(prop, pkt, kmbox, 4600)
                   && npcHandler.State() == NpcInteractState::Completed;
    CHECK(talkFlowOk && npcHandler.InteractionsCompletedCount() == 2,
          "Chu trinh TalkAndSkip FSM phai hoan tat (completed=2)");

    // 6. Bất biến INV-NPC-01: Quái vật sống trong 350u -> Fail-Closed tức thì
    npcHandler.Reset();
    npcHandler.StartInteract(42, NpcInteractMode::IdentifyAll, 1, 5000);

    TelemetryPacket monsterPkt = pkt;
    monsterPkt.entityCount = 2;
    monsterPkt.entities[1].id = 999;
    monsterPkt.entities[1].type = 1; // Monster
    monsterPkt.entities[1].currentHP = 500;
    monsterPkt.entities[1].maxHP = 500;
    monsterPkt.entities[1].extraFlags = 0; // Sống
    monsterPkt.entities[1].posX = 200.0f; // cự ly dx=100, dy=0 < 350u
    monsterPkt.entities[1].posY = 100.0f;

    ActionProposal pFailMonster = npcHandler.Propose(monsterPkt, 5100);
    CHECK(pFailMonster.priority == 0 && npcHandler.State() == NpcInteractState::Failed && !npcHandler.IsActive(),
          "INV-NPC-01: Khi co quai song gan < 350u, FSM phai Failed ngay lap tuc");

    // 7. Bất biến Timeout
    npcHandler.Reset();
    npcHandler.StartInteract(42, NpcInteractMode::IdentifyAll, 1, 6000);
    ActionProposal pTimeout = npcHandler.Propose(pkt, 6000 + 20000); // 20s > 15s timeout
    CHECK(pTimeout.priority == 0 && npcHandler.State() == NpcInteractState::Failed,
          "Qua han timeout 15s, FSM phai Failed");

    // 8. Tích hợp BotBrain Arbiter: NPC_INTERACT (3300) > MAP_DEVICE (3200) > EXPLORE (1000)
    BotBrain brain;
    brain.AddHandler(&npcHandler);

    MapDeviceHandler mapDevice;
    brain.AddHandler(&mapDevice);

    QuestNavConfig qCfg;
    qCfg.enabled = true;
    QuestNavigator qNav(qCfg);
    brain.AddHandler(&qNav);

    npcHandler.Reset();
    mapDevice.Reset();
    npcHandler.StartInteract(42, NpcInteractMode::IdentifyAll, 1, 10000);
    mapDevice.StartMapDevice(0, 0, false, 10000);

    CHECK(brain.Process(pkt, kmbox, 10000) && brain.LastChosen().kind == BotActionKind::NpcInteract && brain.LastChosen().priority == bot_priority::NPC_INTERACT,
          "NPC_INTERACT (3300) phai thang MAP_DEVICE (3200)");

    npcHandler.Reset();
    CHECK(brain.Process(pkt, kmbox, 10500) && brain.LastChosen().kind == BotActionKind::SwitchArea && brain.LastChosen().priority == bot_priority::MAP_DEVICE,
          "Sau khi NPC xong, MAP_DEVICE (3200) phai tiep quan");

    mapDevice.Reset();
    CHECK(!brain.Process(pkt, kmbox, 11000) && brain.LastChosen().kind == BotActionKind::Idle,
          "Sau khi MapDevice xong tai Town, phai ve IDLE (INV-FSM-TOWN-NO-EXPLORE)");

    std::cout << "  -> NpcRecognition & Dialogue Invariants (Test 77) OK" << std::endl;
}

// ==========================================================
// Test 78: MapDevice Precise Targeting & Closed Loop (Doc 29/39)
//  - Kiểm tra lọc thực thể chính xác (loại bỏ Portal / NPC lạc, ưu tiên "Map Device", "Device", "Atlas")
//  - Kiểm tra nhận diện Portal khi hoàn tất kích hoạt
//  - Kiểm tra kích hoạt qua CommandPacket MacroOpCode::EXECUTE_MAP_DEVICE_ROUTINE
//  - Kiểm tra kích hoạt NPC qua CommandPacket MacroOpCode::EXECUTE_NPC_INTERACT_ROUTINE
// ==========================================================
void TestMapDevicePreciseTargetingAndClosedLoop() {
    std::cout << "[Test 78] MapDevice Precise Targeting & Closed Loop (Doc 29/39)..." << std::endl;

    MapDeviceHandler mapDevice;
    CHECK(mapDevice.LoadCoordinates("data/ui/coordinates.toml"), "Nap toa do MapDevice");
    InstantAtlasLoadForTests(mapDevice);

    TelemetryPacket pkt{};
    pkt.player.posX = 100.0f;
    pkt.player.posY = 100.0f;
    pkt.player.currentHP = 1000;
    pkt.player.maxHP = 1000;
    std::snprintf(pkt.area.areaName, sizeof(pkt.area.areaName), "Hideout");
    std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "Hideout1");

    pkt.entityCount = 4;
    pkt.entities[0].id = 100;
    pkt.entities[0].type = 4;
    std::snprintf(pkt.entities[0].name, sizeof(pkt.entities[0].name), "The Hooded One");
    pkt.entities[1].id = 200;
    pkt.entities[1].type = 3;
    std::snprintf(pkt.entities[1].name, sizeof(pkt.entities[1].name), "Town Portal to Kingsmarch");
    pkt.entities[2].id = 300;
    pkt.entities[2].type = 4;
    std::snprintf(pkt.entities[2].name, sizeof(pkt.entities[2].name), "Map Device");
    pkt.entities[3].id = 400;
    pkt.entities[3].type = 3;
    std::snprintf(pkt.entities[3].name, sizeof(pkt.entities[3].name), "Map Portal - Dunes");
    pkt.entities[3].posX = 104.0f;
    pkt.entities[3].posY = 100.0f;

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    mapDevice.Reset();
    InstantAtlasLoadForTests(mapDevice);
    mapDevice.StartMapDevice(0, 0, false, 1000);
    ActionProposal prop = mapDevice.Propose(pkt, 1000);

    uint64_t t78 = 1000;
    bool fsmOk = (prop.priority == 3200) &&
                 DriveMapDeviceTo(mapDevice, prop, pkt, kmbox, t78, MapDeviceState::EnterPortal);
    CHECK(fsmOk, "MapDevice FSM progression OpenDevice -> EnterPortal failed");

    uint32_t prevCount = mapDevice.MapsOpenedCount();
    CHECK(DriveMapDeviceTo(mapDevice, prop, pkt, kmbox, t78, MapDeviceState::Completed) &&
          (mapDevice.MapsOpenedCount() == prevCount + 1),
          "MapDevice EnterPortal -> Completed failed");

    CommandPacket mapCmd{};
    mapCmd.commandId = 888;
    mapCmd.opCode = MacroOpCode::EXECUTE_MAP_DEVICE_ROUTINE;
    mapCmd.timeoutMs = 18000;
    mapDevice.Reset();
    bool started = mapDevice.StartMapDevice(mapCmd, 5000);
    ActionProposal cmdProp = mapDevice.Propose(pkt, 5000);
    CHECK(started && mapDevice.IsActive() && (cmdProp.priority == 3200),
          "StartMapDevice via CommandPacket failed");

    NpcInteractionHandler npcHandler;
    CommandPacket npcCmd{};
    npcCmd.commandId = 889;
    npcCmd.opCode = MacroOpCode::EXECUTE_NPC_INTERACT_ROUTINE;
    npcCmd.targetEntityId = 100;
    npcCmd.targetX = static_cast<float>(NpcInteractMode::IdentifyAll);
    npcCmd.targetY = 1.0f;
    npcCmd.timeoutMs = 12000;
    CHECK(npcHandler.StartInteract(npcCmd, 6000) && npcHandler.IsActive() &&
          npcHandler.TargetNpcId() == 100 && npcHandler.Mode() == NpcInteractMode::IdentifyAll,
          "NpcInteractionHandler via CommandPacket failed");

    std::cout << "  -> MapDevice Precise Targeting & Closed Loop (Test 78) OK" << std::endl;
}

// ==========================================================
// Test 88: Atlas Node Selector & Map Device 10-Step FSM (Doc 56)
// ==========================================================
void TestAtlasNodeSelectorAndMapDeviceFsm() {
    std::cout << "[Test 88] Atlas Node Selector & Map Device 10-Step FSM (Doc 56)..." << std::endl;

    MapDeviceHandler mapDevice;
    CHECK(mapDevice.LoadCoordinates("data/ui/coordinates.toml"), "Phai nap toa do atlas_ui thanh cong");
    InstantAtlasLoadForTests(mapDevice);

    KMBoxNet kmbox;
    kmbox.SetIgnoreWindowFocus(true);
    kmbox.EnableEmulationMode(true);

    TelemetryPacket pkt{};
    pkt.player.posX = 100.0f;
    pkt.player.posY = 100.0f;
    pkt.entityCount = 0; // Hideout khong co quai

    // 1. Kiem tra khoi tao & dat muc tieu Atlas Node
    mapDevice.SetAtlasNodeTarget(1018.0f, 542.0f, ATLAS_FLAG_REQUIRE_HOVER_VERIFY | ATLAS_FLAG_SOCKET_PRECURSOR_TOWER, 42);
    CHECK(mapDevice.RequiresNodeSelection(), "RequiresNodeSelection phai la true");
    CHECK(mapDevice.TargetNodeX() == 1018.0f && mapDevice.TargetNodeY() == 542.0f, "TargetNode toa do phai khop");

    // 2. Chuyen dong FSM tu Idle -> ApproachDevice -> OpenDevice -> SelectAtlasNode -> InspectPrecursorTowers -> PlaceWaystone -> PlaceTablet -> Activate -> WaitForPortals -> EnterPortal -> Completed
    mapDevice.StartMapDevice(0, 0, true, 1000);
    CHECK(mapDevice.State() == MapDeviceState::ApproachDevice, "State ban dau phai la ApproachDevice");

    // ApproachDevice -> OpenDevice
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 1400), pkt, kmbox, 1400), "Advance sang OpenDevice");
    CHECK(mapDevice.State() == MapDeviceState::OpenDevice, "Phai o OpenDevice");

    // OpenDevice -> SelectAtlasNode (vi requiresNodeSelection == true)
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 1800), pkt, kmbox, 1800), "Advance sang SelectAtlasNode");
    CHECK(mapDevice.State() == MapDeviceState::SelectAtlasNode, "Phai chuyen sang SelectAtlasNode");

    // SelectAtlasNode (Giai đoạn 1: Di chuột hover, non-blocking 120Hz)
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 2200), pkt, kmbox, 2200), "SelectAtlasNode hover move");
    CHECK(mapDevice.State() == MapDeviceState::SelectAtlasNode, "Sau hover move van phai o SelectAtlasNode");

    // SelectAtlasNode (Giai đoạn 2: Cho hover delay < 120ms phai return false, khong freeze thread)
    CHECK(!mapDevice.Execute(mapDevice.Propose(pkt, 2250), pkt, kmbox, 2250), "Hover delay chua het phai return false phi phong toa");

    // SelectAtlasNode (Giai đoạn 3: Sau khi du delay >= 120ms -> Click va chuyen sang InspectPrecursorTowers)
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 2350), pkt, kmbox, 2350), "Advance sang InspectPrecursorTowers sau hover");
    CHECK(mapDevice.State() == MapDeviceState::InspectPrecursorTowers, "Phai chuyen sang InspectPrecursorTowers");

    // InspectPrecursorTowers -> PlaceWaystone
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 2750), pkt, kmbox, 2750), "Advance sang PlaceWaystone");
    CHECK(mapDevice.State() == MapDeviceState::PlaceWaystone, "Phai chuyen sang PlaceWaystone");

    // PlaceWaystone -> PlaceTablet (vi useTablet == true)
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 3150), pkt, kmbox, 3150), "Advance sang PlaceTablet");
    CHECK(mapDevice.State() == MapDeviceState::PlaceTablet, "Phai chuyen sang PlaceTablet");

    // PlaceTablet -> Activate
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 3550), pkt, kmbox, 3550), "Advance sang Activate");
    CHECK(mapDevice.State() == MapDeviceState::Activate, "Phai chuyen sang Activate");

    // Activate -> WaitForPortals
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 3950), pkt, kmbox, 3950), "Advance sang WaitForPortals");
    CHECK(mapDevice.State() == MapDeviceState::WaitForPortals, "Phai chuyen sang WaitForPortals");

    // WaitForPortals -> EnterPortal (sau activateDelayMs)
    PutNearbyMapPortal(pkt);
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 6500), pkt, kmbox, 6500), "Advance sang EnterPortal");
    CHECK(mapDevice.State() == MapDeviceState::EnterPortal, "Phai chuyen sang EnterPortal");

    // EnterPortal: Space đóng panel rồi F
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 6900), pkt, kmbox, 6900), "EnterPortal Space close panels");
    CHECK(mapDevice.State() == MapDeviceState::EnterPortal, "Sau Space van o EnterPortal");
    CHECK(mapDevice.Execute(mapDevice.Propose(pkt, 7300), pkt, kmbox, 7300), "Advance sang Completed");
    CHECK(mapDevice.State() == MapDeviceState::Completed, "Phai chuyen sang Completed");
    CHECK(mapDevice.MapsOpenedCount() >= 1, "MapsOpenedCount phai tang");

    // 3. Kiem tra nhan lenh SELECT_ATLAS_NODE sau khi da Completed (khong bi ket FSM / nuot lenh)
    CommandPacket nextCmd{};
    nextCmd.commandId = 902;
    nextCmd.opCode = MacroOpCode::SELECT_ATLAS_NODE;
    nextCmd.targetX = 1000.0f;
    nextCmd.targetY = 500.0f;
    nextCmd.targetZ = ATLAS_FLAG_REQUIRE_HOVER_VERIFY;
    nextCmd.targetEntityId = 100;
    mapDevice.SelectAtlasNode(nextCmd, 7500);
    CHECK(mapDevice.State() == MapDeviceState::ApproachDevice, "SelectAtlasNode sau Completed phai chuyen sang ApproachDevice");
    CHECK(mapDevice.IsActive(), "MapDevice phai Active sau khi nhan lenh SelectAtlasNode moi");

    // 4. Bat bien INV-ATLAS-01: An toan tai Hideout, chan dung quai vat
    TelemetryPacket monsterPkt = pkt;
    monsterPkt.entityCount = 1;
    monsterPkt.entities[0].type = 1; // Monster
    monsterPkt.entities[0].maxHP = 500;
    monsterPkt.entities[0].extraFlags = 0; // Alive

    mapDevice.Reset();
    mapDevice.StartMapDevice(0, 0, false, 10000);
    ActionProposal pFail = mapDevice.Propose(monsterPkt, 10100);
    CHECK(pFail.priority == 0 && mapDevice.State() == MapDeviceState::Failed,
          "INV-ATLAS-01: Phat hien quai vat quanh Map Device phai Fail-Closed");

    std::cout << "  -> Atlas Node Selector & Map Device 10-Step FSM OK" << std::endl;
}

// ==========================================================
// Test 90: Map Device Dynamic Camera Zoom-in & 3D Projection (INV-NAV-TERRAIN-COMMERCIAL)
// ==========================================================
void TestMapDeviceDynamicZoomAnd3DProjectionInvariants() {
    std::cout << "[Test 90] Map Device Dynamic Camera Zoom-in & 3D Projection (INV-NAV-TERRAIN-COMMERCIAL)..." << std::endl;

    MapDeviceHandler mapDevice;
    CHECK(mapDevice.LoadCoordinates("data/ui/coordinates.toml"), "Nap coordinates.toml thanh cong");
    InstantAtlasLoadForTests(mapDevice);

    KMBoxNet kmbox;
    kmbox.SetIgnoreWindowFocus(true);
    kmbox.EnableEmulationMode(true);

    // 1. Kiểm tra khởi tạo biến zoom & 3D target
    CHECK(!mapDevice.IsZoomedIn(), "Ban dau camera khong duoc o trang thai ZoomedIn");
    CHECK(!mapDevice.HasDeviceWorldPos(), "Ban dau chua co toa do 3D");

    // Đặt tọa độ Map Device ở xa: (300, 100), trong khi Player ở (100, 100) -> Dist = 200 > interactRadius (18.0)
    mapDevice.SetDeviceWorldPos(300.0f, 100.0f, 10.0f);
    CHECK(mapDevice.HasDeviceWorldPos(), "Phai xac nhan da co toa do 3D");

    TelemetryPacket pkt{};
    pkt.player.posX = 100.0f;
    pkt.player.posY = 100.0f;
    pkt.entityCount = 1;
    pkt.entities[0].id = 777;
    pkt.entities[0].type = 3;
    pkt.entities[0].posX = 300.0f;
    pkt.entities[0].posY = 100.0f;
    pkt.entities[0].posZ = 10.0f;
    std::snprintf(pkt.entities[0].name, sizeof(pkt.entities[0].name), "%s", "Map Device");

    mapDevice.StartMapDevice(0, 0, false, 1000);
    CHECK(mapDevice.State() == MapDeviceState::ApproachDevice, "State phai la ApproachDevice");

    // Di chuyển khi ở xa: Không được nhảy ngay sang OpenDevice mà phải di chuyển tiếp cận!
    mapDevice.Execute(mapDevice.Propose(pkt, 1400), pkt, kmbox, 1400);
    CHECK(mapDevice.State() == MapDeviceState::ApproachDevice, "Khoang cach xa (200 > 18) phai o ApproachDevice");

    // Đưa nhân vật tới gần (dist = 10.0 <= 18.0)
    pkt.player.posX = 290.0f;
    pkt.player.posY = 100.0f;
    mapDevice.Execute(mapDevice.Propose(pkt, 1800), pkt, kmbox, 1800);
    CHECK(mapDevice.State() == MapDeviceState::OpenDevice, "Khi tiep can trong interactRadius (10 <= 18), phai chuyen OpenDevice");

    // 2. OpenDevice: Kích hoạt Dynamic Camera Zoom-in (+480 wheel), chiếu 3D sang WorldToScreen & Click mở + gửi 'F'
    CHECK(!mapDevice.IsZoomedIn(), "Truoc OpenDevice chua zoom-in");
    mapDevice.Execute(mapDevice.Propose(pkt, 2200), pkt, kmbox, 2200);
    CHECK(mapDevice.IsZoomedIn(), "Trong OpenDevice phai kich hoat Dynamic Camera Zoom-in (+480 wheel)");
    CHECK(mapDevice.State() == MapDeviceState::SelectAtlasNode, "Sau khi gui 'F' (atlas delay=0) chuyen sang SelectAtlasNode");

    // 4. Kiểm tra phục hồi zoom (Restore Default Zoom) khi Reset() hoặc EnterPortal
    mapDevice.Reset();
    CHECK(!mapDevice.IsZoomedIn(), "Reset() phai khoi phuc camera zoom ve mac dinh");

    // 5. Kiểm tra KMBoxNet Wheel method
    CHECK(kmbox.Wheel(120), "KMBoxNet::Wheel phai tra ve true o che do emulation");
    CHECK(kmbox.Wheel(-120), "KMBoxNet::Wheel phai tra ve true khi cuon nguoc");

    std::cout << "  -> Map Device Dynamic Camera Zoom-in & 3D Projection (INV-NAV-TERRAIN-COMMERCIAL) OK" << std::endl;
}

void TestCharacterFSMAndHideoutAutonomousLoopInvariants() {
    std::cout << "[Test 91] CharacterFSM & Hideout Autonomous Loop Invariants..." << std::endl;

    CharacterFSM fsm;
    MapDeviceHandler mapDevice;
    mapDevice.LoadCoordinates("data/ui/coordinates.toml");
    fsm.SetMapDeviceHandler(&mapDevice);

    QuestNavConfig qCfg;
    qCfg.enabled = true;
    QuestNavigator qNav(qCfg);
    qNav.SetCharacterFSM(&fsm);

    UnstuckConfig uCfg;
    UnstuckHandler unstuck(uCfg);
    unstuck.SetCharacterFSM(&fsm);

    BotBrain brain;
    brain.AddHandler(&unstuck);
    brain.AddHandler(&mapDevice);
    brain.AddHandler(&qNav);

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    TelemetryPacket hideoutPkt{};
    hideoutPkt.player.posX = 100.0f;
    hideoutPkt.player.posY = 100.0f;
    hideoutPkt.player.currentHP = 1000;
    hideoutPkt.player.maxHP = 1000;
    std::snprintf(hideoutPkt.area.areaName, sizeof(hideoutPkt.area.areaName), "Hideout");
    std::snprintf(hideoutPkt.area.areaCode, sizeof(hideoutPkt.area.areaCode), "Hideout1");
    hideoutPkt.entityCount = 1;
    hideoutPkt.entities[0].type = 3;
    hideoutPkt.entities[0].posX = 105.0f;
    hideoutPkt.entities[0].posY = 100.0f;
    std::snprintf(hideoutPkt.entities[0].name, sizeof(hideoutPkt.entities[0].name), "%s", "Map Device");

    // 1. INV-FSM-TOWN-NO-EXPLORE: Khi F8 chưa bật (Autonomous = false), ở HideoutIdle, QuestNav và Unstuck phải có priority = 0
    fsm.SetAutonomousEnabled(false);
    fsm.Update(hideoutPkt, 1000);
    CHECK(fsm.State() == CharacterState::HideoutIdle, "Phai o HideoutIdle");
    CHECK(qNav.Propose(hideoutPkt, 1000).priority == 0, "INV-FSM-TOWN-NO-EXPLORE: QuestNav phai Propose=0 o HideoutIdle");
    CHECK(unstuck.Propose(hideoutPkt, 1000).priority == 0, "INV-FSM-UNSTUCK-NO-TOWN-ROLL: Unstuck phai Propose=0 o HideoutIdle");
    CHECK(!brain.Process(hideoutPkt, kmbox, 1000) && brain.LastChosen().kind == BotActionKind::Idle,
          "BotBrain khong duoc thuc thi di chuyen trong Hideout khi idle");

    // 2. INV-FSM-HIDEOUT-F8-MAP-LOOP: Bật F8 (Autonomous = true) -> tự động tiếp cận Map Device (Priority 3200) thay vì Explore (1000)
    fsm.SetAutonomousEnabled(true);
    fsm.Update(hideoutPkt, 1100);
    CHECK(fsm.State() == CharacterState::HideoutMapDeviceApproach,
          "Bat F8 tai Hideout phai chuyen sang HideoutMapDeviceApproach");
    CHECK(mapDevice.IsActive() && mapDevice.State() == MapDeviceState::ApproachDevice,
          "MapDeviceHandler phai duoc kich hoat tu dong");

    bool brainOk = brain.Process(hideoutPkt, kmbox, 1500);
    CHECK(brainOk && brain.LastChosen().kind == BotActionKind::SwitchArea &&
          brain.LastChosen().priority == bot_priority::MAP_DEVICE,
          "MapDeviceHandler (3200) phai chien thang Arbiter thay vi Explore (1000)");

    // 3. Chuyển vào Combat Map (Seed thay đổi) -> chuyển tiếp InMapExploring
    TelemetryPacket mapPkt = hideoutPkt;
    mapPkt.area.areaSeed = 99887766;
    mapPkt.entityCount = 0; // Không có portal hay NPC chuyển cảnh trong bãi quái
    std::snprintf(mapPkt.area.areaName, sizeof(mapPkt.area.areaName), "Creek");
    std::snprintf(mapPkt.area.areaCode, sizeof(mapPkt.area.areaCode), "MapCreek");
    fsm.Update(mapPkt, 5000);
    CHECK(fsm.State() == CharacterState::InMapExploring, "Vao Map moi phai chuyen sang InMapExploring");
    CHECK(fsm.CanExplore(), "Trong Map chien dau thi CanExplore phai true");
    CHECK(qNav.Propose(mapPkt, 5000).priority == bot_priority::EXPLORE,
          "Trong Map chien dau, QuestNav duoc phep de xuat Explore (4000)");

    // 4. Invariant 1: Unstuck attempts exhausted in Town/Hideout -> STRICTLY FORBID emergency portal key
    unstuck.Reset();
    unstuck.ManualTriggerStuck(UnstuckReason::MovementStuck, 6000);
    ActionProposal unstuckProp{};
    unstuckProp.kind = BotActionKind::Unstuck;
    unstuckProp.priority = bot_priority::UNSTUCK;
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6100);
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6500);
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6510);
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6520);
    CHECK(unstuck.RecoveryAttempts() == 3, "Unstuck da dat max recovery attempts (3)");
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6870);
    fsm.ForceState(CharacterState::HideoutIdle, 6880);
    bool exhaustedResult = unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6880);
    CHECK(!exhaustedResult, "O Hideout/Town, Unstuck exhausted phai return false (CAM mo portal)");
    CHECK(!unstuck.IsStuck(), "Unstuck phai duoc Reset() sau khi exhausted o Hideout");

    // Kiểm tra thêm trường hợp m_isTown = true trực tiếp
    unstuck.Reset();
    unstuck.SetCharacterFSM(nullptr);
    unstuck.SetIsTown(true);
    unstuck.ManualTriggerStuck(UnstuckReason::MovementStuck, 6500);
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6600);
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6950);
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6960);
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 6970);
    CHECK(unstuck.RecoveryAttempts() == 3, "Town recovery attempts phai dat 3 truoc khi exhausted");
    unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 7320);
    bool townExhaustedResult = unstuck.Execute(unstuckProp, hideoutPkt, kmbox, 7330);
    CHECK(!townExhaustedResult, "Khi m_isTown = true, Unstuck exhausted phai return false");
    CHECK(!unstuck.IsStuck(), "Unstuck phai duoc Reset() khi exhausted o Town");
    unstuck.SetCharacterFSM(&fsm);
    unstuck.SetIsTown(false);

    // 5. Invariant 2: MapDevice failure cooldown (3000ms) in CharacterFSM
    fsm.ForceState(CharacterState::HideoutMapDeviceApproach, 7000);
    mapDevice.ForceState(MapDeviceState::Failed);
    fsm.Update(hideoutPkt, 7100);
    CHECK(fsm.State() == CharacterState::HideoutIdle, "Khi MapDevice Failed, FSM phai ve HideoutIdle");
    CHECK(fsm.LastMapDeviceFailMs() == 7100, "FSM phai ghi nhan m_lastMapDeviceFailMs = 7100");

    // Trong thời gian cooldown (< 3000ms, ví dụ 7100 + 1500 = 8600ms), FSM không được kích hoạt MapDevice
    fsm.Update(hideoutPkt, 8600);
    CHECK(fsm.State() == CharacterState::HideoutIdle, "Trong thoi gian cooldown 3000ms, FSM phai o HideoutIdle");
    CHECK(!fsm.TriggerMapDeviceRoutine(8600), "TriggerMapDeviceRoutine phai return false khi con cooldown");

    // Sau khi hết cooldown (7100 + 3100 = 10200ms), FSM được phép trigger lại MapDevice
    mapDevice.Reset();
    fsm.Update(hideoutPkt, 10200);
    CHECK(fsm.State() == CharacterState::HideoutMapDeviceApproach, "Het cooldown 3000ms, FSM duoc trigger lai MapDevice");

    // 6. Invariant 3: Shoreline Hideout fallback coordinates (878, 602) instead of player feet (960, 540)
    MapDeviceHandler mdFallback;
    mdFallback.LoadCoordinates("data/ui/coordinates.toml");
    mdFallback.StartMapDevice(0, 0, false, 20000);
    mdFallback.ForceState(MapDeviceState::OpenDevice);
    CHECK(!mdFallback.HasDeviceWorldPos(), "Ban dau chua co 3D world pos");
    ActionProposal mdProp = mdFallback.Propose(hideoutPkt, 20400);
    mdFallback.Execute(mdProp, hideoutPkt, kmbox, 20400);

    int actW = GetSystemMetrics(SM_CXSCREEN);
    int actH = GetSystemMetrics(SM_CYSCREEN);
    float sW = (actW > 0) ? static_cast<float>(actW) : 1920.0f;
    float sH = (actH > 0) ? static_cast<float>(actH) : 1080.0f;
    const float rawFallbackX = mdFallback.Coordinates().shorelineFallbackX;
    const float rawFallbackY = mdFallback.Coordinates().shorelineFallbackY;
    auto expPt = mdFallback.Coordinates().ScalePoint(rawFallbackX, rawFallbackY, sW, sH);

    CHECK(std::fabs(mdFallback.LastTargetScreenX() - expPt.x) < 1.0f,
          "Fallback X phai la Shoreline Hideout coords scaled");
    CHECK(std::fabs(mdFallback.LastTargetScreenY() - expPt.y) < 1.0f,
          "Fallback Y phai la Shoreline Hideout coords scaled");
    CHECK(std::fabs(mdFallback.LastTargetScreenX() - (sW * 0.5f)) > 30.0f,
          "Fallback click TUYET DOI KHONG duoc roi vao tam man hinh (chan nhan vat)");

    std::cout << "  -> CharacterFSM & Hideout Autonomous Loop Invariants (Test 91) OK" << std::endl;
}

// ==========================================================
// 92. Autonomy Gap Resolution (GAP-1 to GAP-6 Verification)
// ==========================================================
void TestAutonomyGapResolution() {
    std::cout << "[Test 92] Autonomy Gap Resolution & Invariants Verification (GAP-1 to GAP-6)..." << std::endl;

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    // 1. GAP-1: MapDeviceHandler dùng MoveMouseSmooth và phím F (0x09 HID), không bấm phím U
    {
        MapDeviceHandler md;
        md.LoadCoordinates("data/ui/coordinates.toml");
        InstantAtlasLoadForTests(md);
        TelemetryPacket pkt{};
        pkt.player.posX = 100.0f;
        pkt.player.posY = 100.0f;
        pkt.player.currentHP = 1000;
        pkt.player.maxHP = 1000;
        std::snprintf(pkt.area.areaName, sizeof(pkt.area.areaName), "Hideout");
        pkt.entityCount = 1;
        pkt.entities[0].type = 3;
        pkt.entities[0].posX = 108.0f;
        pkt.entities[0].posY = 100.0f;
        std::snprintf(pkt.entities[0].name, sizeof(pkt.entities[0].name), "%s", "Map Device");

        md.StartMapDevice(0, 0, false, 1000);
        md.ForceState(MapDeviceState::OpenDevice);
        ActionProposal pOpen = md.Propose(pkt, 1400);
        CHECK(pOpen.kind == BotActionKind::SwitchArea, "GAP-1: MapDevice OpenDevice phai tao proposal");
        bool execOk = md.Execute(pOpen, pkt, kmbox, 1400);
        CHECK(execOk, "GAP-1: Execute OpenDevice phai thanh cong bang click LMB len Map Device");
        CHECK(md.InteractFSent(), "GAP-1: Phai ghi nhan da tuong tac Map Device");
        CHECK(md.State() == MapDeviceState::SelectAtlasNode, "GAP-1: Sau click LMB + node mac dinh phai sang SelectAtlasNode");
    }

    // 2. GAP-2: QuestNavigator Hold-Until-Waypoint & WASD Dwell >= 300ms
    {
        QuestNavConfig qCfg;
        qCfg.enabled = true;
        qCfg.moveMode = MovementMode::WASD;
        QuestNavigator qNav(qCfg);

        // Ban đầu không có phím nào được giữ
        CHECK(qNav.CurrentlyHeldKeys().empty(), "GAP-2: Ban dau QuestNav khong giu phim");

        // Gửi hướng di chuyển sang phải (dx > 0) tại t=1000ms
        qNav.SendDirectionForTest(100.0f, 0.0f, kmbox, 1000);
        CHECK(!qNav.CurrentlyHeldKeys().empty(), "GAP-2: Phai giu phim sau SendDirection");
        CHECK(std::find(qNav.CurrentlyHeldKeys().begin(), qNav.CurrentlyHeldKeys().end(), 0x44) != qNav.CurrentlyHeldKeys().end(),
              "GAP-2: Huong phai phai giu phim 'D' (0x44)");

        // Cố gắng đổi hướng sang trái (dx < 0) sau 150ms (< 300ms dwell time)
        // Bất biến INV-WASD-MIN-DWELL bắt buộc từ chối đổi hướng để triệt tiêu micro-jitter!
        qNav.SendDirectionForTest(-100.0f, 0.0f, kmbox, 1150);
        CHECK(std::find(qNav.CurrentlyHeldKeys().begin(), qNav.CurrentlyHeldKeys().end(), 0x44) != qNav.CurrentlyHeldKeys().end(),
              "GAP-2: Khi chua du 300ms dwell, bat bien INV-WASD-MIN-DWELL duy tri phim cu 'D'");

        // Đổi hướng sau 350ms (>= 300ms dwell time) -> Cho phép chuyển sang 'A' (0x41)
        qNav.SendDirectionForTest(-100.0f, 0.0f, kmbox, 1350);
        CHECK(std::find(qNav.CurrentlyHeldKeys().begin(), qNav.CurrentlyHeldKeys().end(), 0x41) != qNav.CurrentlyHeldKeys().end(),
              "GAP-2: Sau khi vuot nguong 300ms dwell, chuyen sang phim 'A' (0x41) thanh cong");

        // StopMovement giải phóng và xóa phím đang giữ
        qNav.StopMovement(kmbox);
        CHECK(qNav.CurrentlyHeldKeys().empty(), "GAP-2: StopMovement phai giai phong phim WASD");
    }

    // 3. GAP-3: TerrainGrid RecenterIfNeeded resample từ NativeTerrainData & MapDeviceHandler SetTerrainGrid
    {
        navigation::TerrainGrid grid;
        memory::NativeTerrainData ntd{};
        ntd.cols = 200;
        ntd.rows = 200;
        ntd.bytesPerRow = 200;
        ntd.worldOriginX = -100.0f;
        ntd.worldOriginY = -100.0f;
        ntd.gridToWorldScale = 1.0f;
        ntd.walkability.assign(200 * 200, 1);
        ntd.walkability[100 * 200 + 100] = 0; // obstacle
        grid.LoadFromNativeTerrain(ntd, 0.0f, 0.0f);
        CHECK(grid.HasNativeTerrain(), "GAP-3: Grid phai co NativeTerrain");

        grid.RecenterIfNeeded(10.0f, 10.0f);
        CHECK(grid.HasNativeTerrain(), "GAP-3: Sau khi Recenter van duy tri NativeTerrain");

        MapDeviceHandler md;
        md.SetTerrainGrid(&grid);
        // Tangent slide su dung grid thuc te thay vi nullptr
    }

    // 4. GAP-4: ComboManager dem quai song thuc te va goi OnSkillCastStarted
    {
        ComboConfig comboCfg;
        comboCfg.enabled = true;
        ComboManager comboMgr(comboCfg);

        TelemetryPacket combatPkt{};
        combatPkt.player.posX = 100.0f;
        combatPkt.player.posY = 100.0f;
        combatPkt.player.currentHP = 1000;
        combatPkt.player.maxHP = 1000;
        combatPkt.entityCount = 3;

        // Quai 0: Quai song
        combatPkt.entities[0].id = 1;
        combatPkt.entities[0].type = 1;
        combatPkt.entities[0].posX = 130.0f;
        combatPkt.entities[0].posY = 100.0f;
        combatPkt.entities[0].currentHP = 500;
        combatPkt.entities[0].maxHP = 500;

        // Quai 1: Quai chet (HP = 0, Dead flag)
        combatPkt.entities[1].id = 2;
        combatPkt.entities[1].type = 1;
        combatPkt.entities[1].posX = 110.0f;
        combatPkt.entities[1].posY = 100.0f;
        combatPkt.entities[1].currentHP = 0;
        combatPkt.entities[1].maxHP = 500;
        combatPkt.entities[1].extraFlags = 4;

        // Entity 2: Item drop
        combatPkt.entities[2].id = 3;
        combatPkt.entities[2].type = 2;
        combatPkt.entities[2].posX = 105.0f;
        combatPkt.entities[2].posY = 100.0f;

        ActionProposal comboProp = comboMgr.Propose(combatPkt, 2000);
        CHECK(comboProp.kind == BotActionKind::EmergencyAttack || comboProp.kind == BotActionKind::Combat,
              "GAP-4: ComboManager phai de xuat EmergencyAttack hoac Combat cho quai song");
        CHECK(comboProp.entityId == 1, "GAP-4: Combo phai nham vao quai 1 (song) thay vi quai 2 (chet)");
    }

    // 5. GAP-5: LootController Walk-To-Loot trong che do WASD
    {
        LootConfig lCfg;
        lCfg.moveMode = MovementMode::WASD;
        lCfg.lootRadius = 120.0f;
        lCfg.pickupRange = 22.0f;
        lCfg.wasdDwellMs = 300;
        CHECK(lCfg.wasdDwellMs >= 300, "GAP-5: wasdDwellMs phai >= 300ms theo INV-WASD-MIN-DWELL");

        LootController lootCtrl(lCfg);
        TelemetryPacket lootPkt{};
        lootPkt.player.posX = 100.0f;
        lootPkt.player.posY = 100.0f;
        lootPkt.player.currentHP = 1000;
        lootPkt.player.maxHP = 1000;
        lootPkt.entityCount = 1;
        lootPkt.entities[0].id = 77;
        lootPkt.entities[0].type = 2; // ItemDrop
        lootPkt.entities[0].posX = 160.0f; // Cach 60u (> pickupRange 22u)
        lootPkt.entities[0].posY = 100.0f;
        std::snprintf(lootPkt.entities[0].name, sizeof(lootPkt.entities[0].name), "%s", "Divine Orb");

        ActionProposal lootProp = lootCtrl.Propose(lootPkt, 3000);
        CHECK(lootProp.kind == BotActionKind::Pickup, "GAP-5: LootController phai de xuat Pickup cho Divine Orb");
        CHECK(lootProp.distance > 22.0f, "GAP-5: Item o xa hon pickupRange");

        bool lootExec = lootCtrl.Execute(lootProp, lootPkt, kmbox, 3000);
        CHECK(lootExec, "GAP-5: Execute Walk-To-Loot WASD phai thanh cong");
    }

    // 6. GAP-6: CharacterFSM Closed-Loop Transitions & Telemetry Encoding
    {
        CharacterFSM fsm;
        TelemetryPacket fsmPkt{};
        fsmPkt.player.posX = 100.0f;
        fsmPkt.player.posY = 100.0f;
        fsmPkt.player.currentHP = 1000;
        fsmPkt.player.maxHP = 1000;
        fsmPkt.area.areaSeed = 112233;
        std::snprintf(fsmPkt.area.areaName, sizeof(fsmPkt.area.areaName), "Strand");
        std::snprintf(fsmPkt.area.areaCode, sizeof(fsmPkt.area.areaCode), "MapStrand");

        fsm.Update(fsmPkt, 4000);
        CHECK(fsm.State() == CharacterState::InMapExploring, "GAP-6: Vao map khoi tao phai la InMapExploring");

        // Khi co quai song -> InMapCombat
        fsmPkt.entityCount = 1;
        fsmPkt.entities[0].id = 10;
        fsmPkt.entities[0].type = 1;
        fsmPkt.entities[0].posX = 150.0f;
        fsmPkt.entities[0].posY = 100.0f;
        fsmPkt.entities[0].currentHP = 800;
        fsmPkt.entities[0].maxHP = 800;
        fsm.Update(fsmPkt, 4050);
        CHECK(fsm.State() == CharacterState::InMapCombat, "GAP-6: Co quai song phai chuyen InMapCombat");

        // Quai chet, co do roi -> InMapLooting
        fsmPkt.entities[0].type = 2; // ItemDrop
        fsm.Update(fsmPkt, 4100);
        CHECK(fsm.State() == CharacterState::InMapLooting, "GAP-6: Quai chet co do phai chuyen InMapLooting");

        // Nhat do xong -> InMapExploring
        fsmPkt.entityCount = 0;
        fsm.Update(fsmPkt, 4150);
        CHECK(fsm.State() == CharacterState::InMapExploring, "GAP-6: Het do phai ve InMapExploring");

        // TRIỆT TIÊU TEST RÁC 12S:
        // Cũ: Map yên 12s tự thoát (vô lý vì chưa xong map).
        // Mới: Sau 12s vắng quái, map CHƯA xong (boss chưa hạ, coverage < 85%) -> CharacterFSM PHẢI tiếp tục InMapExploring!
        fsm.Update(fsmPkt, 4150 + 12000);
        CHECK(fsm.State() == CharacterState::InMapExploring,
              "ANTI-GARBAGE: Khong duoc thoat map som sau 12s khi chua ha Boss hoac chua du coverage");
        CHECK(!fsm.WantsPortalExit(), "ANTI-GARBAGE: Khong duoc WantsPortalExit khi chua xong map");

        // Khi Boss bi ha (SetBossSlain) va yen ang > 3s -> InMapPortalExit chuan Pro Player
        fsm.SetBossSlain(true);
        fsm.Update(fsmPkt, 4150 + 12000 + 3500);
        CHECK(fsm.State() == CharacterState::InMapPortalExit, "PRO-FLOW: Boss chet va yen ang phai InMapPortalExit");
        CHECK(fsm.WantsPortalExit(), "PRO-FLOW: WantsPortalExit khi InMapPortalExit");

        uint32_t flags = 0x00000001;
        flags = (flags & 0x0000FFFF) | (static_cast<uint32_t>(fsm.State()) << 16);
        const uint8_t decoded = (flags >> 16) & 0xFF;
        CHECK(decoded == static_cast<uint8_t>(CharacterState::InMapPortalExit),
              "GAP-6: MovementFlags giai ma dung CharacterState::InMapPortalExit");
    }

    std::cout << "  -> Autonomy Gap Resolution & Invariants Verification (Test 92) OK" << std::endl;
}

void TestBrainConflictGatekeeperAndPortalRetry() {
    std::cout << "[Test 93] Brain Conflict Gatekeeper & Map Device Portal Retry Polling (15/09/2026)..." << std::endl;

    // 1. Kiểm tra TownQuestEngine từ chối kích hoạt phím U khi đang ở Hideout
    {
        TownQuestEngine tqe;
        tqe.OnLoadingFinished("Shoreline Hideout", 1000);
        tqe.StartTravel(1, 0, 1000); // Yêu cầu đi Act 1 The Riverbank
        CHECK(!tqe.IsActive(), "INV-BRAIN-GATEKEEPER: TownQuestEngine phai tu choi StartTravel Act 1 khi o Hideout");
        CHECK(tqe.GetState() == TownQuestState::IDLE, "INV-BRAIN-GATEKEEPER: State phai van la IDLE");

        // Khi ở thị trấn bình thường (Clearfell Encampment), StartTravel phải hoạt động
        tqe.OnLoadingFinished("Clearfell Encampment", 2000);
        tqe.StartTravel(2, 0, 2000);
        CHECK(tqe.IsActive(), "TownQuestEngine phai chap nhan StartTravel khi o thi tran chien dich hop le");
    }

    // 2. Kiểm tra MapDeviceHandler::WaitForPortals Retry Polling
    {
        KMBoxNet kmbox;
        MapDeviceHandler md;
        InstantAtlasLoadForTests(md);

        uint64_t t = 1000;
        md.StartMapDevice(0, 0, false, t);

        TelemetryPacket pkt{};
        pkt.player.posX = 100.0f;
        pkt.player.posY = 100.0f;
        pkt.area.areaCode[0] = 'H';
        std::snprintf(pkt.area.areaName, sizeof(pkt.area.areaName), "Shoreline Hideout");

        ActionProposal prop = md.Propose(pkt, t);
        bool reachWait = DriveMapDeviceTo(md, prop, pkt, kmbox, t, MapDeviceState::WaitForPortals, 20);
        CHECK(reachWait, "MapDeviceHandler phai den duoc WaitForPortals");
        CHECK(md.State() == MapDeviceState::WaitForPortals, "State phai la WaitForPortals");

        // Giai đoạn Retry Polling: Sau 3000ms tính từ t, chưa có portal (entityCount = 0)
        // Handler PHẢI giữ nguyên WaitForPortals (không được Fail ngay)
        t += 3000;
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::WaitForPortals,
              "INV-MAPDEVICE-RETRY-POLL: Khong duoc fail-closed ngay khi chua qua 8000ms!");

        // Xuất hiện portal sau 4500ms
        PutNearbyMapPortal(pkt, 888);
        t += 1500;
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::EnterPortal,
              "INV-MAPDEVICE-RETRY-POLL: Phai chuyen sang EnterPortal khi phat hien portal type=3!");
    }

    // 3. Kiểm tra trường hợp quá 8000ms không có portal thì mới Failed
    {
        KMBoxNet kmbox;
        MapDeviceHandler md;
        InstantAtlasLoadForTests(md);

        uint64_t t = 1000;
        md.StartMapDevice(0, 0, false, t);

        TelemetryPacket pkt{};
        pkt.player.posX = 100.0f;
        pkt.player.posY = 100.0f;

        ActionProposal prop = md.Propose(pkt, t);
        DriveMapDeviceTo(md, prop, pkt, kmbox, t, MapDeviceState::WaitForPortals, 20);
        CHECK(md.State() == MapDeviceState::WaitForPortals, "WaitForPortals state");

        // Quá 8500ms không có portal -> Failed
        t += 8500;
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::Failed,
              "Qua 8000ms maxPortalWaitMs ma khong co portal phai chuyen sang Failed");
    }

    std::cout << "  -> Brain Conflict Gatekeeper & Map Device Portal Retry Polling (Test 93) OK" << std::endl;
}

void TestCoreCoordinatorAndZoneClassifierUpgrade() {
    std::cout << "[Test 94] Core Coordinator & Zone Classifier SSoT Invariants (16/09/2026)..." << std::endl;

    // 1. SSoT ZoneClassifier Verification
    {
        AreaTelemetryData hideoutArea{};
        std::snprintf(hideoutArea.areaName, sizeof(hideoutArea.areaName), "Coastal Hideout");
        std::snprintf(hideoutArea.areaCode, sizeof(hideoutArea.areaCode), "HideoutCoast");
        CHECK(common::ClassifyZone(hideoutArea) == common::ZoneKind::Hideout, "ZoneClassifier: Coastal Hideout phai la Hideout");
        CHECK(common::IsTownOrHideout(common::ClassifyZone(hideoutArea)), "ZoneClassifier: Hideout phai la TownOrHideout");

        AreaTelemetryData townArea{};
        std::snprintf(townArea.areaName, sizeof(townArea.areaName), "Clearfell Encampment");
        std::snprintf(townArea.areaCode, sizeof(townArea.areaCode), "Act1Town");
        CHECK(common::ClassifyZone(townArea) == common::ZoneKind::Town, "ZoneClassifier: Clearfell Encampment phai la Town");
        CHECK(common::IsTownOrHideout(common::ClassifyZone(townArea)), "ZoneClassifier: Town phai la TownOrHideout");

        AreaTelemetryData mapArea{};
        mapArea.areaSeed = 112233;
        std::snprintf(mapArea.areaName, sizeof(mapArea.areaName), "The Strand");
        std::snprintf(mapArea.areaCode, sizeof(mapArea.areaCode), "MapStrand");
        CHECK(common::ClassifyZone(mapArea) == common::ZoneKind::CombatMap, "ZoneClassifier: MapStrand phai la CombatMap");
        CHECK(!common::IsTownOrHideout(common::ClassifyZone(mapArea)), "ZoneClassifier: CombatMap khong phai TownOrHideout");

        AreaTelemetryData loadingArea{};
        CHECK(common::ClassifyZone(loadingArea) == common::ZoneKind::Unknown, "ZoneClassifier: Empty area phai la Unknown");
    }

    // 2. Deadlock Resolution: Combat Range Engage Alignment (151u - 380u)
    {
        ComboConfig comboCfg;
        comboCfg.enabled = true;
        ComboManager comboMgr(comboCfg);
        CharacterFSM fsm;

        TelemetryPacket pkt{};
        pkt.player.posX = 100.0f;
        pkt.player.posY = 100.0f;
        pkt.player.currentHP = 1000;
        pkt.player.maxHP = 1000;
        pkt.area.areaSeed = 112233;
        std::snprintf(pkt.area.areaName, sizeof(pkt.area.areaName), "The Strand");
        std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "MapStrand");

        // Quái ở khoảng cách 250u (trong vùng 151u - 380u từng gây deadlock)
        pkt.entityCount = 1;
        pkt.entities[0].id = 42;
        pkt.entities[0].type = game_layout::EntityTypeMonster;
        pkt.entities[0].posX = 350.0f; // dx = 250, dy = 0 -> dist = 250u
        pkt.entities[0].posY = 100.0f;
        pkt.entities[0].currentHP = 1500;
        pkt.entities[0].maxHP = 1500;

        fsm.Update(pkt, 1000);
        CHECK(fsm.State() == CharacterState::InMapCombat,
              "DEADLOCK-AUDIT: CharacterFSM phai o InMapCombat khi quai o 250u (<= 380u)");

        ActionProposal prop = comboMgr.Propose(pkt, 1000);
        CHECK(prop.kind == BotActionKind::Combat,
              "DEADLOCK-AUDIT: ComboManager PHẢI đề xuất Combat khi quai o 250u (khong duoc tra ve None!)");
        CHECK(prop.priority == bot_priority::COMBAT_ENGAGE || prop.priority == bot_priority::COMBAT,
              "DEADLOCK-AUDIT: Priority phai la COMBAT_ENGAGE (5800) hoac COMBAT (7000)");

        KMBoxNet kmbox;
        kmbox.EnableEmulationMode(true);
        kmbox.SetIgnoreWindowFocus(true);

        bool executed = comboMgr.Execute(prop, pkt, kmbox, 1000);
        CHECK(executed, "DEADLOCK-AUDIT: ComboManager phai Execute thanh cong buoc CombatEngage tiep can");

        // Khi quái quá xa (> 380u, ví dụ 450u)
        pkt.entities[0].posX = 550.0f; // dist = 450u
        ActionProposal propFar = comboMgr.Propose(pkt, 1010);
        CHECK(propFar.kind == BotActionKind::None,
              "DEADLOCK-AUDIT: ComboManager khong duoc de xuat khi quai o 450u (> 380u)");
    }

    // 3. Multi-criteria Map Completion Matrix
    {
        // Nhánh A: Heatmap Coverage >= 85%
        {
            CharacterFSM fsm;
            TelemetryPacket pkt{};
            pkt.player.posX = 100.0f;
            pkt.player.posY = 100.0f;
            pkt.player.currentHP = 1000;
            pkt.player.maxHP = 1000;
            pkt.area.areaSeed = 112233;
            std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "MapStrand");

            fsm.Update(pkt, 1000);
            CHECK(fsm.State() == CharacterState::InMapExploring, "Khoi tao InMapExploring");

            fsm.SetHeatmapCoverage(0.86f);
            fsm.Update(pkt, 5000);
            CHECK(fsm.State() == CharacterState::InMapPortalExit,
                  "PRO-MAP-COMPLETE: Heatmap coverage >= 85% phai InMapPortalExit");
        }

        // Nhánh B: Force Map Completed từ Agent / Macro Coordinator
        {
            CharacterFSM fsm;
            TelemetryPacket pkt{};
            pkt.player.posX = 100.0f;
            pkt.player.posY = 100.0f;
            pkt.player.currentHP = 1000;
            pkt.player.maxHP = 1000;
            pkt.area.areaSeed = 112233;
            std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "MapStrand");

            fsm.Update(pkt, 1000);
            fsm.SetForceMapCompleted(true);
            fsm.Update(pkt, 2000);
            CHECK(fsm.State() == CharacterState::InMapPortalExit,
                  "PRO-MAP-COMPLETE: SetForceMapCompleted(true) phai InMapPortalExit");
        }

        // Nhánh C: Hard Safety Timeout (> 360s)
        {
            CharacterFSM fsm;
            TelemetryPacket pkt{};
            pkt.player.posX = 100.0f;
            pkt.player.posY = 100.0f;
            pkt.player.currentHP = 1000;
            pkt.player.maxHP = 1000;
            pkt.area.areaSeed = 112233;
            std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "MapStrand");

            fsm.Update(pkt, 1000);
            // Sau 365s (366000ms)
            fsm.Update(pkt, 1000 + 365000);
            CHECK(fsm.State() == CharacterState::InMapPortalExit,
                  "PRO-MAP-COMPLETE: Hard safety timeout 360s phai InMapPortalExit");
        }
    }

    std::cout << "  -> Core Coordinator & Zone Classifier SSoT Invariants (Test 94) OK" << std::endl;
}

// ==========================================================
// 95. Test Dual-Mode Navigation & Abstract Combo Engine Invariants (16/09/2026)
//  - Xác minh Dual-Mode Navigation: Nhận diện Map Revealer / Smoother.
//  - Xác minh Topological Frontier: Khám phá dựa trên Walked Heatmap + ngõ cụt escape.
//  - Xác minh Abstract Combo Engine: SkillRole enum, Builder Q (4 hits) -> Spender R.
// ==========================================================
void TestDualModeNavigationAndAbstractComboEngine() {
    std::cout << "[Test 95] Dual-Mode Navigation & Abstract Combo Engine Invariants (16/09/2026)..." << std::endl;

    // Phân đoạn 1: Dual-Mode Navigation & Smoother-Proof Map Revealer
    {
        QuestNavConfig cfg;
        cfg.enabled = true;
        cfg.autoPatrol = true;
        cfg.mapRevealerMode = true; // Bật chế độ tương thích Smoother / Map Revealer
        QuestNavigator nav(cfg);

        CHECK(nav.IsMapRevealedExternally(), "mapRevealerMode=true phai kich hoat IsMapRevealedExternally");

        navigation::TerrainGrid grid(6.0f);
        grid.Initialize(0.0f, 0.0f);
        grid.SetWalkable(100.0f, 100.0f, 50.0f);
        grid.SetWalkable(100.0f, 120.0f, 50.0f);
        nav.SetTerrainGrid(&grid);

        nav.RecordPosition(100.0f, 100.0f);
        CHECK(nav.GetVisitCount(100.0f, 100.0f) == 1, "RecordPosition phai danh dau visitCount = 1");

        float outGoalX = 0.0f, outGoalY = 0.0f;
        bool foundFrontier = nav.FindTopologicalFrontier(100.0f, 100.0f, outGoalX, outGoalY);
        CHECK(foundFrontier, "FindTopologicalFrontier phai tim thay o walkable chua visited");
        CHECK(nav.GetVisitCount(outGoalX, outGoalY) == 0, "Topological frontier goal phai co visitCount == 0");

        nav.State().consecutiveVisitedSteps = 1;
        TelemetryPacket pkt{};
        pkt.player.posX = 100.0f;
        pkt.player.posY = 120.0f;
        pkt.player.posZ = 50.0f;
        pkt.player.currentHP = 1000;
        pkt.player.maxHP = 1000;
        pkt.area.areaSeed = 12345;
        std::snprintf(pkt.area.areaCode, sizeof(pkt.area.areaCode), "MapStrand");

        KMBoxNet kmbox;
        kmbox.EnableEmulationMode(true);
        kmbox.SetIgnoreWindowFocus(true);

        nav.State().lastDirectionChangeMs = 0;
        nav.Update(pkt, kmbox, 5000);
        CHECK(nav.State().isPatrolling, "Smoother-proof patrol phai duy tri isPatrolling");
    }

    // Phân đoạn 2: Abstract Combo Engine (Builder vs Spender)
    {
        ComboConfig comboCfg;
        CHECK(comboCfg.builderSkillVk == 0x51, "builderSkillVk phai mac dinh la phim Q (0x51)");
        CHECK(comboCfg.spenderSkillVk == 0x52, "spenderSkillVk phai mac dinh la phim R (0x52)");
        CHECK(comboCfg.finisherSkillVk == 0x52, "finisherSkillVk phai mac dinh la phim R (0x52)");
        CHECK(comboCfg.bossStrikeGoal == 4, "bossStrikeGoal phai la 4 don tich combo");

        CHECK(std::string(SkillRoleToString(SkillRole::ComboBuilder)) == "ComboBuilder", "SkillRole::ComboBuilder string OK");
        CHECK(std::string(SkillRoleToString(SkillRole::ComboSpender)) == "ComboSpender", "SkillRole::ComboSpender string OK");
        CHECK(std::string(SkillRoleToString(SkillRole::BasicClear)) == "BasicClear", "SkillRole::BasicClear string OK");
        CHECK(std::string(SkillRoleToString(SkillRole::SustainedBuff)) == "SustainedBuff", "SkillRole::SustainedBuff string OK");
        CHECK(std::string(SkillRoleToString(SkillRole::GapCloser)) == "GapCloser", "SkillRole::GapCloser string OK");
        CHECK(std::string(SkillRoleToString(SkillRole::PreBuff)) == "PreBuff", "SkillRole::PreBuff string OK");
        CHECK(std::string(SkillRoleToString(SkillRole::MobilityIframe)) == "MobilityIframe", "SkillRole::MobilityIframe string OK");

        comboCfg.enabled = true;
        comboCfg.isRangedMode = false;
        comboCfg.useMonkRotation = true;
        comboCfg.primarySkillVk = comboCfg.builderSkillVk; // Q
        comboCfg.bellSkillVk = comboCfg.spenderSkillVk;    // R
        ComboManager cm(comboCfg);

        TelemetryPacket bossPkt{};
        bossPkt.player.posX = 100.0f;
        bossPkt.player.posY = 100.0f;
        bossPkt.player.currentHP = 1000;
        bossPkt.player.maxHP = 1000;
        bossPkt.entityCount = 1;
        bossPkt.entities[0].id = 999;
        bossPkt.entities[0].type = 1; // Monster
        bossPkt.entities[0].rarity = 3; // Boss
        bossPkt.entities[0].currentHP = 50000;
        bossPkt.entities[0].maxHP = 50000;
        bossPkt.entities[0].posX = 150.0f;
        bossPkt.entities[0].posY = 100.0f;
        bossPkt.entities[0].distanceToPlayer = 50.0f;

        KMBoxNet kmbox;
        kmbox.EnableEmulationMode(true);
        kmbox.SetIgnoreWindowFocus(true);

        for (uint32_t s = 1; s <= 4; ++s) {
            cm.Update(bossPkt, kmbox, 1000 + s * 250);
            CHECK(cm.State().bossComboHits == s, "bossComboHits phai tang dan 1..4 khi tich combo vao Boss");
            CHECK(cm.State().bossIceStrikeCount == s, "bossIceStrikeCount tuong thich nguoc phai bang bossComboHits");
        }

        cm.Update(bossPkt, kmbox, 2500);
        CHECK(cm.State().totalBellsPlaced == 1, "Dat du 4 combo points phai thi trien Spender / Bell (totalBellsPlaced == 1)");
        CHECK(cm.State().bossComboHits == 0, "Sau khi Spender thi trien, bossComboHits phai reset ve 0");
    }

    std::cout << "  -> Dual-Mode Navigation & Abstract Combo Engine Invariants (Test 95) OK" << std::endl;
}

// ==========================================================
// 96. Test MapDeviceHandler UI Verification & Precision Coordinates
// ==========================================================
void TestMapDeviceUiVerificationAndPrecisionTargeting() {
    std::cout << "[Test 96] MapDeviceHandler UI Verification & Precision Target (INV-MAPDEVICE-VERIFY-UI)..." << std::endl;

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    TelemetryPacket pkt{};
    pkt.player.posX = 50.0f;
    pkt.player.posY = 50.0f;
    pkt.player.currentHP = 1000;
    pkt.player.maxHP = 1000;

    // 1. Kiểm tra tọa độ Fallback Shoreline Hideout
    {
        MapDeviceHandler md;
        const float fbX = md.Coordinates().shorelineFallbackX;
        const float fbY = md.Coordinates().shorelineFallbackY;
        CHECK(fbX == 881.0f || fbX == 402.0f, "shorelineFallbackX phai la 881.0f hoac 402.0f @1080p");
        CHECK(fbY == 484.0f || fbY == 210.0f, "shorelineFallbackY phai la 484.0f hoac 210.0f @1080p");

        auto pt1440 = md.Coordinates().ScalePoint(fbX, fbY, 2560.0f, 1440.0f);
        const float exp1440X = (fbX == 402.0f) ? 536.0f : 1175.0f;
        const float exp1440Y = (fbY == 210.0f) ? 280.0f : 645.0f;
        CHECK(std::abs(pt1440.x - exp1440X) < 2.0f, "Shoreline Map Device X @1440p phai xap xi " + std::to_string(exp1440X) + " (thuc te: " + std::to_string(pt1440.x) + ")");
        CHECK(std::abs(pt1440.y - exp1440Y) < 2.0f, "Shoreline Map Device Y @1440p phai xap xi " + std::to_string(exp1440Y) + " (thuc te: " + std::to_string(pt1440.y) + ")");
    }

    // 2. Kiểm tra INV-MAPDEVICE-VERIFY-UI: Fail-Closed khi UI chưa mở (chống click mù túi đồ)
    {
        MapDeviceHandler md;
        auto coords = md.Coordinates();
        coords.requireUiVerification = true;
        coords.openDeviceTimeoutMs = 3000;
        coords.openDeviceRetryIntervalMs = 1000;
        md.SetCoordinates(coords);

        uint64_t t = 1000;
        md.StartMapDevice(0, 0, false, t);
        ActionProposal prop{};
        prop.kind = BotActionKind::SwitchArea;

        // Tick 1: ApproachDevice -> OpenDevice ngay vi da trong tam tuong tac
        t = 1100;
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::OpenDevice, "Tick 1 phai chuyen sang OpenDevice");

        // Tick 2: Click LMB lan 1
        t = 1200;
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::OpenDevice, "Dang o OpenDevice cho UI");
        CHECK(md.InteractFSent() == true, "Da click LMB lan 1");

        // Tick 3: t vuot qua openDeviceTimeoutMs ma KHONG goi SetAtlasUiVerified(true)
        t = 1200 + 3500; // 4700ms - firstClick (1200) = 3500ms > timeout 3000ms
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::Failed, "INV-MAPDEVICE-VERIFY-UI: Het timeout ma UI chua mo phai FAIL-CLOSED lap tuc");
    }

    // 3. Kiểm tra INV-MAPDEVICE-VERIFY-UI: Tiến bước thành công khi Opcode 10 xác nhận UI
    {
        MapDeviceHandler md;
        auto coords = md.Coordinates();
        coords.requireUiVerification = true;
        coords.openDeviceTimeoutMs = 3000;
        md.SetCoordinates(coords);

        uint64_t t = 1000;
        md.StartMapDevice(0, 0, false, t);
        ActionProposal prop{};
        prop.kind = BotActionKind::SwitchArea;

        t = 1100;
        md.Execute(prop, pkt, kmbox, t);
        t = 1200;
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::OpenDevice, "Dang o OpenDevice");

        // Coordinator OCR phat hien TRAVERSE / WAYSTONE -> gui xac nhan Opcode 10
        md.SetAtlasUiVerified(true);
        CHECK(md.IsAtlasUiVerified() == true, "IsAtlasUiVerified phai la true sau khi xac nhan");

        t = 1300;
        md.Execute(prop, pkt, kmbox, t);
        CHECK(md.State() == MapDeviceState::SelectAtlasNode, "Sau khi verify UI thanh cong, FSM duoc phep advance sang SelectAtlasNode");
    }

    std::cout << "  -> MapDeviceHandler UI Verification & Precision Target (Test 96) OK" << std::endl;
}

// ==========================================================
// 97. ExiledBot-Style Smooth Movement & Micro Priority Arbiter
//  - Xác minh MovementController WASD Mode Non-blocking Stateful Holding & Dwell Time
//  - Xác minh MovementController MOUSE Mode Move-Only Key (ExiledBot 2 Model)
//  - Xác minh BotBrain Priority Hierarchy: COMBAT (7000) > PICKUP (6000) > COMBAT_ENGAGE (5800)
//  - Xác minh triệt tiêu 100% sleep_for(300ms) trong combat loop
// ==========================================================
void TestExiledBotSmoothMovementAndPriorities() {
    std::cout << "[Test 97] ExiledBot Smooth Movement & Micro Priority Arbiter Invariants..." << std::endl;

    KMBoxNet kmbox;
    kmbox.EnableEmulationMode(true);
    kmbox.SetIgnoreWindowFocus(true);

    // 1. Kiểm tra MovementController WASD Mode Non-blocking Stateful Holding & Dwell Time
    {
        navigation::MovementConfig wasdCfg;
        wasdCfg.mode = navigation::MovementMode::WASD;
        wasdCfg.wasdMinDwellMs = 250;
        navigation::MovementController wasdCtrl(wasdCfg);

        // Tick 1: Hướng Down Isometric (-50, -50) -> phải giữ phím 'S' (0x53)
        wasdCtrl.SteerTowards(-50.0f, -50.0f, kmbox, 1000);
        const auto& held1 = wasdCtrl.HeldVks();
        CHECK(held1.size() == 1, "WASD phai giu 1 phim");
        CHECK(held1[0] == 0x53, "Phim dang giu phai la 'S' (0x53)");

        // Tick 2: Giữ nguyên hướng sau 50ms -> không đổi phím
        wasdCtrl.SteerTowards(-50.0f, -50.0f, kmbox, 1050);
        const auto& held2 = wasdCtrl.HeldVks();
        CHECK(held2.size() == 1 && held2[0] == 0x53, "Cung huong phai duy tri phim 'S'");

        // Tick 3: Thử đổi hướng sang Up Isometric (50, 50) sau 100ms (< wasdMinDwellMs 250ms)
        // INV-WASD-MIN-DWELL: Phải giữ nguyên hướng 'S' để chống micro-jitter
        wasdCtrl.SteerTowards(50.0f, 50.0f, kmbox, 1100);
        const auto& held3 = wasdCtrl.HeldVks();
        CHECK(held3.size() == 1 && held3[0] == 0x53, "INV-WASD-MIN-DWELL: Chua het dwell 250ms phai giu nguyen 'S'");

        // Tick 4: Đổi hướng sang Up Isometric sau 300ms (> wasdMinDwellMs 250ms) -> chuyển sang 'W' (0x57)
        wasdCtrl.SteerTowards(50.0f, 50.0f, kmbox, 1300);
        const auto& held4 = wasdCtrl.HeldVks();
        CHECK(held4.size() == 1 && held4[0] == 0x57, "Sau khi het dwell phai chuyen huong sang 'W' (0x57)");

        // Tick 5: Stop -> phải nhả toàn bộ phím
        wasdCtrl.Stop(kmbox);
        CHECK(wasdCtrl.HeldVks().empty(), "Stop phai giai phong toan bo phim WASD");
    }

    // 2. Kiểm tra MovementController MOUSE Mode Move-Only Key (chuẩn ExiledBot 2)
    {
        navigation::MovementConfig mouseCfg;
        mouseCfg.mode = navigation::MovementMode::MOUSE;
        mouseCfg.moveOnlyVk = 0x54; // 'T'
        navigation::MovementController mouseCtrl(mouseCfg);

        // Kích hoạt di chuyển bằng chuột theo waypoint
        mouseCtrl.SteerTowards(50.0f, 50.0f, kmbox, 2000);
        CHECK(mouseCtrl.IsMoveOnlyActive(), "Move-Only Key phai duoc kich hoat khi SteerTowards o MOUSE mode");

        // Stop -> giải phóng phím Move-Only
        mouseCtrl.Stop(kmbox);
        CHECK(!mouseCtrl.IsMoveOnlyActive(), "Stop phai nha Move-Only key");
    }

    // 3. Kiểm tra BotBrain Priority Hierarchy: COMBAT (7000) > PICKUP (6000) > COMBAT_ENGAGE (5800)
    {
        ComboConfig comboCfg{};
        comboCfg.enabled = true;
        comboCfg.isRangedMode = false;
        comboCfg.safeRange = 30.0f;
        comboCfg.monstersAttackRange = 80.0f;
        comboCfg.engageRange = 200.0f;
        ComboManager comboMgr(comboCfg);

        LootConfig lootCfg{};
        lootCfg.enabled = true;
        lootCfg.lootRadius = 120.0f;
        lootCfg.pickupRange = 22.0f;
        LootController lootCtrl(lootCfg);

        BotBrain brain;
        brain.AddHandler(&comboMgr);
        brain.AddHandler(&lootCtrl);

        // Kịch bản A: Quái vật ở xa (khoảng cách 120u: > monstersAttackRange 80u và <= engageRange 200u) + Đồ quý ở gần (khoảng cách 15u)
        TelemetryPacket pktA{};
        pktA.player.posX = 1000.0f;
        pktA.player.posY = 1000.0f;
        pktA.player.currentHP = 1000;
        pktA.player.maxHP = 1000;

        // Quái vật ở xa
        pktA.entities[0].id = 101;
        pktA.entities[0].type = 1; // Monster
        pktA.entities[0].posX = 1120.0f;
        pktA.entities[0].posY = 1000.0f;
        pktA.entities[0].currentHP = 500;
        pktA.entities[0].maxHP = 500;
        std::snprintf(pktA.entities[0].name, sizeof(pktA.entities[0].name), "Dune Rhoa");

        // Đồ quý ở gần chân
        pktA.entities[1].id = 202;
        pktA.entities[1].type = 2; // Item
        pktA.entities[1].rarity = 2; // Rare
        pktA.entities[1].posX = 1015.0f;
        pktA.entities[1].posY = 1000.0f;
        std::snprintf(pktA.entities[1].name, sizeof(pktA.entities[1].name), "Exalted Orb");

        pktA.entityCount = 2;

        // Kiểm tra đề xuất của ComboManager: Quái ở xa -> COMBAT_ENGAGE (5800)
        ActionProposal comboProp = comboMgr.Propose(pktA, 3000);
        CHECK(comboProp.priority == bot_priority::COMBAT_ENGAGE, "Quai o xa phai de xuat priority COMBAT_ENGAGE (5800)");

        // Kiểm tra đề xuất của LootController: Đồ quý -> PICKUP (6000)
        ActionProposal lootProp = lootCtrl.Propose(pktA, 3000);
        CHECK(lootProp.priority == bot_priority::PICKUP, "Do quy phai de xuat priority PICKUP (6000)");

        // BotBrain Arbiter phân xử: PICKUP (6000) > COMBAT_ENGAGE (5800)
        brain.Process(pktA, kmbox, 3000);
        CHECK(brain.LastChosen().kind == BotActionKind::Pickup, "BotBrain phai chon nhat do quy truoc khi tiep can quai o xa");

        // Kịch bản B: Quái vật áp sát trong tầm đánh (cự ly 60u: > safeRange 30u và <= monstersAttackRange 80u)
        pktA.entities[0].posX = 1060.0f; // Quái áp sát trong tầm đánh
        ActionProposal closeComboProp = comboMgr.Propose(pktA, 3050);
        CHECK(closeComboProp.priority == bot_priority::COMBAT, "Quai trong tam danh phai de xuat priority COMBAT (7000)");

        // BotBrain Arbiter phân xử: COMBAT (7000) > PICKUP (6000)
        brain.Process(pktA, kmbox, 3050);
        CHECK(brain.LastChosen().kind == BotActionKind::Combat, "BotBrain phai uu tien COMBAT danh quai can ke truoc nhat do");
    }

    // 4. Kiểm tra Non-blocking Execution (Triệt tiêu 100% sleep_for 300ms trong combat loop)
    {
        ComboConfig comboCfg{};
        comboCfg.enabled = true;
        ComboManager comboMgr(comboCfg);

        navigation::MovementConfig moveCfg;
        moveCfg.mode = navigation::MovementMode::WASD;
        navigation::MovementController moveCtrl(moveCfg);
        comboMgr.SetMovementController(&moveCtrl);

        TelemetryPacket pkt{};
        pkt.player.posX = 1000.0f;
        pkt.player.posY = 1000.0f;
        pkt.player.currentHP = 1000;
        pkt.player.maxHP = 1000;
        pkt.entities[0].id = 999;
        pkt.entities[0].type = 1;
        pkt.entities[0].posX = 1050.0f;
        pkt.entities[0].posY = 1000.0f;
        pkt.entities[0].currentHP = 100;
        pkt.entities[0].maxHP = 100;
        pkt.entityCount = 1;

        ActionProposal prop{};
        prop.kind = BotActionKind::Combat;
        prop.priority = bot_priority::COMBAT_ENGAGE;
        prop.targetX = 1050.0f;
        prop.targetY = 1000.0f;
        prop.entityId = 999;

        const auto start = std::chrono::high_resolution_clock::now();
        comboMgr.ExecuteEngageMovement(prop, pkt, kmbox, 4000);
        const auto end = std::chrono::high_resolution_clock::now();
        const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

        CHECK(elapsedMs < 30, "ExecuteEngageMovement phai non-blocking (<30ms), khong duoc chua sleep_for(300ms)!");
    }

    std::cout << "  -> ExiledBot Smooth Movement & Micro Priority Arbiter (Test 97) OK" << std::endl;
}



