"""Tests for the NMS detector integration."""

from __future__ import annotations

import json
import xml.etree.ElementTree as ET
from dataclasses import asdict
from pathlib import Path

import numpy as np
import pytest

# Fixture paths
FIXTURES_DIR = Path(__file__).parent / "fixtures" / "nms"
H5_PATH = FIXTURES_DIR / "M28n_clip.h5"
NLF_PATH = FIXTURES_DIR / "M28n_clip_nlf.h5"
VIDEO_PATH = FIXTURES_DIR / "M28n_clip.mp4"


@pytest.fixture
def h5_path():
    if not H5_PATH.exists():
        pytest.skip(f"NMS fixture not found: {H5_PATH}")
    return str(H5_PATH)


@pytest.fixture
def nlf_path():
    if not NLF_PATH.exists():
        pytest.skip(f"NMS NLF fixture not found: {NLF_PATH}")
    return str(NLF_PATH)


# ============================================================================
# Core module tests
# ============================================================================


class TestModels:
    def test_import_models(self):
        pytest.importorskip("hdf5plugin")
        from sltk.nms.models import BlinkEvent, FrameState, NMSEvent, TrackingQuality

        assert TrackingQuality is not None
        assert BlinkEvent is not None
        assert NMSEvent is not None
        assert FrameState is not None

    def test_constants(self):
        from sltk.nms.constants import ALL_DETECTORS, FPS, THRESHOLD_DEFAULTS

        assert FPS == 25
        assert "nod" in ALL_DETECTORS
        assert "shake" in ALL_DETECTORS
        assert "prominence" in THRESHOLD_DEFAULTS

    def test_package_exports(self):
        pytest.importorskip("hdf5plugin")
        from sltk.nms import ALL_DETECTORS, detect_nms, export_results

        assert callable(detect_nms)
        assert isinstance(ALL_DETECTORS, set)


class TestLoaders:
    def test_load_teaser_data(self, h5_path):
        from sltk.nms.io.loaders import load_teaser_data

        result = load_teaser_data(h5_path, person="0", load_nms=True)
        assert len(result) == 8

        eyelid, successful_frames, boxes, pose, scores, expression, jaw_pose, quality = result

        assert isinstance(eyelid, np.ndarray)
        assert len(eyelid) > 0
        assert isinstance(successful_frames, np.ndarray)
        assert quality.total_data_frames > 0
        assert quality.total_video_frames > 0

    def test_load_teaser_data_invalid_person(self, h5_path):
        from sltk.nms.io.loaders import load_teaser_data

        with pytest.raises(KeyError, match="Person"):
            load_teaser_data(h5_path, person="999")

    def test_resolve_smpl_path(self, h5_path):
        from sltk.nms.io.loaders import resolve_smpl_path

        # Should find NLF file in same directory
        result = resolve_smpl_path(h5_path)
        assert result is not None
        assert result.exists()
        assert result.name == "M28n_clip_nlf.h5"

    def test_load_smpl_eye_data(self, h5_path, nlf_path):
        from sltk.nms.io.loaders import load_smpl_eye_data, load_teaser_data

        _, successful_frames, boxes, *_ = load_teaser_data(h5_path, "0")

        leye, reye = load_smpl_eye_data(Path(nlf_path), successful_frames, boxes)
        assert leye is not None
        assert reye is not None
        assert leye.shape[0] == len(successful_frames)
        assert leye.shape[1] == 3


class TestProcessing:
    def test_validity_mask(self, h5_path):
        from sltk.nms.io.loaders import load_teaser_data
        from sltk.nms.processing.validity import compute_validity_mask

        eyelid, _, _, pose, scores, *_ = load_teaser_data(h5_path, "0")

        valid = compute_validity_mask(eyelid, scores, pose)
        assert isinstance(valid, np.ndarray)
        assert valid.dtype == bool
        assert len(valid) == len(eyelid)
        assert valid.sum() > 0

    def test_segment_valid_regions(self):
        from sltk.nms.processing.validity import segment_valid_regions

        valid = np.array([False, True, True, True, False, False, True, True, False])
        segments = segment_valid_regions(valid, min_segment=2)
        assert len(segments) > 0
        for start, end in segments:
            assert end > start
            assert end - start >= 2

    def test_mask_to_regions(self):
        from sltk.nms.processing.utils import mask_to_regions

        mask = np.array([False, True, True, False, True])
        regions = mask_to_regions(mask)
        assert regions == [(1, 3), (4, 5)]

    def test_merge_regions(self):
        from sltk.nms.processing.utils import merge_regions

        regions = [(0, 5), (7, 10), (20, 25)]
        merged = merge_regions(regions, gap=3)
        assert merged == [(0, 10), (20, 25)]

    def test_adaptive_thresholds(self, h5_path):
        from sltk.nms.io.loaders import load_teaser_data
        from sltk.nms.processing.adaptive import compute_adaptive_thresholds
        from sltk.nms.processing.validity import compute_validity_mask

        eyelid, _, _, pose, scores, expression, jaw_pose, _ = load_teaser_data(
            h5_path, "0", load_nms=True
        )
        valid = compute_validity_mask(eyelid, scores, pose)

        thresholds = compute_adaptive_thresholds(pose, jaw_pose, expression, valid, eyelid=eyelid)
        assert isinstance(thresholds, dict)
        assert "prominence" in thresholds
        assert all(isinstance(v, float) for v in thresholds.values())


class TestDetectors:
    def test_detect_blinks(self, h5_path):
        from sltk.nms.detectors.blinks import detect_blinks
        from sltk.nms.io.loaders import load_teaser_data
        from sltk.nms.processing.validity import compute_validity_mask, segment_valid_regions

        eyelid, successful_frames, _, pose, scores, *_ = load_teaser_data(h5_path, "0")
        valid = compute_validity_mask(eyelid, scores, pose)
        segments = segment_valid_regions(valid)

        blinks = detect_blinks(eyelid, successful_frames, valid, segments)
        assert isinstance(blinks, list)
        # We expect at least some blinks in the test clip
        assert len(blinks) > 0

        b = blinks[0]
        assert b.start_frame >= 0
        assert b.end_frame >= b.start_frame
        assert b.peak_frame >= b.start_frame
        assert 0 < b.confidence <= 1
        assert b.width_ms > 0

    def test_detect_all_nms(self, h5_path):
        from sltk.nms.detectors.pipeline import detect_all_nms
        from sltk.nms.io.loaders import load_teaser_data
        from sltk.nms.processing.validity import compute_validity_mask, segment_valid_regions

        eyelid, successful_frames, _, pose, scores, expression, jaw_pose, _ = load_teaser_data(
            h5_path, "0", load_nms=True
        )
        valid = compute_validity_mask(eyelid, scores, pose, require_eyelid=False)
        segments = segment_valid_regions(valid)

        events = detect_all_nms(
            pose,
            jaw_pose,
            expression,
            successful_frames,
            valid,
            segments,
            detectors={"nod", "shake", "tilt"},
            eyelid=eyelid,
        )
        assert isinstance(events, list)
        # All events have required fields
        for e in events:
            assert e.tier in {"HEAD-NOD", "HEAD-SHAKE", "HEAD-TILT"}
            assert e.start_frame >= 0
            assert e.end_frame >= e.start_frame
            assert 0 <= e.confidence <= 1
            assert len(e.label) > 0


class TestRunner:
    def test_detect_nms_blinks_only(self, h5_path):
        from sltk.nms.runner import detect_nms

        blinks, nms_events, quality = detect_nms(h5_path)
        assert isinstance(blinks, list)
        assert isinstance(nms_events, list)
        assert len(nms_events) == 0  # blinks only by default
        assert quality.total_data_frames > 0

    def test_detect_nms_all(self, h5_path):
        from sltk.nms.runner import detect_nms

        blinks, nms_events, quality = detect_nms(h5_path, detectors={"all"})
        assert isinstance(blinks, list)
        assert isinstance(nms_events, list)
        assert quality.total_data_frames > 0

        # Should have detected some NMS events
        if nms_events:
            e = nms_events[0]
            assert hasattr(e, "tier")
            assert hasattr(e, "start_frame")
            assert hasattr(e, "label")

    def test_detect_nms_specific_detectors(self, h5_path):
        from sltk.nms.runner import detect_nms

        blinks, nms_events, quality = detect_nms(h5_path, detectors={"blink", "nod"})
        assert isinstance(blinks, list)
        # NMS events should only be HEAD-NOD
        for e in nms_events:
            assert e.tier == "HEAD-NOD"

    def test_detect_nms_file_not_found(self):
        pytest.importorskip("hdf5plugin")
        from sltk.nms.runner import detect_nms

        with pytest.raises(FileNotFoundError):
            detect_nms("/nonexistent/file.h5")


class TestExporters:
    def test_export_json(self, h5_path, tmp_path):
        from sltk.nms.runner import detect_nms, export_results

        blinks, nms_events, _ = detect_nms(h5_path, detectors={"all"})

        paths = export_results(
            blinks,
            nms_events,
            h5_path,
            output_dir=str(tmp_path),
            formats=["json"],
        )
        assert "json" in paths
        json_path = paths["json"]
        assert Path(json_path).exists()

        with open(json_path) as f:
            data = json.load(f)
        assert "blinks" in data
        assert isinstance(data["blinks"], list)

    def test_export_csv(self, h5_path, tmp_path):
        from sltk.nms.runner import detect_nms, export_results

        blinks, nms_events, _ = detect_nms(h5_path, detectors={"all"})

        paths = export_results(
            blinks,
            nms_events,
            h5_path,
            output_dir=str(tmp_path),
            formats=["csv"],
        )
        assert "csv" in paths
        assert Path(paths["csv"]).exists()
        content = Path(paths["csv"]).read_text()
        assert "start_frame" in content

    def test_export_elan(self, h5_path, tmp_path):
        from sltk.nms.runner import detect_nms, export_results

        blinks, nms_events, _ = detect_nms(h5_path, detectors={"all"})

        paths = export_results(
            blinks,
            nms_events,
            h5_path,
            output_dir=str(tmp_path),
            formats=["elan"],
            participant_id="M28",
        )
        assert "elan" in paths
        eaf_path = paths["elan"]
        assert Path(eaf_path).exists()

        # Validate XML structure
        tree = ET.parse(eaf_path)
        root = tree.getroot()
        assert root.tag == "ANNOTATION_DOCUMENT"
        assert root.find("HEADER") is not None
        assert root.find("TIME_ORDER") is not None

        # Should have tiers with participant prefix
        tiers = root.findall("TIER")
        if tiers:
            tier_ids = [t.get("TIER_ID") for t in tiers]
            assert any("M28_" in tid for tid in tier_ids)

    def test_export_no_formats(self, h5_path):
        from sltk.nms.runner import export_results

        paths = export_results([], [], h5_path, formats=None)
        assert paths == {}


# ============================================================================
# API endpoint tests
# ============================================================================


class TestNMSAPI:
    @pytest.fixture
    def client(self):
        from fastapi.testclient import TestClient

        from sltk.api.main import app

        return TestClient(app)

    def test_status_endpoint(self, client):
        response = client.get("/api/nms/status")
        assert response.status_code == 200
        data = response.json()
        assert "available" in data
        assert "dependencies" in data
        assert "numpy" in data["dependencies"]
        assert "scipy" in data["dependencies"]
        assert "h5py" in data["dependencies"]

    def test_detect_endpoint(self, client, h5_path):
        import sltk.api.dependencies as api_deps

        # Temporarily add fixture dir to allowed roots
        original_roots = api_deps.ALLOWED_ROOTS.copy()
        api_deps.ALLOWED_ROOTS.append(FIXTURES_DIR.resolve())

        try:
            response = client.post(
                "/api/nms/detect",
                json={
                    "h5_path": h5_path,
                    "detectors": ["blink"],
                },
            )
            assert response.status_code == 200
            data = response.json()
            assert "video_name" in data
            assert "blinks" in data
            assert "nms_events" in data
            assert "quality" in data
            assert data["video_name"] == "M28n_clip"
            assert isinstance(data["blinks"], list)
        finally:
            api_deps.ALLOWED_ROOTS.clear()
            api_deps.ALLOWED_ROOTS.extend(original_roots)

    def test_detect_all_endpoint(self, client, h5_path):
        import sltk.api.dependencies as api_deps

        original_roots = api_deps.ALLOWED_ROOTS.copy()
        api_deps.ALLOWED_ROOTS.append(FIXTURES_DIR.resolve())

        try:
            response = client.post(
                "/api/nms/detect",
                json={
                    "h5_path": h5_path,
                    "detectors": ["all"],
                },
            )
            assert response.status_code == 200
            data = response.json()
            assert "tier_counts" in data
        finally:
            api_deps.ALLOWED_ROOTS.clear()
            api_deps.ALLOWED_ROOTS.extend(original_roots)

    def test_detect_invalid_path(self, client):
        response = client.post(
            "/api/nms/detect",
            json={
                "h5_path": "/nonexistent/file.h5",
            },
        )
        assert response.status_code in (403, 404)

    def test_detect_non_h5(self, client, tmp_path):
        import sltk.api.dependencies as api_deps

        # Create a real .mp4 file so validate_path(must_exist=True) passes
        mp4_file = tmp_path / "test.mp4"
        mp4_file.write_bytes(b"\x00" * 10)

        original_roots = api_deps.ALLOWED_ROOTS.copy()
        api_deps.ALLOWED_ROOTS.append(tmp_path.resolve())

        try:
            response = client.post(
                "/api/nms/detect",
                json={
                    "h5_path": str(mp4_file),
                },
            )
            assert response.status_code == 400
        finally:
            api_deps.ALLOWED_ROOTS.clear()
            api_deps.ALLOWED_ROOTS.extend(original_roots)

    def test_detect_with_export(self, client, h5_path, tmp_path):
        import sltk.api.dependencies as api_deps

        original_roots = api_deps.ALLOWED_ROOTS.copy()
        api_deps.ALLOWED_ROOTS.append(FIXTURES_DIR.resolve())
        api_deps.ALLOWED_ROOTS.append(tmp_path.resolve())

        try:
            response = client.post(
                "/api/nms/detect",
                json={
                    "h5_path": h5_path,
                    "detectors": ["blink"],
                    "format": ["json"],
                    "output_dir": str(tmp_path),
                },
            )
            assert response.status_code == 200
            data = response.json()
            assert "export_paths" in data
            if data["export_paths"]:
                assert "json" in data["export_paths"]
        finally:
            api_deps.ALLOWED_ROOTS.clear()
            api_deps.ALLOWED_ROOTS.extend(original_roots)
