"""Tests for feature connection and management functionality."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path
from unittest.mock import patch

import pytest

from sltk.data.feature_manager import (
    FEATURE_DEPENDENCIES,
    ExtractionPlan,
    ExtractionStep,
    FeatureAvailability,
    FeatureConnectionConfig,
    FeatureConnectionManager,
    FeatureSource,
    FeatureSummary,
    get_config_dir,
    get_feature_manager,
)


class TestFeatureSource:
    """Tests for FeatureSource dataclass."""

    def test_create_feature_source(self):
        """Test creating a feature source."""
        source = FeatureSource(
            path="/vol/research/features",
            feature_types=["wilor", "smpl"],
            naming_pattern="{sample_id}_{feature_type}.h5",
        )
        assert source.path == "/vol/research/features"
        assert source.feature_types == ["wilor", "smpl"]
        assert source.naming_pattern == "{sample_id}_{feature_type}.h5"

    def test_to_dict(self):
        """Test serialization to dict."""
        source = FeatureSource(
            path="/vol/research/features",
            feature_types=["wilor"],
        )
        d = source.to_dict()
        assert d["path"] == "/vol/research/features"
        assert d["feature_types"] == ["wilor"]
        assert "naming_pattern" in d

    def test_from_dict(self):
        """Test deserialization from dict."""
        d = {
            "path": "/vol/research/features",
            "feature_types": ["smpl"],
            "naming_pattern": "{sample_id}.h5",
        }
        source = FeatureSource.from_dict(d)
        assert source.path == d["path"]
        assert source.feature_types == d["feature_types"]
        assert source.naming_pattern == d["naming_pattern"]


class TestFeatureConnectionConfig:
    """Tests for FeatureConnectionConfig."""

    def test_create_config(self):
        """Test creating a config."""
        source = FeatureSource(path="/path", feature_types=["wilor"])
        config = FeatureConnectionConfig(
            dataset_name="bslcp",
            feature_sources=[source],
            last_scan=None,
        )
        assert config.dataset_name == "bslcp"
        assert len(config.feature_sources) == 1

    def test_save_and_load(self, tmp_path):
        """Test saving and loading config."""
        # Patch config dir to use temp directory
        with patch("sltk.data.feature_manager.get_config_dir", return_value=tmp_path):
            source = FeatureSource(path="/path", feature_types=["wilor"])
            config = FeatureConnectionConfig(
                dataset_name="test_dataset",
                feature_sources=[source],
            )
            config.save()

            loaded = FeatureConnectionConfig.load("test_dataset")
            assert loaded is not None
            assert loaded.dataset_name == "test_dataset"
            assert len(loaded.feature_sources) == 1

    def test_load_nonexistent(self, tmp_path):
        """Test loading nonexistent config."""
        with patch("sltk.data.feature_manager.get_config_dir", return_value=tmp_path):
            loaded = FeatureConnectionConfig.load("nonexistent")
            assert loaded is None


class TestFeatureAvailability:
    """Tests for FeatureAvailability."""

    def test_create_availability(self):
        """Test creating availability record."""
        avail = FeatureAvailability(
            sample_id="sample_001",
            available={"wilor": "/path/sample_001_wilor.h5"},
            missing=["smpl", "segments"],
        )
        assert avail.sample_id == "sample_001"
        assert "wilor" in avail.available
        assert "smpl" in avail.missing

    def test_to_from_dict(self):
        """Test serialization roundtrip."""
        avail = FeatureAvailability(
            sample_id="sample_001",
            available={"wilor": "/path/wilor.h5"},
            missing=["smpl"],
        )
        d = avail.to_dict()
        loaded = FeatureAvailability.from_dict(d)
        assert loaded.sample_id == avail.sample_id
        assert loaded.available == avail.available
        assert loaded.missing == avail.missing


class TestFeatureSummary:
    """Tests for FeatureSummary."""

    def test_create_summary(self):
        """Test creating summary."""
        summary = FeatureSummary(
            dataset_name="bslcp",
            total_samples=1000,
            feature_counts={"wilor": 800, "smpl": 750},
        )
        assert summary.total_samples == 1000
        assert summary.feature_counts["wilor"] == 800


class TestExtractionPlan:
    """Tests for ExtractionPlan."""

    def test_create_plan(self):
        """Test creating extraction plan."""
        steps = [
            ExtractionStep(
                order=1,
                feature_type="mediapipe",
                sample_ids=["s1", "s2"],
                depends_on=[],
                is_extraction=True,
            ),
            ExtractionStep(
                order=2,
                feature_type="angles",
                sample_ids=["s1", "s2"],
                depends_on=["mediapipe"],
                is_extraction=False,
            ),
        ]
        plan = ExtractionPlan(
            target_features=["angles"],
            steps=steps,
            total_samples=2,
            total_extractions=4,
        )
        assert len(plan.steps) == 2
        assert plan.total_extractions == 4


class TestFeatureConnectionManager:
    """Tests for FeatureConnectionManager."""

    @pytest.fixture
    def manager(self, tmp_path):
        """Create a manager with temp directories."""
        with patch("sltk.data.feature_manager.get_config_dir", return_value=tmp_path / "configs"):
            with patch("sltk.data.feature_manager.get_cache_dir", return_value=tmp_path / "cache"):
                (tmp_path / "configs").mkdir()
                (tmp_path / "cache").mkdir()
                yield FeatureConnectionManager()

    @pytest.fixture
    def feature_dir(self, tmp_path):
        """Create a temp feature directory with sample files."""
        feat_dir = tmp_path / "features"
        feat_dir.mkdir()

        # Create some fake feature files
        for i in range(5):
            (feat_dir / f"sample_{i:03d}_wilor.h5").touch()
            (feat_dir / f"sample_{i:03d}_smpl.h5").touch()

        return feat_dir

    def test_connect(self, manager, feature_dir):
        """Test connecting feature sources."""
        source = FeatureSource(
            path=str(feature_dir),
            feature_types=["wilor", "smpl"],
        )
        config = manager.connect("test_dataset", [source])

        assert config.dataset_name == "test_dataset"
        assert len(config.feature_sources) == 1

    def test_get_config(self, manager, feature_dir):
        """Test getting config."""
        # First connect
        source = FeatureSource(path=str(feature_dir), feature_types=["wilor"])
        manager.connect("test_dataset", [source])

        # Then get
        config = manager.get_config("test_dataset")
        assert config is not None
        assert config.dataset_name == "test_dataset"

    def test_resolve_dependencies(self, manager):
        """Test dependency resolution."""
        # Test segments - should resolve to [mediapipe, wilor, angles, hamer, segments]
        resolved = manager._resolve_dependencies(["segments"])

        # Check dependencies come before dependents
        assert resolved.index("mediapipe") < resolved.index("angles")
        assert resolved.index("wilor") < resolved.index("hamer")
        assert resolved.index("angles") < resolved.index("segments")
        assert resolved.index("hamer") < resolved.index("segments")

    def test_resolve_dependencies_no_deps(self, manager):
        """Test resolving features with no dependencies."""
        resolved = manager._resolve_dependencies(["wilor", "mediapipe"])
        assert "wilor" in resolved
        assert "mediapipe" in resolved
        assert len(resolved) == 2

    def test_plan_extraction_all_available(self, manager, tmp_path):
        """Test planning extraction when all features available."""
        # Create cache with all features available
        cache = {
            "sample_001": FeatureAvailability(
                sample_id="sample_001",
                available={
                    "mediapipe": "/path/mp.h5",
                    "wilor": "/path/wilor.h5",
                    "angles": "/path/angles.h5",
                    "hamer": "/path/hamer.h5",
                    "segments": "/path/seg.h5",
                },
                missing=[],
            ),
        }
        manager._cache["test_dataset"] = cache

        plan = manager.plan_extraction("test_dataset", ["segments"])

        # No steps needed since all available
        assert len(plan.steps) == 0
        assert plan.total_extractions == 0

    def test_plan_extraction_missing_features(self, manager, tmp_path):
        """Test planning extraction with missing features."""
        # Create cache with only wilor available
        cache = {
            "sample_001": FeatureAvailability(
                sample_id="sample_001",
                available={"wilor": "/path/wilor.h5"},
                missing=["mediapipe", "angles", "hamer", "segments"],
            ),
        }
        manager._cache["test_dataset"] = cache

        plan = manager.plan_extraction("test_dataset", ["segments"])

        # Should have steps for missing dependencies
        step_types = [s.feature_type for s in plan.steps]
        assert "mediapipe" in step_types  # Need for angles
        assert "angles" in step_types
        assert "hamer" in step_types
        assert "segments" in step_types


class TestFeatureDependencies:
    """Tests for feature dependency definitions."""

    def test_segments_dependencies(self):
        """Test that segments has correct dependencies."""
        assert "angles" in FEATURE_DEPENDENCIES["segments"]
        assert "hamer" in FEATURE_DEPENDENCIES["segments"]

    def test_angles_dependencies(self):
        """Test that angles depends on mediapipe."""
        assert "mediapipe" in FEATURE_DEPENDENCIES["angles"]

    def test_hamer_dependencies(self):
        """Test that hamer depends on wilor."""
        assert "wilor" in FEATURE_DEPENDENCIES["hamer"]

    def test_base_features_no_deps(self):
        """Test that base features have no dependencies."""
        assert FEATURE_DEPENDENCIES["mediapipe"] == []
        assert FEATURE_DEPENDENCIES["wilor"] == []
        assert FEATURE_DEPENDENCIES["smpl"] == []


class TestGetFeatureManager:
    """Tests for the global manager instance."""

    def test_get_manager(self):
        """Test getting global manager instance."""
        manager1 = get_feature_manager()
        manager2 = get_feature_manager()
        # Should return same instance
        assert manager1 is manager2
