"""
Tests for the SLTK FastAPI backend.
"""

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

import pytest

# Skip all tests if fastapi not installed
pytest.importorskip("fastapi")

# Mark all tests in this module as API tests
pytestmark = pytest.mark.api

from fastapi.testclient import TestClient

from sltk.api import dependencies as api_deps
from sltk.api.main import app


@pytest.fixture
def client():
    """Create a test client for the FastAPI app."""
    return TestClient(app)


@pytest.fixture
def temp_dir():
    """Create a temporary directory for test files."""
    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir_path = Path(tmpdir)
        # Temporarily add /tmp to allowed roots for testing
        original_roots = api_deps.ALLOWED_ROOTS.copy()
        api_deps.ALLOWED_ROOTS.append(tmpdir_path.resolve())
        yield tmpdir_path
        # Restore original roots
        api_deps.ALLOWED_ROOTS.clear()
        api_deps.ALLOWED_ROOTS.extend(original_roots)


class TestHealthEndpoints:
    """Tests for health check and info endpoints."""

    def test_health_check(self, client):
        """Test the health check endpoint."""
        response = client.get("/api/health")
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "healthy"
        assert "timestamp" in data

    def test_api_info(self, client):
        """Test the API info endpoint."""
        response = client.get("/api/info")
        assert response.status_code == 200
        data = response.json()
        assert data["name"] == "SLTK API"
        assert "endpoints" in data


class TestVideoDiscovery:
    """Tests for video discovery endpoints."""

    def test_discover_videos_invalid_path(self, client):
        """Test video discovery with path outside allowed roots returns 403."""
        response = client.post(
            "/api/videos/discover", json={"root_path": "/nonexistent/path/that/does/not/exist"}
        )
        # Path is outside allowed roots, so we get 403 (access denied)
        assert response.status_code == 403

    def test_discover_videos_empty_dir(self, client, temp_dir):
        """Test video discovery in empty directory."""
        response = client.post("/api/videos/discover", json={"root_path": str(temp_dir)})
        assert response.status_code == 200
        data = response.json()
        assert data["total_count"] == 0
        assert data["videos"] == []

    def test_discover_videos_path_traversal_blocked(self, client, temp_dir):
        """Test that path traversal attempts are blocked."""
        # Try to use .. to escape the allowed directory
        malicious_path = str(temp_dir) + "/../../../etc/passwd"
        response = client.post("/api/videos/discover", json={"root_path": malicious_path})
        assert response.status_code == 403

    def test_discover_videos_null_byte_blocked(self, client, temp_dir):
        """Test that null byte injection is blocked."""
        malicious_path = str(temp_dir) + "\x00/etc/passwd"
        response = client.post("/api/videos/discover", json={"root_path": malicious_path})
        assert response.status_code == 403


class TestExtractionJobs:
    """Tests for extraction job endpoints."""

    def test_list_jobs_empty(self, client):
        """Test listing jobs when none exist."""
        response = client.get("/api/extraction/jobs")
        assert response.status_code == 200
        assert isinstance(response.json(), list)

    def test_get_nonexistent_job(self, client):
        """Test getting status of nonexistent job."""
        response = client.get("/api/extraction/status/nonexistent-job-id")
        assert response.status_code == 404


class TestPoseEndpoints:
    """Tests for pose data endpoints."""

    def test_load_poses_missing_file(self, client):
        """Test loading poses from missing file (or outside allowed dirs)."""
        response = client.get("/api/poses/load?path=/nonexistent/file.h5")
        # 404 = file not found, 403 = outside allowed directories (security)
        assert response.status_code in [403, 404]

    def test_get_metadata_missing_file(self, client):
        """Test getting metadata from missing file (or outside allowed dirs)."""
        response = client.get("/api/poses/metadata?path=/nonexistent/file.h5")
        # 404 = file not found, 403 = outside allowed directories (security)
        assert response.status_code in [403, 404]


class TestDatasetEndpoints:
    """Tests for dataset browsing endpoints."""

    def test_list_datasets(self, client):
        """Test listing available datasets."""
        response = client.get("/api/datasets/list")
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        # Should have at least the built-in datasets
        names = [d["name"] for d in data]
        assert "bslcp" in names or len(data) >= 0  # At least returns list


class TestAPIModels:
    """Tests for Pydantic model validation."""

    def test_extraction_config_defaults(self):
        """Test ExtractionConfig has sensible defaults."""
        from sltk.api.models import ExtractionConfig

        config = ExtractionConfig()
        assert config.enable_mediapipe is True
        assert config.device == "cuda:0"
        assert config.skip_existing is True

    def test_video_discovery_request(self):
        """Test VideoDiscoveryRequest model."""
        from sltk.api.models import VideoDiscoveryRequest

        req = VideoDiscoveryRequest(root_path="/some/path")
        assert req.recursive is True
        assert ".mp4" in req.extensions

    def test_segment_data(self):
        """Test SegmentData model."""
        from sltk.api.models import SegmentData

        seg = SegmentData(start=1.0, end=2.0, tier="test")
        assert seg.start == 1.0
        assert seg.end == 2.0
        assert seg.label is None


class TestFeatureConnectionEndpoints:
    """Tests for feature connection endpoints."""

    def test_get_feature_config_nonexistent(self, client):
        """Test getting feature config for dataset without config."""
        response = client.get("/api/datasets/nonexistent_dataset/features/config")
        assert response.status_code == 200
        # Should return None/null for nonexistent config
        assert response.json() is None

    def test_connect_features_invalid_path(self, client):
        """Test connecting features with path outside allowed roots."""
        response = client.post(
            "/api/datasets/test_dataset/features/connect",
            json={
                "sources": [
                    {
                        "path": "/nonexistent/path",
                        "feature_types": ["wilor"],
                        "naming_pattern": "{sample_id}_{feature_type}.h5",
                    }
                ]
            },
        )
        # Should be 403 (forbidden) for path outside allowed roots
        assert response.status_code == 403

    def test_connect_features_success(self, client, temp_dir):
        """Test successfully connecting features."""
        # Create the feature directory
        feature_dir = temp_dir / "features"
        feature_dir.mkdir()

        response = client.post(
            "/api/datasets/test_connection_dataset/features/connect",
            json={
                "sources": [
                    {
                        "path": str(feature_dir),
                        "feature_types": ["wilor", "smpl"],
                        "naming_pattern": "{sample_id}_{feature_type}.h5",
                    }
                ]
            },
        )
        assert response.status_code == 200
        data = response.json()
        assert data["dataset_name"] == "test_connection_dataset"
        assert len(data["feature_sources"]) == 1
        assert data["feature_sources"][0]["feature_types"] == ["wilor", "smpl"]

    def test_get_feature_availability_no_scan(self, client):
        """Test getting feature availability without scanning first."""
        response = client.get("/api/datasets/test_dataset/features/availability")
        assert response.status_code == 200
        data = response.json()
        assert data["total"] == 0
        assert "error" in data  # Should have error message about no scan

    def test_get_feature_summary_no_config(self, client):
        """Test getting feature summary for dataset without config."""
        response = client.get("/api/datasets/nonexistent_dataset/features/summary")
        assert response.status_code == 200
        data = response.json()
        assert data["total_samples"] == 0
        assert data["feature_counts"] == {}

    def test_scan_features_no_config(self, client):
        """Test scanning features without connecting first."""
        response = client.post(
            "/api/datasets/unconnected_dataset/features/scan",
            json={"sample_ids": ["sample1"]},
        )
        assert response.status_code == 400
        assert "No feature configuration" in response.json()["detail"]

    def test_get_scan_status_nonexistent(self, client):
        """Test getting status of nonexistent scan job."""
        response = client.get("/api/datasets/test/features/scan/nonexistent-job-id")
        assert response.status_code == 404

    def test_get_sample_features_nonexistent(self, client):
        """Test getting features for nonexistent sample."""
        response = client.get("/api/datasets/test/samples/nonexistent_sample/features")
        assert response.status_code == 200
        # Returns empty dict for sample without cached features
        assert response.json() == {}

    def test_get_extraction_plan_no_config(self, client):
        """Test getting extraction plan without connecting first."""
        response = client.get(
            "/api/datasets/unconnected_dataset/features/extract/plan",
            params={"target_features": ["segments"]},
        )
        assert response.status_code == 400
        # Error can be about missing config OR missing scan data
        detail = response.json()["detail"]
        assert "No feature configuration" in detail or "No scan data" in detail

    def test_detect_naming_patterns_invalid_path(self, client):
        """Test detecting naming patterns with invalid path."""
        response = client.post(
            "/api/features/detect-patterns",
            params={"path": "/nonexistent/path"},
        )
        assert response.status_code == 403  # Outside allowed roots

    def test_detect_naming_patterns_empty_dir(self, client, temp_dir):
        """Test detecting naming patterns in empty directory."""
        empty_dir = temp_dir / "empty_features"
        empty_dir.mkdir()

        response = client.post(
            "/api/features/detect-patterns",
            params={"path": str(empty_dir)},
        )
        assert response.status_code == 200
        assert response.json() == []  # No patterns detected

    def test_detect_naming_patterns_with_files(self, client, temp_dir):
        """Test detecting naming patterns with feature files."""
        feature_dir = temp_dir / "features"
        feature_dir.mkdir()

        # Create some feature files
        (feature_dir / "sample_001_wilor.h5").touch()
        (feature_dir / "sample_002_wilor.h5").touch()
        (feature_dir / "sample_001_smpl.h5").touch()

        response = client.post(
            "/api/features/detect-patterns",
            params={"path": str(feature_dir)},
        )
        assert response.status_code == 200
        patterns = response.json()
        assert len(patterns) > 0
        # Should detect the ID-based pattern
        assert any("sample_id" in p["pattern"] for p in patterns)

    def test_start_extraction_no_config(self, client, temp_dir):
        """Test starting extraction without connecting first."""
        output_dir = temp_dir / "output"
        output_dir.mkdir()

        response = client.post(
            "/api/datasets/unconnected_dataset/features/extract",
            json={
                "target_features": ["segments"],
                "output_path": str(output_dir),
            },
        )
        assert response.status_code == 400
        # Error can be about missing config OR missing scan data
        detail = response.json()["detail"]
        assert "No feature configuration" in detail or "No scan data" in detail


class TestFeatureConnectionModels:
    """Tests for feature connection Pydantic models."""

    def test_feature_source_model(self):
        """Test FeatureSourceModel validation."""
        from sltk.api.models import FeatureSourceModel

        source = FeatureSourceModel(
            path="/some/path",
            feature_types=["wilor", "smpl"],
            naming_pattern="{sample_id}_{feature_type}.h5",
        )
        assert source.path == "/some/path"
        assert len(source.feature_types) == 2

    def test_feature_connection_config_model(self):
        """Test FeatureConnectionConfigModel validation."""
        from sltk.api.models import FeatureConnectionConfigModel, FeatureSourceModel

        config = FeatureConnectionConfigModel(
            dataset_name="test",
            feature_sources=[
                FeatureSourceModel(
                    path="/some/path",
                    feature_types=["wilor"],
                    naming_pattern="{sample_id}_{feature_type}.h5",
                )
            ],
            last_scan=None,
        )
        assert config.dataset_name == "test"
        assert len(config.feature_sources) == 1

    def test_feature_scan_job_model(self):
        """Test FeatureScanJobModel validation."""
        from sltk.api.models import FeatureScanJobModel

        job = FeatureScanJobModel(
            job_id="test-job",
            dataset_name="test",
            status="pending",
            progress=0,
            total_samples=100,
            scanned_samples=0,
        )
        assert job.status == "pending"
        assert job.error is None

    def test_extraction_plan_model(self):
        """Test ExtractionPlanModel validation."""
        from sltk.api.models import ExtractionPlanModel, ExtractionStepModel

        plan = ExtractionPlanModel(
            target_features=["segments"],
            steps=[
                ExtractionStepModel(
                    order=1,
                    feature_type="mediapipe",
                    sample_count=10,
                    sample_ids=["s1", "s2"],
                    depends_on=[],
                    is_extraction=True,
                )
            ],
            total_samples=10,
            total_extractions=10,
        )
        assert len(plan.steps) == 1
        assert plan.total_extractions == 10
