"""
Unit Tests cho POE2 Realtime Trading Advisor & Overlay HUD
"""

import os
import sys

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")

# Thêm root vào sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from src.assistant_tool.gold_tracker import GoldTracker
from src.assistant_tool.log_watcher import LogWatcher
from src.assistant_tool.market_advisor import MarketAdvisor


def test_gold_tracker():
    gt = GoldTracker(50000)
    assert gt.current_gold == 50000
    assert gt.can_afford_tax("PINNACLE_KEY") is True
    assert gt.get_tax_for("PINNACLE_KEY") == 450
    assert gt.max_trades_affordable("PINNACLE_KEY") == 50000 // 450

    # Trừ thuế thành công
    ok = gt.deduct_tax("PINNACLE_KEY")
    assert ok is True
    assert gt.current_gold == 50000 - 450

    # Thêm vàng
    gt.add_gold(10000)
    assert gt.current_gold == 50000 - 450 + 10000
    print("[PASS] test_gold_tracker passed.")

def test_market_advisor():
    gt = GoldTracker(1000)
    ma = MarketAdvisor(gt)

    # 1000 vàng không đủ cho kèo cần 3500 vàng thuế
    trades = ma.get_market_opportunities()
    assert len(trades) >= 5

    # Kiểm tra tính khả thi
    bulk_trade = next(t for t in trades if t.id == "TRADE_BULK_ORBS")
    assert bulk_trade.is_affordable is False

    # Đưa thêm 100k vàng vào
    gt.set_gold(100000)
    trades_updated = ma.get_market_opportunities()
    bulk_trade_updated = next(t for t in trades_updated if t.id == "TRADE_BULK_ORBS")
    assert bulk_trade_updated.is_affordable is True
    print("[PASS] test_market_advisor passed.")

def test_log_watcher_parsing():
    lw = LogWatcher("dummy.txt")

    # Test 1: Whisper trade
    line_whisper = "2026/09/05 20:00:00 120469812 91c6cc1 [INFO Client 25032] @From Exile_Master: Hi, I'd like to buy your Sekhema Key for 4.6 divine in Standard"
    event1 = lw.parse_line(line_whisper)
    assert event1 is not None
    assert event1.event_type == "WHISPER_TRADE"
    assert "Exile_Master" in event1.sender
    assert "4.6 divine" in event1.message

    # Test 2: Trade chat
    line_trade = "2026/09/05 19:56:52 120530718 3ef23348 [INFO Client 25032] $DeyyuS_Sekhema: WTS 4th Trial of the Sekhemas - AFK run - 2 Divine"
    event2 = lw.parse_line(line_trade)
    assert event2 is not None
    assert event2.event_type == "TRADE_CHAT"
    assert event2.sender == "DeyyuS_Sekhema"

    # Test 3: Zone transition
    line_zone = "2026/09/05 19:55:52 120470937 7fbd1225 [INFO Client 25032] [SCENE] Set Source [The Grelwood]"
    event3 = lw.parse_line(line_zone)
    assert event3 is not None
    assert event3.event_type == "ZONE_CHANGE"
    assert "The Grelwood" in event3.message
    assert event3.area_name == "The Grelwood"

    # Test 3b: Generating level + Hive Fortress (Client.txt 10/09/2026 18:29:57 PID 36104)
    line_gen = '2026/09/10 18:29:57 547320703 2caa229f [DEBUG Client 36104] Generating level 77 area "ChayulaLeague_Tower" with seed 3565528473'
    event_gen = lw.parse_line(line_gen)
    assert event_gen is not None
    assert event_gen.event_type == "AREA_GENERATED"
    assert event_gen.area_code == "ChayulaLeague_Tower"

    line_hive = "2026/09/10 18:29:57 547321000 7fbd1225 [INFO Client 36104] [SCENE] Set Source [Hive Fortress]"
    event_hive = lw.parse_line(line_hive)
    assert event_hive is not None
    assert event_hive.event_type == "ZONE_CHANGE"
    assert event_hive.area_name == "Hive Fortress"

    # Test 4: Bỏ qua các UI scene overlay (Act 1..4, Atlas, null, unknown)
    for ignored in ["Act 1", "Act 2", "Act 3", "Act 4", "Atlas", "(null)", "(unknown)"]:
        line_ignored = f"2026/09/05 19:55:52 120470937 7fbd1225 [INFO Client 25032] [SCENE] Set Source [{ignored}]"
        assert lw.parse_line(line_ignored) is None, f"Scene {ignored} phải bị bỏ qua!"

    print("[PASS] test_log_watcher_parsing passed.")


def test_log_watcher_seed_latest_playable_zone():
    """Seed đuôi Client.txt: overlay Act 2/(null) bị bỏ, Hive Fortress là zone hiện tại."""
    import tempfile

    with tempfile.TemporaryDirectory() as tmp:
        path = os.path.join(tmp, "Client.txt")
        with open(path, "w", encoding="utf-8") as f:
            f.write("2026/09/10 18:20:23 546746515 7fbd1225 [INFO Client 36104] [SCENE] Set Source [Act 2]\n")
            f.write("2026/09/10 18:20:24 546747750 7fbd1225 [INFO Client 36104] [SCENE] Set Source [(null)]\n")
            f.write("2026/09/10 18:20:24 546747750 7fbd1225 [INFO Client 36104] [SCENE] Set Source [Shoreline Hideout]\n")
            f.write('2026/09/10 18:29:57 547320703 2caa229f [DEBUG Client 36104] Generating level 77 area "ChayulaLeague_Tower" with seed 3565528473\n')
            f.write("2026/09/10 18:29:57 547321000 7fbd1225 [INFO Client 36104] [SCENE] Set Source [(null)]\n")
            f.write("2026/09/10 18:29:57 547321000 7fbd1225 [INFO Client 36104] [SCENE] Set Source [Hive Fortress]\n")
            f.write("2026/09/10 18:29:59 547323234 4cba6a94 [INFO Client 36104] [LOADING SCREEN] (Hive Fortress) Duration = 2.65302 seconds\n")
            f.write("2026/09/10 18:45:00 338000000 7fbd1225 [INFO Client 36104] [SCENE] Set Source [Atlas]\n")

        lw = LogWatcher(path)
        seeded = lw.seed_latest_playable_zone()
        assert seeded is not None
        assert seeded.event_type == "ZONE_CHANGE"
        assert seeded.area_name == "Hive Fortress"
    print("[PASS] test_log_watcher_seed_latest_playable_zone passed.")


def test_find_poe2_client_log():
    import tempfile
    from src.assistant_tool.config import Config

    original = list(Config.DEFAULT_LOG_PATHS)
    try:
        with tempfile.TemporaryDirectory() as tmp:
            client = os.path.join(tmp, "Client.txt")
            with open(client, "w", encoding="utf-8") as f:
                f.write("x")
            Config.DEFAULT_LOG_PATHS = [client, os.path.join(tmp, "missing", "Client.txt")]
            assert Config.find_poe2_client_log() == client

            missing_dir = os.path.join(tmp, "other")
            os.makedirs(missing_dir, exist_ok=True)
            latest = os.path.join(missing_dir, "LatestClient.txt")
            with open(latest, "w", encoding="utf-8") as f:
                f.write("y")
            Config.DEFAULT_LOG_PATHS = [os.path.join(missing_dir, "Client.txt")]
            assert Config.find_poe2_client_log() == latest
    finally:
        Config.DEFAULT_LOG_PATHS = original
    print("[PASS] test_find_poe2_client_log passed.")

def test_discord_parser():
    from src.assistant_tool.discord_monitor import DiscordTradeParser

    # Test parse WTS Sekhema Key
    msg = "WTS 5 Sekhema Keys 4.2 div each - fast trade"
    offer = DiscordTradeParser.parse_message(msg, current_league="Forbidden Rite")
    assert offer is not None
    assert offer.action == "WTS"
    assert offer.item_name == "Sekhema Key"
    assert offer.quantity == 5
    assert offer.price_unit == 4.2
    assert "KÈO RẺ" in offer.evaluation
    assert offer.profit_estimate_div > 0
    print("[PASS] test_discord_parser passed.")

def test_core_shm_bridge():
    from src.assistant_tool.core_shm_bridge import CoreShmBridge

    bridge = CoreShmBridge(low_hp_threshold=35.0)
    alerts = []
    bridge.register_low_hp_alert(lambda cur, max_hp, pct: alerts.append((cur, max_hp, pct)))

    # Test alert triggering logic
    bridge._notify_low_hp(20, 100, 20.0)
    assert len(alerts) == 1
    assert alerts[0] == (20, 100, 20.0)

    # Test start and stop
    assert bridge.start() is True
    bridge.stop()
    print("[PASS] test_core_shm_bridge passed.")

def test_gem_manager():
    from src.assistant_tool.gem_manager import GemManager

    gm = GemManager("WARRIOR_MELEE")
    rec = gm.recommend_engraving(uncut_gem_level=1, is_support=False)
    assert rec is not None
    assert rec.gem_name == "Cleave"
    assert rec.role == "MAIN_DPS"

    gm.record_engraved("Cleave")
    gm.record_socketed("Cleave")
    assert "Cleave" in gm.engraved_gems
    assert "Cleave" in gm.socketed_gems

    # Support gem recommendation
    rec_sup = gm.recommend_engraving(uncut_gem_level=1, is_support=True)
    assert rec_sup is not None
    assert rec_sup.gem_name == "Brutality Support"
    print("[PASS] test_gem_manager passed.")

def test_auto_quester():
    from src.assistant_tool.auto_quester import AutoQuester

    aq = AutoQuester()
    obj1 = aq.get_current_objective("G1_1", "The Riverbank")
    assert obj1 is not None
    assert "The Riverbank" in obj1.area_name
    assert obj1.action_type == "TRANSITION"

    # Complete objective with Book of Skill reward
    aq.record_objective_completed("Una", reward="BOOK_OF_SKILL")
    assert aq.skill_books_consumed == 1
    assert ">> [QUEST" in aq.get_status_banner()
    print("[PASS] test_auto_quester passed.")

def test_task_opt_02_command_packet():
    import mmap
    import struct
    import time

    from src.assistant_tool.core_shm_bridge import PROTOCOL_VERSION, SHARED_MEM_MAGIC, CoreShmBridge

    # 1. Verify CommandPacket format string <QIfffIII matches 36 bytes
    cmd_dummy = struct.pack("<QIfffIII", 999, 99, 0.0, 0.0, 0.0, 0, 0, 0)
    assert len(cmd_dummy) == 36, f"CommandPacket must be 36 bytes, got {len(cmd_dummy)}"

    # 2. Setup mock SHM
    shm = mmap.mmap(-1, 4 * 1024 * 1024)
    bridge = CoreShmBridge()
    bridge._shm = shm

    # Write initial header: cmd_seq = 0, cmd_read = 0
    now_ms = int(time.time() * 1000)
    shm[:64] = struct.pack("<QIIQQQQQQ", SHARED_MEM_MAGIC, PROTOCOL_VERSION, 1, 0, 0, 0, now_ms, 0, 0)

    # Test send_panic_stop
    ok = bridge.send_panic_stop()
    assert ok is True, "send_panic_stop should succeed when queue is not full"
    # Verify cmd_seq increased to 1
    _, _, _, _, cmd_seq, cmd_read, _, _, _ = struct.unpack("<QIIQQQQQQ", shm[:64])
    assert cmd_seq == 1
    # Verify packet in slot 0 (offset 0x300000)
    pkt = shm[0x300000:0x300000 + 36]
    cmd_id, op_code, x, y, z, entity_id, prio, timeout = struct.unpack("<QIfffIII", pkt)
    assert cmd_id == 999
    assert op_code == 99

    # Test SPSC queue full check (cmd_seq - cmd_read >= 16)
    shm[24:32] = struct.pack("<Q", 16)  # cmd_seq = 16
    shm[32:40] = struct.pack("<Q", 0)   # cmd_read = 0
    assert bridge.send_panic_stop() is False, "send_panic_stop must return False when queue is full"
    assert bridge.send_recalibrate_xyz() is False, "send_recalibrate_xyz must return False when queue is full"
    assert bridge.send_trigger_auto_login() is False, "send_trigger_auto_login must return False when queue is full"

    bridge._shm = None
    shm.close()
    print("[PASS] test_task_opt_02_command_packet passed.")

def test_task_opt_03_seqlock_reader():
    import mmap
    import struct
    import time

    from src.assistant_tool.core_shm_bridge import PROTOCOL_VERSION, SHARED_MEM_MAGIC, CoreShmBridge

    shm = mmap.mmap(-1, 4 * 1024 * 1024)
    bridge = CoreShmBridge(poll_interval=0.01)
    bridge._shm = shm

    # Slot 1 is at 0x11000 for telem_seq = 1 (1 & 1 == 1)
    slot1_off = 0x11000
    # Player telemetry: snap_id=101, ts=99999, paddr=0x140000000, pos=(12.0, 34.0, 56.0), yaw=1.0, pitch=2.0, hp=150/200, mana=80/100, es=10/10, ward=0/0, spirit=5/20, weapon=1, mov=0, debuff=0
    # Kích thước chuẩn: 16B (snapshotId, timestamp) + 84B (PlayerTelemetryData) = 100B
    player_data = struct.pack("<QQQfffffIIIIIIIIIIIIQ", 101, 99999, 0x140000000, 12.0, 34.0, 56.0, 1.0, 2.0, 150, 200, 80, 100, 10, 10, 0, 0, 5, 20, 1, 0, 0)
    shm[slot1_off:slot1_off + len(player_data)] = player_data

    # Entity count = 2 at slot1_off + 100
    shm[slot1_off + 100:slot1_off + 104] = struct.pack("<I", 2)
    # Entity 0: type 1 (Monster) at slot1_off + 104 + 4
    shm[slot1_off + 104 + 4:slot1_off + 104 + 6] = struct.pack("<H", 1)
    # Entity 1: type 2 (Item) at slot1_off + 104 + 88 + 4
    shm[slot1_off + 104 + 88 + 4:slot1_off + 104 + 88 + 6] = struct.pack("<H", 2)

    # Area data at slot1_off + 39016
    area_data = struct.pack("<QII32s48s", 12345, 10, 888, b"The Riverbank\x00", b"Riverbank\x00")
    shm[slot1_off + 39016:slot1_off + 39016 + len(area_data)] = area_data

    now_ms = int(time.time() * 1000)
    # Header: telem_seq = 1, isRunning = 1, core_hb = now_ms
    shm[:64] = struct.pack("<QIIQQQQQQ", SHARED_MEM_MAGIC, PROTOCOL_VERSION, 1, 1, 0, 0, now_ms, 0, 0)

    # Duplicate seq check: if bridge._last_seq is already 1, snapshot should not be updated
    bridge._last_seq = 1
    assert bridge.last_snapshot.is_online is False

    # Now reset bridge._last_seq to 0 and verify reading through worker loop
    bridge._last_seq = 0
    received = []
    bridge.register_listener(lambda snap: received.append(snap))

    bridge.start()
    time.sleep(0.08)
    bridge.stop()

    assert len(received) >= 1, "Should have received at least 1 snapshot via Seqlock"
    snap = received[-1]
    assert snap.is_online is True
    assert snap.player.player_addr == 0x140000000
    assert snap.player.current_hp == 150
    assert snap.player.max_hp == 200
    assert snap.player.active_weapon_set == 1
    assert snap.monsters_count == 1
    assert snap.items_count == 1
    assert bridge._last_seq == 1, "Last seq should be updated to 1"

    shm.close()
    print("[PASS] test_task_opt_03_seqlock_reader passed.")

def test_task_opt_08_overlay_thread_safety():
    import inspect

    from src.assistant_tool.overlay_gui import OverlayHUD
    init_src = inspect.getsource(OverlayHUD.__init__)
    assert "self.gold_tracker.register_listener(lambda new_gold: self.after(0, self._on_gold_changed, new_gold))" in init_src, "gold_tracker listener must be wrapped with self.after(0, ...)"
    assert "self.log_watcher.register_listener(lambda evt: self.after(0, self._on_log_event, evt))" in init_src, "log_watcher listener must be wrapped with self.after(0, ...)"
    print("[PASS] test_task_opt_08_overlay_thread_safety passed.")

def test_task_opt_10_snapshot_cleanup():
    from src.assistant_tool.sync_memory_snapshot import SyncMemorySnapshotManager
    mgr = SyncMemorySnapshotManager(pid=99999999)
    assert hasattr(mgr, "close"), "SyncMemorySnapshotManager must have close() method"
    assert hasattr(mgr, "__del__"), "SyncMemorySnapshotManager must have __del__() method"
    assert hasattr(mgr, "__enter__") and hasattr(mgr, "__exit__"), "SyncMemorySnapshotManager should support context manager"
    mgr.close()
    assert mgr.h_process is None

    # Context manager check
    with SyncMemorySnapshotManager(pid=99999999) as m:
        pass
    assert m.h_process is None
    print("[PASS] test_task_opt_10_snapshot_cleanup passed.")

def test_binary_ipc_contract_and_struct_alignment():
    """Kiểm tra tính nhất quán nhị phân 100% của toàn bộ struct IPC."""
    import ctypes

    from scripts.test_ipc_bridge import (
        AreaTelemetryData,
        CommandPacket,
        EntityTelemetryData,
        PlayerTelemetryData,
        SharedMemoryHeader,
        TelemetryPacket,
    )

    # 1. Kích thước chuẩn các struct
    assert ctypes.sizeof(SharedMemoryHeader) == 64, f"SharedMemoryHeader != 64 bytes ({ctypes.sizeof(SharedMemoryHeader)})"
    assert ctypes.sizeof(PlayerTelemetryData) == 84, f"PlayerTelemetryData != 84 bytes ({ctypes.sizeof(PlayerTelemetryData)})"
    assert ctypes.sizeof(EntityTelemetryData) == 88, f"EntityTelemetryData != 88 bytes ({ctypes.sizeof(EntityTelemetryData)})"
    assert ctypes.sizeof(AreaTelemetryData) == 96, f"AreaTelemetryData != 96 bytes ({ctypes.sizeof(AreaTelemetryData)})"
    assert ctypes.sizeof(CommandPacket) == 36, f"CommandPacket != 36 bytes ({ctypes.sizeof(CommandPacket)})"
    assert ctypes.sizeof(TelemetryPacket) == 39112, f"TelemetryPacket != 39112 bytes ({ctypes.sizeof(TelemetryPacket)})"

    # 2. Offset của SharedMemoryHeader
    assert SharedMemoryHeader.magic.offset == 0
    assert SharedMemoryHeader.protocolVersion.offset == 8
    assert SharedMemoryHeader.isRunning.offset == 12
    assert SharedMemoryHeader.telemetrySequence.offset == 16
    assert SharedMemoryHeader.commandSequence.offset == 24
    assert SharedMemoryHeader.commandReadIndex.offset == 32
    assert SharedMemoryHeader.lastCoreHeartbeat.offset == 40
    assert SharedMemoryHeader.lastAgentHeartbeat.offset == 48
    assert SharedMemoryHeader.reserved.offset == 56

    # 3. Offset của CommandPacket
    assert CommandPacket.commandId.offset == 0
    assert CommandPacket.opCode.offset == 8
    assert CommandPacket.targetX.offset == 12
    assert CommandPacket.targetY.offset == 16
    assert CommandPacket.targetZ.offset == 20
    assert CommandPacket.targetEntityId.offset == 24
    assert CommandPacket.priority.offset == 28
    assert CommandPacket.timeoutMs.offset == 32

    # 4. Offset của TelemetryPacket
    assert TelemetryPacket.snapshotId.offset == 0
    assert TelemetryPacket.timestamp.offset == 8
    assert TelemetryPacket.player.offset == 16
    assert TelemetryPacket.entityCount.offset == 100
    assert TelemetryPacket.entities.offset == 104
    assert TelemetryPacket.terrainWalkability.offset == 22632
    assert TelemetryPacket.area.offset == 39016

    print("[PASS] test_binary_ipc_contract_and_struct_alignment passed.")

def test_telemetry_snapshot_monster_fields():
    """Kiểm tra các trường monsters_alive_total và entities_monster_count trên CoreTelemetrySnapshot."""
    from src.assistant_tool.core_shm_bridge import CoreTelemetrySnapshot, PlayerTelemetry

    snap = CoreTelemetrySnapshot(
        is_online=True,
        player=PlayerTelemetry(current_hp=100, max_hp=100),
        monsters_count=8,
        monsters_alive_total=30,
        entities_monster_count=8
    )
    assert snap.monsters_count == 8
    assert snap.monsters_alive_total == 30
    assert snap.entities_monster_count == 8
    print("[PASS] test_telemetry_snapshot_monster_fields passed.")

if __name__ == "__main__":
    test_gold_tracker()
    test_market_advisor()
    test_log_watcher_parsing()
    test_log_watcher_seed_latest_playable_zone()
    test_find_poe2_client_log()
    test_discord_parser()
    test_core_shm_bridge()
    test_task_opt_02_command_packet()
    test_task_opt_03_seqlock_reader()
    test_task_opt_08_overlay_thread_safety()
    test_task_opt_10_snapshot_cleanup()
    test_binary_ipc_contract_and_struct_alignment()
    test_gem_manager()
    test_auto_quester()
    test_telemetry_snapshot_monster_fields()
    print("\n✅ TẤT CẢ UNIT TESTS ĐỀU VƯỢT QUA 100%!")

