import io
import json
import os
import struct
from pathlib import Path

import pytest

_DETECT_SLOW_TIMEOUT = 120
_MEDIA = os.environ.get("MEDIA_DIR", "/media")
_EPS = 1e-6
_WEATHER_CLASS_STRIDE = 20


def _image_width_height(data):
    if len(data) >= 24 and data[:8] == b"\x89PNG\r\n\x1a\n":
        w, h = struct.unpack(">II", data[16:24])
        return w, h
    if len(data) >= 2 and data[:2] == b"\xff\xd8":
        i = 2
        while i + 1 < len(data):
            if data[i] != 0xFF:
                i += 1
                continue
            i += 1
            while i < len(data) and data[i] == 0xFF:
                i += 1
            if i >= len(data):
                break
            m = data[i]
            i += 1
            if m in (0xD8, 0xD9):
                continue
            if i + 3 > len(data):
                break
            seg_len = (data[i] << 8) | data[i + 1]
            i += 2
            if m in (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7):
                if i + 5 > len(data):
                    return None
                h = (data[i + 1] << 8) | data[i + 2]
                w = (data[i + 3] << 8) | data[i + 4]
                return w, h
            i += max(0, seg_len - 2)
    return None


def _overlap_to_min_area_ratio(a, b):
    ox = 0.5 * (a["width"] + b["width"]) - abs(a["centerX"] - b["centerX"])
    oy = 0.5 * (a["height"] + b["height"]) - abs(a["centerY"] - b["centerY"])
    overlap_area = max(0.0, ox) * max(0.0, oy)
    aa = a["width"] * a["height"]
    ab = b["width"] * b["height"]
    m = min(aa, ab)
    if m <= 0:
        return 0.0
    return overlap_area / m


def _load_classes_media():
    p = Path(_MEDIA) / "classes.json"
    if not p.is_file():
        pytest.skip(f"missing {p}")
    raw = json.loads(p.read_text())
    by_id = {}
    names = []
    for row in raw:
        cid = row["Id"]
        by_id[cid] = float(row["MaxSizeM"])
        names.append(row["Name"])
    return by_id, names


def _weather_label_ok(label, base_names):
    for n in base_names:
        if label == n:
            return True
        if label == n + "(Wint)" or label == n + "(Night)":
            return True
    return False


@pytest.mark.slow
def test_ft_p_03_detection_response_structure_ac1(image_detect, image_small, warm_engine):
    detections, _ = image_detect(image_small, "img.jpg")
    assert isinstance(detections, list)
    for d in detections:
        assert isinstance(d["centerX"], (int, float))
        assert isinstance(d["centerY"], (int, float))
        assert isinstance(d["width"], (int, float))
        assert isinstance(d["height"], (int, float))
        assert 0.0 <= float(d["centerX"]) <= 1.0
        assert 0.0 <= float(d["centerY"]) <= 1.0
        assert 0.0 <= float(d["width"]) <= 1.0
        assert 0.0 <= float(d["height"]) <= 1.0
        assert isinstance(d["classNum"], int)
        assert isinstance(d["label"], str)
        assert isinstance(d["confidence"], (int, float))
        assert 0.0 <= float(d["confidence"]) <= 1.0


@pytest.mark.slow
def test_ft_p_05_confidence_filtering_ac2(image_detect, image_small, warm_engine):
    hi, _ = image_detect(image_small, "img.jpg", config=json.dumps({"probability_threshold": 0.8}))
    assert isinstance(hi, list)
    for d in hi:
        assert float(d["confidence"]) + _EPS >= 0.8

    lo, _ = image_detect(image_small, "img.jpg", config=json.dumps({"probability_threshold": 0.1}))
    assert isinstance(lo, list)
    assert len(lo) >= len(hi)


@pytest.mark.slow
def test_ft_p_06_overlap_deduplication_ac3(image_detect, image_dense, warm_engine):
    dets, _ = image_detect(
        image_dense, "img.jpg",
        config=json.dumps({"tracking_intersection_threshold": 0.6}),
        timeout=_DETECT_SLOW_TIMEOUT,
    )
    assert isinstance(dets, list)
    by_label = {}
    for d in dets:
        by_label.setdefault(d["label"], []).append(d)
    for label, group in by_label.items():
        for i in range(len(group)):
            for j in range(i + 1, len(group)):
                ratio = _overlap_to_min_area_ratio(group[i], group[j])
                assert ratio <= 0.6 + _EPS, (label, ratio)

    strict, _ = image_detect(
        image_dense, "img.jpg",
        config=json.dumps({"tracking_intersection_threshold": 0.01}),
        timeout=_DETECT_SLOW_TIMEOUT,
    )
    assert isinstance(strict, list)
    assert len(strict) <= len(dets)


@pytest.mark.slow
def test_ft_p_07_physical_size_filtering_ac4(image_detect, image_small, warm_engine):
    by_id, _ = _load_classes_media()
    wh = _image_width_height(image_small)
    assert wh is not None
    image_width_px, _ = wh
    altitude = 400.0
    focal_length = 24.0
    sensor_width = 23.5
    gsd = (sensor_width * altitude) / (focal_length * image_width_px)
    cfg = json.dumps(
        {
            "altitude": altitude,
            "focal_length": focal_length,
            "sensor_width": sensor_width,
        }
    )
    body, _ = image_detect(image_small, "img.jpg", config=cfg, timeout=_DETECT_SLOW_TIMEOUT)
    assert isinstance(body, list)
    for d in body:
        base_id = d["classNum"] % _WEATHER_CLASS_STRIDE
        assert base_id in by_id
        physical_width = float(d["width"]) * image_width_px * gsd
        assert physical_width <= by_id[base_id] + _EPS


@pytest.mark.slow
def test_ft_p_13_weather_mode_class_variants_ac5(
    image_detect, image_different_types, warm_engine
):
    _, base_names = _load_classes_media()
    body, _ = image_detect(image_different_types, "img.jpg", timeout=_DETECT_SLOW_TIMEOUT)
    assert isinstance(body, list)
    for d in body:
        label = d["label"]
        assert isinstance(label, str)
        assert len(label) > 0
        assert _weather_label_ok(label, base_names)
