"""Bộ kiểm thử đơn vị cho Dual-Pass CLAHE & Robust Perception (Doc 48/56/61).

Xác minh:
1. Thuật toán CLAHE và Sobel Edge thuần NumPy tăng cường tương phản và triệt tiêu bloom ánh sáng.
2. Nhận diện nút TRAVERSE đạt độ chính xác 100% với sai số <= 3 pixel so với ground truth (1091, 747).
3. Kháng 100% false-positives trên các ảnh chụp sàn 3D Hideout khi chưa mở giao diện Atlas.
4. Xác minh trạng thái mở của giao diện Map Device (is_atlas_ui_open_robust) trên toàn bộ capture thực tế.
5. Kiểm chứng khả năng kháng nhiễu ánh sáng vàng/cam giả lập (Simulated Amber Bloom).
"""

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

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from src.common.vision_filters import (
    apply_clahe_fast,
    dual_pass_enhance_image,
    find_traverse_button_dual_pass,
    is_atlas_ui_open_robust,
    match_traverse_template_edge,
    sobel_edge_magnitude,
)
from src.common.vision_ocr import is_atlas_ui_open, find_button_by_text


class TestDualPassPerception(unittest.TestCase):
    """Kiểm thử tầng thị giác máy tính Dual-Pass CLAHE & Edge Template Matching."""

    @classmethod
    def setUpClass(cls):
        cls.repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
        cls.template_path = os.path.join(cls.repo_root, "captures", "traverse_button_crop.png")

    def _get_path(self, rel_path: str) -> str:
        return os.path.join(self.repo_root, rel_path)

    def test_clahe_and_edge_filters_math(self):
        """1. Kiểm chứng tính toán lý của CLAHE và Sobel gradient trên ma trận số thực nghiệm."""
        # Tạo ảnh thử nghiệm có gradient và vùng cháy sáng (bloom)
        synth = np.zeros((100, 200), dtype=np.uint8)
        synth[:, :100] = 30   # Nền tối
        synth[:, 100:] = 240  # Vùng chói sáng
        # Thêm 1 vạch chữ mờ ở vùng chói
        synth[40:60, 140:160] = 200

        # Áp dụng CLAHE
        enhanced = apply_clahe_fast(synth, grid_size=(4, 4), clip_limit=2.0)
        self.assertEqual(enhanced.shape, (100, 200))
        self.assertEqual(enhanced.dtype, np.uint8)

        # Biên độ tương phản tại vùng chữ mờ phải được kéo giãn rõ nét
        contrast_before = int(synth[50, 120]) - int(synth[50, 150])  # 240 - 200 = 40
        contrast_after = abs(int(enhanced[50, 120]) - int(enhanced[50, 150]))
        self.assertGreaterEqual(contrast_after, contrast_before)

        # Kiểm tra Sobel Edge
        edges = sobel_edge_magnitude(enhanced)
        self.assertEqual(edges.shape, (100, 200))
        # Cạnh bước nhảy tại x=100 phải có giá trị gradient lớn
        self.assertGreater(int(edges[50, 99]), 100)

    def test_traverse_button_ground_truth_accuracy(self):
        """2. Xác minh nút TRAVERSE đạt độ chính xác 100% với sai số <= 3 pixel so với ground truth (1091, 747)."""
        gt_x, gt_y = 1091, 747

        test_files = [
            "captures/20260913_102100_639_AFTER_CLICK_ATLAS_NODE.png",
            "captures/20260913_102457_287_AFTER_CLICK_TRAVERSE.png",
        ]

        for rel_p in test_files:
            full_p = self._get_path(rel_p)
            if not os.path.exists(full_p):
                continue

            with self.subTest(file=rel_p):
                # 1. Kiểm tra qua Dual-Pass Locator
                pos = find_traverse_button_dual_pass(full_p, template_path=self.template_path)
                self.assertIsNotNone(pos, f"Không phát hiện được nút TRAVERSE trong {rel_p}")
                assert pos is not None
                det_x, det_y = pos
                err_x = abs(det_x - gt_x)
                err_y = abs(det_y - gt_y)

                self.assertLessEqual(
                    err_x, 3, f"Sai số trục X {err_x}px vượt quá ngưỡng 3px tại {rel_p} (detected: {pos}, GT: ({gt_x}, {gt_y}))"
                )
                self.assertLessEqual(
                    err_y, 3, f"Sai số trục Y {err_y}px vượt quá ngưỡng 3px tại {rel_p} (detected: {pos}, GT: ({gt_x}, {gt_y}))"
                )

                # 2. Kiểm tra qua find_button_by_text tích hợp
                pos_text = find_button_by_text(full_p, "TRAVERSE")
                self.assertIsNotNone(pos_text, f"find_button_by_text thất bại tại {rel_p}")
                assert pos_text is not None
                self.assertLessEqual(abs(pos_text[0] - gt_x), 3)
                self.assertLessEqual(abs(pos_text[1] - gt_y), 3)

    def test_traverse_button_zero_false_positives_on_negative_captures(self):
        """3. Kháng 100% false-positives trên các ảnh chụp sàn Hideout 3D khi chưa mở giao diện Atlas."""
        neg_captures = [
            "captures/current_map_device.png",
            "captures/after_click_map_device.png",
            "captures/hideout_map_device_check.png",
            "captures/pipeline_run/20260914_213227_453_STAGE2_MAP_DEVICE_OPENED.png",
            "captures/pipeline_run/20260914_213230_352_STAGE3_NODE_SELECTED.png",
            "captures/pipeline_run/20260914_213232_001_STAGE3_NODE_READY.png",
            "captures/crop_failure_traverse.png",
        ]

        for rel_p in neg_captures:
            full_p = self._get_path(rel_p)
            if not os.path.exists(full_p):
                continue

            with self.subTest(file=rel_p):
                pos = find_traverse_button_dual_pass(full_p, template_path=self.template_path)
                self.assertIsNone(
                    pos,
                    f"Bị False-Positive! Nút TRAVERSE không được phép xuất hiện trong ảnh âm tính {rel_p} (nhận diện: {pos})",
                )

    def test_atlas_ui_open_verification_on_real_captures(self):
        """4. Xác minh trạng thái mở giao diện Atlas / Map Device (is_atlas_ui_open_robust) trên artifact thật."""
        # Tập ảnh dương tính (Atlas UI thực sự mở)
        pos_ui_captures = [
            "captures/pipeline_run/20260913_215721_622_STAGE2_MAP_DEVICE_OPENED.png",
            "captures/pipeline_run/20260913_220052_769_STAGE2_MAP_DEVICE_OPENED.png",
            "captures/pipeline_run/20260914_004936_950_STAGE2_MAP_DEVICE_OPENED.png",
            "captures/20260913_102100_639_AFTER_CLICK_ATLAS_NODE.png",
            "captures/20260913_102457_287_AFTER_CLICK_TRAVERSE.png",
        ]
        for rel_p in pos_ui_captures:
            full_p = self._get_path(rel_p)
            if not os.path.exists(full_p):
                continue
            with self.subTest(file=rel_p, expected=True):
                self.assertTrue(
                    is_atlas_ui_open_robust(full_p),
                    f"Giao diện Atlas thực sự mở nhưng is_atlas_ui_open_robust báo False tại {rel_p}",
                )

        # Tập ảnh âm tính (Sàn 3D Hideout, chưa mở Atlas UI)
        neg_ui_captures = [
            "captures/current_map_device.png",
            "captures/after_click_map_device.png",
            "captures/pipeline_run/20260914_213227_453_STAGE2_MAP_DEVICE_OPENED.png",
            "captures/pipeline_run/20260914_213230_352_STAGE3_NODE_SELECTED.png",
            "captures/pipeline_run/20260914_213232_001_STAGE3_NODE_READY.png",
        ]
        for rel_p in neg_ui_captures:
            full_p = self._get_path(rel_p)
            if not os.path.exists(full_p):
                continue
            with self.subTest(file=rel_p, expected=False):
                self.assertFalse(
                    is_atlas_ui_open_robust(full_p),
                    f"Giao diện Atlas chưa mở (sàn 3D Hideout) nhưng is_atlas_ui_open_robust báo True tại {rel_p}",
                )

    def test_resilience_to_simulated_amber_bloom(self):
        """5. Kiểm chứng khả năng kháng nhiễu ánh sáng vàng/cam giả lập (Amber Bloom & Flicker)."""
        base_path = self._get_path("captures/20260913_102100_639_AFTER_CLICK_ATLAS_NODE.png")
        if not os.path.exists(base_path):
            self.skipTest("Thiếu capture 20260913_102100_639")

        im = Image.open(base_path).convert("RGB")
        arr = np.array(im, dtype=np.float32)

        # Giả lập bloom ánh sáng cam cực mạnh quanh nút TRAVERSE (x: 1000..1200, y: 720..770)
        # Tăng thêm 80 đơn vị kênh Đỏ và 60 đơn vị kênh Lục (vàng cam chói lóa)
        arr[720:770, 1000:1200, 0] = np.clip(arr[720:770, 1000:1200, 0] + 80.0, 0, 255)
        arr[720:770, 1000:1200, 1] = np.clip(arr[720:770, 1000:1200, 1] + 60.0, 0, 255)

        # Thêm nhiễu ngẫu nhiên giả lập hạt lửa nhấp nháy
        noise = np.random.normal(0, 15, arr[720:770, 1000:1200].shape)
        arr[720:770, 1000:1200] = np.clip(arr[720:770, 1000:1200] + noise, 0, 255)

        bloomed_img = Image.fromarray(arr.astype(np.uint8))

        # Nhận diện qua Dual-Pass Edge Locator
        coords, score = match_traverse_template_edge(bloomed_img, Image.open(self.template_path))
        self.assertIsNotNone(coords)
        assert coords is not None
        self.assertGreaterEqual(score, 0.55, f"Điểm tương quan {score} không đạt ngưỡng sau bloom")
        self.assertLessEqual(abs(coords[0] - 1091), 3)
        self.assertLessEqual(abs(coords[1] - 747), 3)


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