"""AutoPOE2 - Test Suite Kiểm Chứng Minimap Camera Calibrator (Doc 69 §3.4 / Stage 5).
=====================================================================================
Kiểm thử toàn diện:
1. Bất biến toán học ma trận xoay 2D (Orthogonality, Inversion, Determinant = 1.0).
2. Kiểm chứng bù trừ góc xoay vector (Rotation compensation) trên các góc (0, 45, 90, 180, -45 độ).
3. Nhận diện Zoom level từ chuỗi ký tự OCR banner trên HUD client thực tế.
4. Ước lượng góc xoay bản đồ Isometric từ phổ gradient cạnh Sobel.
5. Bất biến Dwell Time Scaling (INV-WASD-MIN-DWELL >= 0.30s) trên dải zoom rộng (0.2x .. 32.0x).
6. Tương thích Dynamic Resolution Scaler (1080p, 1440p, Windowed 1189x891 PID 22632, 720p).
7. Tích hợp liền mạch với MinimapNavigationAdapter tự động bù trừ hướng phím WASD.
8. Kiểm thử trên các artifact ảnh thật từ captures/ (test_after_minimap_zoom.png, test_minimap_scale_05.png).

Tuân thủ nghiêm ngặt:
- Rule 5: Sử dụng artifact thật từ captures/.
- Rule 14: Quality Gate, 100% tests PASS.
- Bất biến: INV-WASD-MIN-DWELL, INV-SCALE-IDEMPOTENT-1PX, INV-NAV-FRONTIER-MOMENTUM.
"""

from __future__ import annotations

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

from src.assistant_tool.navigation.minimap_camera_calibrator import (
    CameraMinimapProfile,
    MinimapCameraCalibrator,
)
from src.assistant_tool.navigation.minimap_navigation_adapter import (
    MinimapNavigationAdapter,
    ProjectionMode,
    WasdCommand,
    WasdDirection,
)
from src.common.coordinate_transform import (
    RES_1080P,
    RES_1440P,
    RES_720P,
    RES_WINDOWED_DEFAULT,
    Resolution,
)


class TestMinimapCameraCalibrator(unittest.TestCase):
    """Test suite kiểm thử MinimapCameraCalibrator."""

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

    def test_rotation_matrix_orthogonality_and_inversion(self) -> None:
        """Kiểm chứng bất biến toán học của ma trận xoay: Det=1, R^T * R = I, R(theta)*R(-theta)=I."""
        for angle in [0.0, 30.0, 45.0, 90.0, 135.0, 180.0, 270.0, -45.0]:
            calibrator = MinimapCameraCalibrator(base_zoom=1.0, base_orientation_deg=angle)
            rot = calibrator._rot_matrix
            inv_rot = calibrator._inv_rot_matrix

            # 1. Định thức bằng 1.0 (không làm biến dạng diện tích)
            det = np.linalg.det(rot)
            self.assertAlmostEqual(det, 1.0, places=5, msg=f"Det != 1.0 at {angle} deg")

            # 2. Tính trực giao: R * R^T = I
            identity = np.eye(2)
            prod = np.dot(rot, rot.T)
            np.testing.assert_allclose(prod, identity, atol=1e-6, err_msg=f"R*R^T != I at {angle} deg")

            # 3. Nghịch đảo ma trận xoay: R(theta) * InvR(theta) = I
            inv_prod = np.dot(rot, inv_rot)
            np.testing.assert_allclose(inv_prod, identity, atol=1e-6, err_msg=f"R*InvR != I at {angle} deg")

    def test_vector_transformation_and_orientation_compensation(self) -> None:
        """Kiểm chứng bù trừ góc xoay: Vector màn hình được xoay ngược một góc orientation."""
        # Trường hợp 1: Không xoay (orientation = 0.0) -> Vector giữ nguyên
        calibrator_0 = MinimapCameraCalibrator(base_zoom=1.0, base_orientation_deg=0.0)
        vx, vy = calibrator_0.transform_vector(1.0, 0.0)
        self.assertAlmostEqual(vx, 1.0, places=4)
        self.assertAlmostEqual(vy, 0.0, places=4)

        # Trường hợp 2: Camera xoay 90 độ theo chiều kim đồng hồ
        # Vector trên Minimap chỉ về phía Đông (1.0, 0.0). Để bù trừ xoay 90 deg thuận,
        # vector thế giới cần xoay 90 deg ngược chiều kim đồng hồ -> hướng Bắc (0.0, -1.0)
        calibrator_90 = MinimapCameraCalibrator(base_zoom=1.0, base_orientation_deg=90.0)
        vx90, vy90 = calibrator_90.transform_vector(1.0, 0.0)
        self.assertAlmostEqual(vx90, 0.0, places=4)
        self.assertAlmostEqual(vy90, -1.0, places=4)

        # Trường hợp 3: Camera xoay 180 độ -> Đảo ngược vector
        calibrator_180 = MinimapCameraCalibrator(base_zoom=1.0, base_orientation_deg=180.0)
        vx180, vy180 = calibrator_180.transform_vector(1.0, 0.0)
        self.assertAlmostEqual(vx180, -1.0, places=4)
        self.assertAlmostEqual(vy180, 0.0, places=4)

        # Trường hợp 4: Vector gốc = 0 -> Trả về (0.0, 0.0)
        vx_zero, vy_zero = calibrator_90.transform_vector(0.0, 0.0)
        self.assertEqual((vx_zero, vy_zero), (0.0, 0.0))

    def test_detect_zoom_from_banner_text(self) -> None:
        """Kiểm chứng trích xuất zoom level từ các chuỗi banner OCR thực tế."""
        calibrator = MinimapCameraCalibrator()

        # Banner chuẩn từ POE2 visual tool
        test_cases = [
            ("Visual Tool | Zoom 1.0x/8.0x | Fog OFF", 1.0),
            ("[ZOOM] Delta +0.3x Zoom 8.0x", 8.0),  # Lấy zoom thực tế 8.0x, không lấy delta +0.3x
            ("Camera Zoom 1.4X active", 1.4),
            ("Minimap Zoom x2.5 Enabled", 2.5),
            ("Minimap Zoom In", None),
            ("No zoom banner here", None),
            ("", None),
        ]

        for text_input, expected_zoom in test_cases:
            detected = calibrator.detect_zoom_from_banner_text(text_input)
            if expected_zoom is None:
                self.assertIsNone(detected, f"Kỳ vọng None cho '{text_input}' nhưng nhận {detected}")
            else:
                self.assertIsNotNone(detected, f"Không tìm thấy zoom trong '{text_input}'")
                self.assertAlmostEqual(detected, expected_zoom, places=2)

    def test_dwell_time_scaling_and_min_dwell_invariant(self) -> None:
        """Kiểm chứng bất biến INV-WASD-MIN-DWELL >= 0.30s trên mọi mức zoom."""
        base_dwell = 0.40  # Mặc định 400ms

        calibrator = MinimapCameraCalibrator()

        # Zoom bình thường (1.0x) -> dwell time giữ nguyên
        calibrator.set_calibration(zoom_level=1.0)
        d_10 = calibrator.adjust_dwell_time(base_dwell)
        self.assertAlmostEqual(d_10, base_dwell, places=3)
        self.assertGreaterEqual(d_10, 0.30)

        # Zoom phóng to (4.0x) -> sqrt(4) = 2.0 -> dwell = 0.40 / 2.0 = 0.20 -> clamp về min 0.30s
        calibrator.set_calibration(zoom_level=4.0)
        d_40 = calibrator.adjust_dwell_time(base_dwell)
        self.assertAlmostEqual(d_40, 0.30, places=3)
        self.assertGreaterEqual(d_40, 0.30, "Vi phạm bất biến INV-WASD-MIN-DWELL!")

        # Zoom cực đại (8.0x và 32.0x) -> luôn được kẹp ở sàn tối thiểu 0.30s
        for zoom in [8.0, 16.0, 32.0]:
            calibrator.set_calibration(zoom_level=zoom)
            d_high = calibrator.adjust_dwell_time(base_dwell)
            self.assertGreaterEqual(d_high, 0.30, f"Zoom {zoom}x vi phạm INV-WASD-MIN-DWELL: {d_high}s")

        # Zoom thu nhỏ (0.5x) -> dwell kéo dài để đi hết cự ly nhưng <= max_dwell
        calibrator.set_calibration(zoom_level=0.5)
        d_05 = calibrator.adjust_dwell_time(base_dwell)
        self.assertGreater(d_05, base_dwell)
        self.assertLessEqual(d_05, 1.50)

    def test_estimate_orientation_from_edges_synthetic(self) -> None:
        """Kiểm chứng thuật toán ước lượng góc nghiêng từ cạnh bản đồ giả lập."""
        calibrator = MinimapCameraCalibrator()

        # Tạo ảnh giả lập có các đường chéo 45 độ (chuẩn Isometric)
        synthetic_img = np.zeros((100, 100), dtype=np.uint8)
        for i in range(10, 90):
            synthetic_img[i, i] = 255
            synthetic_img[min(99, i + 1), i] = 200

        delta_angle = calibrator.estimate_orientation_from_minimap_edges(synthetic_img)
        # Độ lệch so với 45 độ chuẩn phải xấp xỉ 0
        self.assertLess(abs(delta_angle), 15.0)

        # Kiểm chứng với ảnh đồng nhất (phẳng) -> giữ nguyên orientation mặc định
        blank_img = np.zeros((100, 100), dtype=np.uint8)
        delta_blank = calibrator.estimate_orientation_from_minimap_edges(blank_img)
        self.assertAlmostEqual(delta_blank, calibrator.orientation_deg)

    def test_resolution_scaler_integration_all_modes(self) -> None:
        """Kiểm chứng khả năng tương thích với Dynamic Resolution Scaler."""
        resolutions = [
            RES_1080P,
            RES_1440P,
            RES_WINDOWED_DEFAULT,
            RES_720P,
        ]

        for res in resolutions:
            calibrator = MinimapCameraCalibrator(
                base_zoom=1.5,
                base_orientation_deg=30.0,
                target_resolution=res,
            )
            profile = calibrator.get_profile()
            self.assertAlmostEqual(profile.zoom_level, 1.5)
            self.assertAlmostEqual(profile.orientation_deg, 30.0)
            self.assertGreater(profile.scale_factor_x, 0.0)
            self.assertGreater(profile.scale_factor_y, 0.0)

    def test_adapter_integration_with_calibrator(self) -> None:
        """Kiểm chứng tích hợp calibrator trực tiếp vào MinimapNavigationAdapter."""
        calibrator = MinimapCameraCalibrator(base_zoom=2.0, base_orientation_deg=90.0)
        adapter = MinimapNavigationAdapter(
            target_resolution=RES_1080P,
            calibrator=calibrator,
            default_dwell_sec=0.50,
        )

        # 1. Kiểm tra auto_calibrate_from_hud
        success = adapter.auto_calibrate_from_hud("Camera Zoom 4.0x Online")
        self.assertTrue(success)
        self.assertAlmostEqual(adapter.calibrator.zoom_level, 4.0)

        # 2. Kiểm tra set_camera_calibration
        adapter.set_camera_calibration(zoom_level=1.0, orientation_deg=90.0)
        self.assertAlmostEqual(adapter.calibrator.zoom_level, 1.0)
        self.assertAlmostEqual(adapter.calibrator.orientation_deg, 90.0)

        # 3. Tạo ảnh giả lập minimap và chạy compute_navigation_step
        # Khi orientation = 90 deg, hướng vector được bù trừ xoay ngược 90 deg
        dummy_minimap = np.zeros((120, 120, 3), dtype=np.uint8)
        # Giả lập vùng fog chưa mở ở nửa bên phải (East)
        dummy_minimap[:, 60:, :] = 40

        cmd: WasdCommand = adapter.compute_navigation_step(dummy_minimap)
        self.assertIsInstance(cmd, WasdCommand)
        if cmd.direction != WasdDirection.NONE:
            # Dwell time phải luôn tuân thủ INV-WASD-MIN-DWELL
            self.assertGreaterEqual(cmd.dwell_sec, 0.30)
            self.assertIsInstance(cmd.direction, WasdDirection)

    def test_real_capture_artifacts(self) -> None:
        """Kiểm thử end-to-end với các file capture thật từ captures/."""
        real_files = [
            "test_after_minimap_zoom.png",
            "test_minimap_scale_05.png",
            "minimap_zoom.png",
        ]

        adapter = MinimapNavigationAdapter(
            target_resolution=RES_WINDOWED_DEFAULT,
            calibrator=MinimapCameraCalibrator(base_zoom=1.0, base_orientation_deg=0.0),
            default_dwell_sec=0.35,
        )

        tested_count = 0
        for fname in real_files:
            fpath = os.path.join(self.captures_dir, fname)
            if not os.path.exists(fpath):
                continue

            img = Image.open(fpath)
            cmd = adapter.compute_navigation_step(img)

            self.assertIsInstance(cmd, WasdCommand)
            if cmd.direction != WasdDirection.NONE:
                self.assertGreaterEqual(cmd.dwell_sec, 0.30)
            tested_count += 1

        self.assertGreater(tested_count, 0, "Không tìm thấy ảnh capture thật nào trong captures/!")


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