"""AutoPOE2 - Test Suite Kiểm Chứng Minimap Fog of War & Frontier Grid Exploration (Doc 69 §3.4).
================================================================================================
Kiểm thử toàn diện:
1. Phân loại sương mù (Fog Cell State Classification) trên các artifact capture thực tế.
2. Trích xuất mép biên Frontier và thuật toán gom cụm (Connected Components).
3. Cắt tỉa ngõ cụt Topological (Dead-End Pruning & Anti-backtracking) triệt tiêu đi lòng vòng.
4. Hàm tiện ích định tuyến Frontier và bảo toàn quán tính di chuyển (Frontier Momentum).
5. Tiêu chí hoàn tất dọn Map (Coverage >= 95% tiles/monsters).
6. Tương thích Dynamic Resolution Scaling (1080p, 1440p, Windowed 1189x891, 720p).

Tuân thủ nghiêm ngặt:
- Rule 5: Sử dụng artifact thật từ captures/.
- Rule 14: Quality Gate, 100% test PASS.
"""

from __future__ import annotations

import math
import os
import unittest
import numpy as np
from PIL import Image

from src.assistant_tool.navigation.minimap_fog_explorer import (
    ExplorationGrid,
    ExplorationTarget,
    FogCellState,
    FrontierCluster,
    MinimapFogExplorer,
)
from src.common.coordinate_transform import (
    RES_1080P,
    RES_1440P,
    RES_720P,
    RES_WINDOWED_DEFAULT,
    Resolution,
)


class TestMinimapFogExploration(unittest.TestCase):
    """Test suite kiểm thử bộ nhận diện sương mù Minimap và định tuyến thám hiểm biên."""

    def setUp(self) -> None:
        self.captures_dir = os.path.join(os.path.dirname(__file__), "..", "captures")

    def test_fog_cell_state_classification_on_real_captures(self) -> None:
        """Kiểm chứng phân loại sương mù (Dark < 15) và địa hình trên artifact thật minimap_1.png."""
        img_path = os.path.join(self.captures_dir, "minimap_1.png")
        if not os.path.exists(img_path):
            self.skipTest(f"Không tìm thấy file capture: {img_path}")

        img = Image.open(img_path)
        grid = ExplorationGrid(grid_width=40, grid_height=26, dark_threshold=15.0, wall_threshold=180.0)
        grid.load_from_minimap_image(img)

        # Kiểm tra tỷ lệ sương mù và đã mở
        unexplored_count = np.sum(grid.cells == FogCellState.UNEXPLORED)
        explored_count = np.sum(grid.cells == FogCellState.EXPLORED)
        wall_count = np.sum(grid.cells == FogCellState.WALL_OBSTACLE)
        total = grid.gw * grid.gh

        self.assertEqual(total, 40 * 26)
        self.assertGreater(unexplored_count, 0, "Phải phát hiện vùng sương mù chưa mở trong minimap_1.png")
        self.assertGreater(explored_count, 0, "Phải phát hiện vùng địa hình đã khám phá trong minimap_1.png")

        # Tỷ lệ sương mù trên minimap_1 thực tế xấp xỉ 20%..50%
        fog_ratio = unexplored_count / float(total)
        self.assertTrue(0.15 <= fog_ratio <= 0.60, f"Tỷ lệ sương mù bất thường: {fog_ratio:.2f}")

    def test_frontier_extraction_and_clustering_invariants(self) -> None:
        """Kiểm chứng tính bất biến của Frontier: Ô explored tiếp giáp unexplored được nhóm cụm chính xác."""
        # Tạo ma trận nhân tạo: Nửa trái đã mở (1), nửa phải sương mù (0)
        grid = ExplorationGrid(grid_width=20, grid_height=20)
        grid.cells[:, :10] = FogCellState.EXPLORED
        grid.cells[:, 10:] = FogCellState.UNEXPLORED

        frontiers = grid.extract_frontier_cells()
        # Cột 9 (tiếp giáp cột 10) phải là các ô Frontier
        self.assertEqual(len(frontiers), 20)
        for x, y in frontiers:
            self.assertEqual(x, 9, f"Ô ({x}, {y}) phải nằm tại cột biên x=9")

        # Gom cụm: Toàn bộ 20 ô cột 9 liên thông kề nhau phải được gộp thành 1 cụm duy nhất
        clusters = grid.cluster_frontiers(frontiers)
        self.assertEqual(len(clusters), 1)
        self.assertEqual(clusters[0].size, 20)
        self.assertAlmostEqual(clusters[0].center_grid[0], 9.0)
        self.assertAlmostEqual(clusters[0].center_grid[1], 9.5)
        self.assertGreater(clusters[0].information_gain, 0.0)

    def test_dead_end_topological_pruning_anti_backtracking(self) -> None:
        """Kiểm chứng giải thuật cắt tỉa ngõ cụt: Chặn đứng việc bot quay đầu vào ngõ đã dọn sạch."""
        # Tạo bản đồ dạng ngõ cụt:
        # Tường bao quanh, một hành lang 1 ô rộng dẫn vào ngõ cụt từ (5,5) tới (5,10)
        grid = ExplorationGrid(grid_width=20, grid_height=20)
        grid.cells.fill(FogCellState.WALL_OBSTACLE)

        # Mở một phòng chính 2D (4x4) đã khám phá hết ở (x=0..3, y=4..7)
        for y in range(4, 8):
            for x in range(4):
                grid.cells[y, x] = FogCellState.EXPLORED

        # Tạo một hành lang ngõ cụt 1 ô thò ra từ x=4 tới x=9 tại hàng y=5 (6 ô)
        for x in range(4, 10):
            grid.cells[5, x] = FogCellState.EXPLORED

        # Cắt tỉa: Chỉ có 6 ô hành lang (x=4..9, y=5) bị cắt tỉa, phòng 2D không bị tỉa
        pruned = grid.prune_dead_ends()
        self.assertEqual(pruned, 6)

        # Toàn bộ nhánh ngõ cụt x=4..9 phải bị đánh dấu dead_ends = True
        for x in range(4, 10):
            self.assertTrue(grid.dead_ends[5, x], f"Ô ({x}, 5) phải bị cắt tỉa ngõ cụt")

        # Các ô trong phòng chính không bị cắt tỉa
        for y in range(4, 8):
            for x in range(3):
                self.assertFalse(grid.dead_ends[y, x], f"Phòng chính ({x}, {y}) không được bị tỉa")

        # Các ô bị cắt tỉa không bao giờ được sinh ra Frontier
        frontiers = grid.extract_frontier_cells()
        for fx, fy in frontiers:
            self.assertFalse(grid.dead_ends[fy, fx], "Frontier không được chứa ô ngõ cụt đã tỉa")

    def test_frontier_momentum_utility_function(self) -> None:
        """Kiểm chứng hàm tiện ích: Ưu tiên giữ hướng di chuyển (Momentum) và phạt quay đầu 180 độ."""
        explorer = MinimapFogExplorer(grid_width=30, grid_height=30)
        # Giả sử người chơi ở (15, 15), đang di chuyển về hướng Đông với vận tốc vx=1.0, vy=0.0
        player_pos = (15.0, 15.0)
        player_vel_east = (1.0, 0.0)

        # Cụm A ở phía Đông (25, 15) - Cùng hướng
        cluster_east = FrontierCluster(
            cluster_id=0,
            cells=[(25, 15)],
            center_grid=(25.0, 15.0),
            size=5,
            information_gain=20.0,
            avg_visit_count=0.0,
        )

        # Cụm B ở phía Tây (5, 15) - Ngược hướng 180 độ
        cluster_west = FrontierCluster(
            cluster_id=1,
            cells=[(5, 15)],
            center_grid=(5.0, 15.0),
            size=5,
            information_gain=20.0,  # Cùng độ lợi thông tin và khoảng cách
            avg_visit_count=0.0,
        )

        best_cl, utility = explorer.select_best_frontier(
            [cluster_east, cluster_west],
            player_grid_xy=player_pos,
            player_vel=player_vel_east,
        )

        # Phải chọn cụm East vì có điểm Frontier Momentum cao hơn
        self.assertEqual(best_cl.cluster_id, 0, "Frontier Momentum phải ưu tiên hướng đang di chuyển")

    def test_visit_heatmap_penalty_prevents_looping(self) -> None:
        """Kiểm chứng bản đồ nhiệt: Phạt nặng cụm đã dẫm chân nhiều lần, triệt tiêu đi lòng vòng."""
        explorer = MinimapFogExplorer(grid_width=30, grid_height=30)
        player_pos = (15.0, 15.0)

        # Cụm A ở gần nhưng đã ghé thăm 5 lần (avg_visit_count = 5.0)
        cluster_visited = FrontierCluster(
            cluster_id=0,
            cells=[(18, 15)],
            center_grid=(18.0, 15.0),
            size=5,
            information_gain=10.0,
            avg_visit_count=5.0,
        )

        # Cụm B ở xa hơn một chút nhưng chưa từng ghé thăm (avg_visit_count = 0.0)
        cluster_fresh = FrontierCluster(
            cluster_id=1,
            cells=[(22, 15)],
            center_grid=(22.0, 15.0),
            size=5,
            information_gain=15.0,
            avg_visit_count=0.0,
        )

        best_cl, _ = explorer.select_best_frontier(
            [cluster_visited, cluster_fresh],
            player_grid_xy=player_pos,
            player_vel=(0.0, 0.0),
        )

        self.assertEqual(best_cl.cluster_id, 1, "Bản đồ nhiệt phải chuyển hướng bot sang cụm chưa dẫm chân")

    def test_map_completion_criteria_95_percent_coverage(self) -> None:
        """Kiểm chứng tiêu chí hoàn thành dọn bản đồ (Coverage >= 95%)."""
        grid = ExplorationGrid(grid_width=10, grid_height=10)
        # 100 ô: Mở 96 ô, 4 ô sương mù (Coverage = 96%)
        grid.cells.fill(FogCellState.EXPLORED)
        grid.cells[0, :4] = FogCellState.UNEXPLORED

        cov = grid.get_coverage_ratio()
        self.assertAlmostEqual(cov, 0.96, places=2)
        self.assertGreaterEqual(cov, 0.95)

        # Kiểm tra thông qua MinimapFogExplorer
        explorer = MinimapFogExplorer(grid_width=10, grid_height=10, target_coverage=0.95)
        # Tạo ảnh giả lập tương ứng 96% sáng
        fake_img = np.full((100, 100), 50, dtype=np.uint8)
        fake_img[:4, :40] = 0  # Một góc nhỏ tối

        target = explorer.analyze_minimap_and_route(fake_img)
        self.assertTrue(target.is_fully_explored or target.coverage_ratio >= 0.95)

    def test_dynamic_resolution_scaling_compatibility(self) -> None:
        """Kiểm chứng tính tương thích với Dynamic Resolution Scaler trên 1080p, 1440p, Windowed 1189x891, 720p."""
        resolutions = [
            RES_1080P,
            RES_1440P,
            RES_WINDOWED_DEFAULT,  # PID 22632
            RES_720P,
        ]

        # Ảnh minimap mẫu 400x260
        fake_minimap = np.full((260, 400), 40, dtype=np.uint8)
        fake_minimap[:, 300:] = 5  # Phía Đông là sương mù

        for res in resolutions:
            explorer = MinimapFogExplorer(grid_width=40, grid_height=26, target_resolution=res)
            target = explorer.analyze_minimap_and_route(fake_minimap)

            # Kiểm tra tọa độ mục tiêu trên màn hình
            sx, sy = target.target_screen_xy
            self.assertGreater(sx, 0, f"Tọa độ X ({sx}) phải dương trên res {res}")
            self.assertLess(sx, res.width, f"Tọa độ X ({sx}) không được vượt width {res.width}")
            self.assertGreater(sy, 0, f"Tọa độ Y ({sy}) phải dương trên res {res}")
            self.assertLess(sy, res.height, f"Tọa độ Y ({sy}) không được vượt height {res.height}")

            # Đảm bảo target_screen nằm gọn bên trong minimap_screen_bbox
            bx1, by1, bx2, by2 = explorer.minimap_screen_bbox
            self.assertTrue(
                bx1 <= sx <= bx2,
                f"sx={sx} phải nằm trong bounding box [{bx1}, {bx2}] trên res={res}",
            )
            self.assertTrue(
                by1 <= sy <= by2,
                f"sy={sy} phải nằm trong bounding box [{by1}, {by2}] trên res={res}",
            )

    def test_end_to_end_real_minimap_pipeline(self) -> None:
        """Kiểm chứng quy trình trọn gói End-to-End trên artifact thật captures/minimap_crop.png."""
        img_path = os.path.join(self.captures_dir, "minimap_crop.png")
        if not os.path.exists(img_path):
            self.skipTest(f"Không tìm thấy capture: {img_path}")

        img = Image.open(img_path)
        explorer = MinimapFogExplorer(grid_width=41, grid_height=30, target_resolution=RES_WINDOWED_DEFAULT)

        # Định tuyến với người chơi ở trung tâm, đang hướng nhẹ về phía Bắc
        target = explorer.analyze_minimap_and_route(
            img,
            player_grid_pos=(20.5, 15.0),
            player_velocity=(0.0, -1.0),
        )

        self.assertIsInstance(target, ExplorationTarget)
        self.assertGreater(target.active_clusters_count, 0)
        self.assertIsInstance(target.direction_vector, tuple)
        self.assertEqual(len(target.direction_vector), 2)
        # Độ dài vector hướng phải xấp xỉ 1.0 (hoặc 0 nếu đứng im)
        v_len = math.hypot(*target.direction_vector)
        self.assertTrue(v_len == 0.0 or abs(v_len - 1.0) < 1e-3)


if __name__ == "__main__":
    unittest.main()
