"""
Unit Tests for Optical Inventory Scanner & Hooded One Identify All (Doc 48 / Doc 43 P1-5)
==========================================================================================
Kiểm thử các bất biến kiến trúc:
- INV-UI-INVENTORY-ROW1: Độ khớp tọa độ Slot 0..11 trên 2560x1440 và 1920x1080 (Sai số Delta = 0px).
- INV-NPC-01: Interlock an toàn vùng Town/Hideout, fail-closed khi có quái vật.
- Full Inventory Detection: Phát hiện chính xác trạng thái túi đồ đầy (>= 48/60 slots).
- Opcode 8 Emission: Gửi lệnh EXECUTE_NPC_INTERACT_ROUTINE với mode 1 (IdentifyAll) khi túi đầy tại làng.
"""

import os
import tempfile
import pytest
from PIL import Image

from src.assistant_tool.optical_inventory_scanner import OpticalInventoryScanner
from src.assistant_tool.autonomous_lifecycle import AutonomousLifecycleManager, LifecyclePhase


def test_optical_inventory_coordinates_scaling():
    """Kiểm tra độ khớp tọa độ 1440p so với Ground-Truth thực địa tại 0ms (Doc 48 Mục 2.1)."""
    scanner_1440 = OpticalInventoryScanner(screen_w=2560, screen_h=1440)

    # Đỉnh góc trên bên trái: origin_x = 1696.0, origin_y = 786.67 ~ 787, cell = 70.0
    # Slot 0 tâm: (1731, 822)
    c0_x, c0_y = scanner_1440.get_slot_center(0, 0)
    assert c0_x == 1731
    assert c0_y == 822

    # Slot 1: (1801, 822)
    c1_x, c1_y = scanner_1440.get_slot_center(1, 0)
    assert c1_x == 1801
    assert c1_y == 822

    # Slot 4: (2011, 822)
    c4_x, c4_y = scanner_1440.get_slot_center(4, 0)
    assert c4_x == 2011
    assert c4_y == 822

    # Slot 11: (2501, 822)
    c11_x, c11_y = scanner_1440.get_slot_center(11, 0)
    assert c11_x == 2501
    assert c11_y == 822

    # Hàng 4 (hàng cuối cùng): row = 4 -> Y = 787 + 4*70 + 35 = 1102
    c0_r4_x, c0_r4_y = scanner_1440.get_slot_center(0, 4)
    assert c0_r4_x == 1731
    assert c0_r4_y == 1102


def test_optical_inventory_row1_waystones_detection():
    """Kiểm tra phát hiện Waystone trên ảnh thực nghiệm."""
    scanner = OpticalInventoryScanner(screen_w=2560, screen_h=1440)

    with tempfile.TemporaryDirectory() as tmpdir:
        img_path = os.path.join(tmpdir, "test_inv.png")
        img = Image.new("RGB", (2560, 1440), (8, 8, 8))  # Nền tối (empty slot ~ 8.0)

        # Giả lập vẽ 5 Waystones sáng màu (AvgRGB ~ 100) tại Slot 0..4
        for col in range(5):
            cx, cy = scanner.get_slot_center(col, 0)
            for dx in range(-15, 16):
                for dy in range(-15, 16):
                    img.putpixel((cx + dx, cy + dy), (120, 110, 90))

        img.save(img_path)

        found = scanner.scan_row1_waystones(img_path)
        assert len(found) == 5
        found_cols = [f[0] for f in found]
        assert found_cols == [0, 1, 2, 3, 4]


def test_optical_inventory_full_detection():
    """Kiểm tra phát hiện túi đồ đầy (is_full == True khi >= 48 slots có đồ)."""
    scanner = OpticalInventoryScanner(screen_w=2560, screen_h=1440)

    with tempfile.TemporaryDirectory() as tmpdir:
        img_path = os.path.join(tmpdir, "full_inv.png")
        img = Image.new("RGB", (2560, 1440), (8, 8, 8))

        # Giả lập túi đồ đầy: 50/60 ô có vật phẩm
        occupied = 0
        for r in range(5):
            for c in range(12):
                if occupied < 50:
                    cx, cy = scanner.get_slot_center(c, r)
                    for dx in range(-10, 11):
                        for dy in range(-10, 11):
                            img.putpixel((cx + dx, cy + dy), (100, 100, 100))
                    occupied += 1

        img.save(img_path)

        res = scanner.scan_full_inventory(img_path)
        assert res["total_slots"] == 60
        assert res["occupied_count"] == 50
        assert res["empty_count"] == 10
        assert res["is_full"] is True


def test_hooded_one_identify_trigger_town_safety():
    """Kiểm tra bất biến INV-NPC-01: Chỉ kích hoạt Identify All tại Town/Hideout và khi không có quái vật."""
    scanner = OpticalInventoryScanner(screen_w=2560, screen_h=1440)

    class MockBridge:
        def __init__(self):
            self.last_call = None

        def send_npc_interact(self, npc_id: int = 0, mode: int = 1, option_idx: int = 0, timeout_ms: int = 15000) -> bool:
            self.last_call = (npc_id, mode, option_idx, timeout_ms)
            return True

    mock_bridge = MockBridge()

    with tempfile.TemporaryDirectory() as tmpdir:
        img_path = os.path.join(tmpdir, "inv.png")
        img = Image.new("RGB", (2560, 1440), (8, 8, 8))
        # 55 ô đầy
        occupied = 0
        for r in range(5):
            for c in range(12):
                if occupied < 55:
                    cx, cy = scanner.get_slot_center(c, r)
                    for dx in range(-10, 11):
                        for dy in range(-10, 11):
                            img.putpixel((cx + dx, cy + dy), (100, 100, 100))
                    occupied += 1
        img.save(img_path)

        # 1. Thất bại nếu đang ở map chiến đấu (không phải Town/Hideout)
        res_combat = scanner.check_and_trigger_hooded_one_identify(
            img_path, mock_bridge, area_name="Map Deserted", monster_count=0
        )
        assert res_combat["triggered"] is False
        assert "khong phai la Town/Hideout" in res_combat["error"]
        assert mock_bridge.last_call is None

        # 2. Thất bại nếu có quái vật (INV-NPC-01)
        res_monsters = scanner.check_and_trigger_hooded_one_identify(
            img_path, mock_bridge, area_name="Shoreline Hideout", monster_count=5
        )
        assert res_monsters["triggered"] is False
        assert "Vi pham INV-NPC-01" in res_monsters["error"]
        assert mock_bridge.last_call is None

        # 3. Thành công tại Shoreline Hideout với 0 quái vật và túi đồ đầy
        res_ok = scanner.check_and_trigger_hooded_one_identify(
            img_path, mock_bridge, area_name="Shoreline Hideout", monster_count=0
        )
        assert res_ok["triggered"] is True
        assert res_ok["is_in_town"] is True
        assert res_ok["is_full"] is True
        assert mock_bridge.last_call == (0, 1, 1, 15000)  # mode 1 = IdentifyAll, option_idx 1


def test_autonomous_lifecycle_hooded_one_integration():
    """Kiểm tra tích hợp Hooded One Identify All vào AutonomousLifecycleManager."""
    class MockBridge:
        def __init__(self):
            self.last_call = None

        def send_npc_interact(self, npc_id: int = 0, mode: int = 1, option_idx: int = 0, timeout_ms: int = 15000) -> bool:
            self.last_call = (npc_id, mode, option_idx, timeout_ms)
            return True

    bridge = MockBridge()
    manager = AutonomousLifecycleManager(core_bridge=bridge)
    manager.current_area_name = "Shoreline Hideout"
    manager.is_in_town = True

    with tempfile.TemporaryDirectory() as tmpdir:
        img_path = os.path.join(tmpdir, "full_inv.png")
        img = Image.new("RGB", (2560, 1440), (8, 8, 8))
        for r in range(5):
            for c in range(12):
                cx, cy = manager.inventory_scanner.get_slot_center(c, r)
                for dx in range(-10, 11):
                    for dy in range(-10, 11):
                        img.putpixel((cx + dx, cy + dy), (120, 120, 120))
        img.save(img_path)

        res = manager.trigger_hooded_one_identify(img_path)
        assert res["triggered"] is True
        assert bridge.last_call == (0, 1, 1, 15000)


def test_optical_inventory_boundary_47_vs_48():
    """Kiểm tra chính xác ngưỡng biên: 47 slots (Chưa đầy, không kích hoạt) vs 48 slots (Đầy, kích hoạt)."""
    scanner = OpticalInventoryScanner(screen_w=2560, screen_h=1440)

    class MockBridge:
        def __init__(self):
            self.last_call = None

        def send_npc_interact(self, npc_id: int = 0, mode: int = 1, option_idx: int = 0, timeout_ms: int = 15000) -> bool:
            self.last_call = (npc_id, mode, option_idx, timeout_ms)
            return True

    mock_bridge = MockBridge()

    with tempfile.TemporaryDirectory() as tmpdir:
        # Trường hợp 1: Đúng 47 ô có đồ (dưới ngưỡng 48)
        img_47_path = os.path.join(tmpdir, "inv_47.png")
        img_47 = Image.new("RGB", (2560, 1440), (8, 8, 8))
        occupied = 0
        for r in range(5):
            for c in range(12):
                if occupied < 47:
                    cx, cy = scanner.get_slot_center(c, r)
                    for dx in range(-10, 11):
                        for dy in range(-10, 11):
                            img_47.putpixel((cx + dx, cy + dy), (120, 120, 120))
                    occupied += 1
        img_47.save(img_47_path)

        res_47 = scanner.scan_full_inventory(img_47_path)
        assert res_47["occupied_count"] == 47
        assert res_47["is_full"] is False

        trigger_47 = scanner.check_and_trigger_hooded_one_identify(
            img_47_path, mock_bridge, area_name="Shoreline Hideout", monster_count=0
        )
        assert trigger_47["triggered"] is False
        assert trigger_47["is_full"] is False
        assert "Tui do chua day (47/60 o)" in trigger_47["error"]
        assert mock_bridge.last_call is None

        # Trường hợp 2: Đúng 48 ô có đồ (đạt ngưỡng 48)
        img_48_path = os.path.join(tmpdir, "inv_48.png")
        img_48 = Image.new("RGB", (2560, 1440), (8, 8, 8))
        occupied = 0
        for r in range(5):
            for c in range(12):
                if occupied < 48:
                    cx, cy = scanner.get_slot_center(c, r)
                    for dx in range(-10, 11):
                        for dy in range(-10, 11):
                            img_48.putpixel((cx + dx, cy + dy), (120, 120, 120))
                    occupied += 1
        img_48.save(img_48_path)

        res_48 = scanner.scan_full_inventory(img_48_path)
        assert res_48["occupied_count"] == 48
        assert res_48["is_full"] is True

        trigger_48 = scanner.check_and_trigger_hooded_one_identify(
            img_48_path, mock_bridge, area_name="Shoreline Hideout", monster_count=0
        )
        assert trigger_48["triggered"] is True
        assert trigger_48["is_full"] is True
        assert mock_bridge.last_call == (0, 1, 1, 15000)


def test_optical_inventory_row0_and_loot_partition():
    """Kiểm tra phân vùng Row 0 (Reserved Zone) và Rows 1-4 (Loot Zone) theo Doc 63 SSoT."""
    scanner = OpticalInventoryScanner(screen_w=2560, screen_h=1440)

    with tempfile.TemporaryDirectory() as tmpdir:
        img_path = os.path.join(tmpdir, "partition_test.png")
        img = Image.new("RGB", (2560, 1440), (8, 8, 8))

        # 1. Vẽ 3 Waystones ở Row 0 (Cột 0, 1, 2)
        for c in range(3):
            cx, cy = scanner.get_slot_center(c, 0)
            for dx in range(-12, 13):
                for dy in range(-12, 13):
                    img.putpixel((cx + dx, cy + dy), (120, 110, 90))

        # 2. Vẽ 45 items ở Rows 1-4 (Loot Zone có 48 ô, vẽ 45 ô -> còn 3 ô trống -> is_loot_full = True)
        loot_occupied = 0
        for r in range(1, 5):
            for c in range(12):
                if loot_occupied < 45:
                    cx, cy = scanner.get_slot_center(c, r)
                    for dx in range(-10, 11):
                        for dy in range(-10, 11):
                            img.putpixel((cx + dx, cy + dy), (100, 100, 100))
                    loot_occupied += 1

        img.save(img_path)

        # Kiểm tra Row 0 status
        row0 = scanner.scan_row0_status(img_path)
        assert row0["total_reserved_slots"] == 12
        assert row0["ready_count"] == 3
        assert row0["empty_count"] == 9
        assert row0["has_waystone"] is True

        # Kiểm tra Loot capacity
        loot_cap = scanner.scan_loot_capacity(img_path)
        assert loot_cap["total_loot_slots"] == 48
        assert loot_cap["occupied_loot_slots"] == 45
        assert loot_cap["empty_loot_slots"] == 3
        assert loot_cap["is_loot_full"] is True
        assert scanner.is_inventory_full(img_path) is True


def test_optical_inventory_scanner_loading_screen_protection():
    """Kiểm tra chống đọc nhầm hình nền artwork Loading Screen thành Waystone / ô túi đồ (INV-LOADING-SCREEN-GATE)."""
    from src.common.vision_ocr import is_loading_screen, find_map_device_screen_pos

    real_loading_img = "captures/pipeline_run/20260918_132025_748_STAGE2_MAP_DEVICE_VIEW.png"
    if not os.path.exists(real_loading_img):
        pytest.skip(f"Khong tim thay tap tin chup loading screen that: {real_loading_img}")

    # 1. Cảm biến quang học WinOCR nhận diện chính xác 100% Loading Screen thật
    assert is_loading_screen(real_loading_img) is True

    scanner = OpticalInventoryScanner(screen_w=1189, screen_h=891)

    # 2. Không nhận diện nhầm artwork thành Waystones ở Hàng 1 (Row 0)
    waystones_row1 = scanner.scan_row1_waystones(real_loading_img)
    assert waystones_row1 == []

    # 3. Không nhận diện nhầm artwork thành Waystones ở bất kỳ ô nào trong 60 ô
    all_waystones = scanner.scan_all_waystones(real_loading_img)
    assert all_waystones == []

    # 4. Quét toàn bộ túi đồ fail-closed an toàn, không tính slot ảo
    inv_data = scanner.scan_full_inventory(real_loading_img)
    assert inv_data["occupied_count"] == 0
    assert inv_data["is_full"] is False
    assert "LOADING_SCREEN_DETECTED" in inv_data["error"]

    # 5. Quét dung lượng Loot fail-closed
    loot_data = scanner.scan_loot_capacity(real_loading_img)
    assert loot_data["occupied_loot_slots"] == 0
    assert loot_data["is_loot_full"] is False
    assert "LOADING_SCREEN_DETECTED" in loot_data["error"]

    # 6. Tuyệt đối không kích hoạt The Hooded One Identify All trên Loading Screen
    trigger_res = scanner.check_and_trigger_hooded_one_identify(
        real_loading_img, None, area_name="Shoreline Hideout", monster_count=0
    )
    assert trigger_res["triggered"] is False
    assert "LOADING_SCREEN_DETECTED" in trigger_res["error"]

    # 7. find_map_device_screen_pos trả về tọa độ fallback an toàn, không tìm cụm màu giả
    pos = find_map_device_screen_pos(real_loading_img)
    assert isinstance(pos, tuple) and len(pos) == 2
    assert pos[0] > 0 and pos[1] > 0


def test_is_loading_screen_fast_path_in_game():
    """Kiểm tra Fast-Path của is_loading_screen trên ảnh in-game thật trả về False < 10ms (Rule 5 Authentic Data)."""
    import time
    from src.common.vision_ocr import is_loading_screen, find_map_device_screen_pos, _load_image

    img_path = "captures/reconnect/RECONNECT_STEP4_IN_GAME.png"
    if not os.path.exists(img_path):
        pytest.skip(f"Khong tim thay artifact: {img_path}")

    # Preload vào memory/cache
    img_obj = _load_image(img_path)
    assert img_obj is not None

    # Đo thời gian thực thi trên Image object (< 10ms, thực tế ~0.1-2.0ms)
    t0 = time.perf_counter()
    is_loading = is_loading_screen(img_obj)
    elapsed_ms = (time.perf_counter() - t0) * 1000.0

    assert is_loading is False, "Ảnh in-game Hideout không được nhận diện nhầm thành Loading Screen!"
    assert elapsed_ms < 10.0, f"Fast-Path quá chậm ({elapsed_ms:.2f}ms >= 10ms)!"

    # Đo thời gian thực thi khi truyền đường dẫn file (sử dụng _IMAGE_CACHE)
    t0_path = time.perf_counter()
    is_loading_path = is_loading_screen(img_path)
    elapsed_path_ms = (time.perf_counter() - t0_path) * 1000.0

    assert is_loading_path is False
    assert elapsed_path_ms < 10.0, f"Fast-Path qua file path quá chậm ({elapsed_path_ms:.2f}ms >= 10ms)!"

    # Kiểm tra nhận diện chính xác nhãn và tọa độ bệ đá Map Device trong Shoreline Hideout
    map_device_img = "captures/pipeline_run/20260918_153346_454_STAGE2_MAP_DEVICE_VIEW.png"
    target_img = map_device_img if os.path.exists(map_device_img) else img_path
    pos = find_map_device_screen_pos(target_img)
    assert pos in ((306, 171), (249, 173), (306, 196)), f"Tọa độ bệ đá Map Device ({pos}) không khớp nhãn thật (306, 171), (249, 173) hoặc (306, 196)!"


def test_endgame_tab_perception_contract():
    """Kiểm tra nhận diện tab ENDGAME trên thanh tab World Map (chuẩn hóa pixel tham chiếu ~65%)."""
    from src.common.vision_ocr import find_endgame_tab_pos

    # 1. Kiểm tra tọa độ tham chiếu chuẩn (Fallback / 0ms)
    pos_1440_ref = find_endgame_tab_pos(None, scale_x=1.0, scale_y=1.0)
    assert pos_1440_ref == (1647, 94), f"Tọa độ tham chiếu 1440p ({pos_1440_ref}) phải là (1647, 94) ~65% thanh tab!"

    # 2. Kiểm tra co giãn sang Windowed mode (1197x897)
    sx_win = 1197.0 / 2560.0
    sy_win = 897.0 / 1440.0
    pos_win_ref = find_endgame_tab_pos(None, scale_x=sx_win, scale_y=sy_win)
    assert pos_win_ref[0] == int(1647 * sx_win) and pos_win_ref[1] == int(94 * sy_win)
    assert 760 <= pos_win_ref[0] <= 780
    assert 50 <= pos_win_ref[1] <= 65

    # 3. Kiểm tra nhận diện WinOCR thực nghiệm trên artifact thật 1440p
    img_real = "captures/20260913_101215_171_ATLAS_MAP_DEVICE_LOADED.png"
    if os.path.exists(img_real):
        detected_pos = find_endgame_tab_pos(img_real)
        # Nằm trong vùng tab ENDGAME ở đỉnh màn hình
        assert 1500 <= detected_pos[0] <= 1700
        assert 70 <= detected_pos[1] <= 120


def test_traverse_button_screen_pos_contract():
    """Kiểm tra nhận diện nút TRAVERSE (kích thước ~150x40 ở panel phụ bản giữa/dưới màn hình)."""
    from src.common.vision_ocr import find_traverse_button_screen_pos

    # 1. Kiểm tra fail-closed khi không có ảnh và return_none_if_missing=True
    assert find_traverse_button_screen_pos(None, return_none_if_missing=True) is None

    # 2. Kiểm tra tọa độ tham chiếu khi return_none_if_missing=False
    ref_pos = find_traverse_button_screen_pos(None, return_none_if_missing=False)
    assert ref_pos == (1092, 748)

    # 3. Kiểm tra trên capture thực địa có nút TRAVERSE
    img_real = "captures/pipeline_run/20260914_212020_423_STAGE3_NODE_SELECTED.png"
    if os.path.exists(img_real):
        pos = find_traverse_button_screen_pos(img_real)
        assert pos is not None, "Phải nhận diện được nút TRAVERSE trên ảnh thực nghiệm!"
        # Tọa độ nút nằm trong panel phụ bản giữa/dưới màn hình (x: 30%-60%, y: 35%-88%)
        assert 768 <= pos[0] <= 1536  # 0.30 * 2560 .. 0.60 * 2560
        assert 504 <= pos[1] <= 1267  # 0.35 * 1440 .. 0.88 * 1440


def test_reachable_green_nodes_inventory_open_bounds():
    """Kiểm tra nhận diện Reachable Green Nodes trên Atlas nửa bên trái khi Inventory mở (x < 650 trên 1197x897)."""
    from src.common.vision_ocr import find_reachable_green_node, find_all_reachable_green_nodes

    img_real = "captures/20260914_055336_887_LIVE_CLEAR_SEARCH_TEST_CORRELATED.png"
    if os.path.exists(img_real):
        # 1. Khi inventory mở ở 1440p (giới hạn x < 2560 * 0.55 = 1408)
        all_nodes = find_all_reachable_green_nodes(img_real, inventory_open=True)
        assert len(all_nodes) > 0
        for n in all_nodes:
            assert n[0] < int(2560 * 0.55), f"Node {n} vượt quá nửa trái màn hình khi mở túi đồ!"

        # 2. Khi giới hạn max_x cụ thể (ví dụ 650 ở Windowed mode)
        best_node = find_reachable_green_node(img_real, inventory_open=True, max_x=650)
        assert best_node is not None
        assert best_node[0] < 650, f"Node {best_node} phải nằm ở nửa trái (x < 650) khi inventory mở!"





