"""
Tests for SLTK API validators.

Tests comprehensive validation for Segmentation Tool inputs including:
- Segment validation (timing, labels, confidence)
- Merge validation (adjacency, same tier)
- Split validation (within bounds, minimum duration)
- Export path validation (writable, valid extension)
"""

import tempfile
from pathlib import Path

import pytest

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


class TestSegmentValidation:
    """Tests for validate_segment function."""

    def test_valid_segment(self):
        """Test validation of a valid segment."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_segment

        segment = SegmentData(start=1.0, end=2.0, label="HELLO", tier="default")
        result = validate_segment(segment)

        assert result.valid is True
        assert len(result.errors) == 0

    def test_negative_start_time(self):
        """Test that negative start time fails validation."""
        from sltk.api.models import SegmentData

        # Pydantic should reject this with ge=0 constraint
        with pytest.raises(ValueError, match="greater than or equal to 0"):
            SegmentData(start=-1.0, end=2.0, tier="default")

    def test_end_before_start(self):
        """Test that end <= start fails validation."""
        from sltk.api.models import SegmentData

        # Pydantic model validator should reject this
        with pytest.raises(ValueError, match="must be greater than start"):
            SegmentData(start=2.0, end=1.0, tier="default")

    def test_end_equals_start(self):
        """Test that end == start fails validation (zero duration)."""
        from sltk.api.models import SegmentData

        with pytest.raises(ValueError, match="must be greater than start"):
            SegmentData(start=1.0, end=1.0, tier="default")

    def test_empty_label(self):
        """Test that empty label fails validation."""
        from sltk.api.models import SegmentData

        with pytest.raises(ValueError, match="empty or whitespace"):
            SegmentData(start=1.0, end=2.0, label="", tier="default")

    def test_whitespace_only_label(self):
        """Test that whitespace-only label fails validation."""
        from sltk.api.models import SegmentData

        with pytest.raises(ValueError, match="empty or whitespace"):
            SegmentData(start=1.0, end=2.0, label="   ", tier="default")

    def test_none_label_is_valid(self):
        """Test that None label is valid."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_segment

        segment = SegmentData(start=1.0, end=2.0, label=None, tier="default")
        result = validate_segment(segment)

        assert result.valid is True

    def test_confidence_out_of_range(self):
        """Test that confidence outside [0, 1] triggers warning."""
        from sltk.api.models import SegmentData

        # Pydantic should reject confidence > 1
        with pytest.raises(ValueError, match="less than or equal to 1"):
            SegmentData(start=1.0, end=2.0, confidence=1.5, tier="default")

        # Pydantic should reject confidence < 0
        with pytest.raises(ValueError, match="greater than or equal to 0"):
            SegmentData(start=1.0, end=2.0, confidence=-0.5, tier="default")

    def test_empty_tier(self):
        """Test that empty tier fails validation."""
        from sltk.api.models import SegmentData

        # Pydantic min_length=1 should reject empty tier
        with pytest.raises(ValueError):
            SegmentData(start=1.0, end=2.0, tier="")

    def test_inf_start_time(self):
        """Test that inf start time fails validation."""
        import math

        from sltk.api.models import SegmentData

        with pytest.raises(ValueError, match="finite"):
            SegmentData(start=math.inf, end=2.0, tier="default")

    def test_nan_end_time(self):
        """Test that NaN end time fails validation."""
        import math

        from sltk.api.models import SegmentData

        with pytest.raises(ValueError, match="NaN"):
            SegmentData(start=1.0, end=math.nan, tier="default")

    def test_max_duration_warning(self):
        """Test that exceeding max_duration produces warning."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_segment

        segment = SegmentData(start=0.0, end=100.0, tier="default")
        result = validate_segment(segment, max_duration=10.0)

        assert result.valid is True  # Still valid, just a warning
        assert len(result.warnings) > 0
        assert "exceeds maximum" in result.warnings[0]


class TestSegmentListValidation:
    """Tests for validate_segment_list function."""

    def test_valid_list(self):
        """Test validation of a valid segment list."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_segment_list

        segments = [
            SegmentData(start=0.0, end=1.0, label="A", tier="default"),
            SegmentData(start=1.0, end=2.0, label="B", tier="default"),
        ]
        result = validate_segment_list(segments)

        assert result.valid is True

    def test_empty_list(self):
        """Test that empty list produces warning."""
        from sltk.api.validators import validate_segment_list

        result = validate_segment_list([])

        assert result.valid is True
        assert "empty" in result.warnings[0].lower()

    def test_unsorted_segments_warning(self):
        """Test that unsorted segments produce warning when check_sorted=True."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_segment_list

        segments = [
            SegmentData(start=2.0, end=3.0, tier="default"),
            SegmentData(start=0.0, end=1.0, tier="default"),
        ]
        result = validate_segment_list(segments, check_sorted=True)

        assert result.valid is True
        assert any("not sorted" in w.lower() for w in result.warnings)

    def test_overlapping_segments_warning(self):
        """Test that overlapping segments produce warning when check_overlaps=True."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_segment_list

        segments = [
            SegmentData(start=0.0, end=2.0, tier="default"),
            SegmentData(start=1.0, end=3.0, tier="default"),
        ]
        result = validate_segment_list(segments, check_overlaps=True)

        assert result.valid is True
        assert any("overlap" in w.lower() for w in result.warnings)


class TestMergeValidation:
    """Tests for validate_merge_candidates function."""

    def test_valid_merge(self):
        """Test validation of valid merge candidates."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_merge_candidates

        segments = [
            SegmentData(start=0.0, end=1.0, label="A", tier="default"),
            SegmentData(start=1.1, end=2.0, label="B", tier="default"),
        ]
        result = validate_merge_candidates(segments, gap_threshold=0.5)

        assert result.valid is True

    def test_single_segment_fails(self):
        """Test that single segment fails merge validation."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_merge_candidates

        segments = [SegmentData(start=0.0, end=1.0, tier="default")]
        result = validate_merge_candidates(segments)

        assert result.valid is False
        assert any("at least 2" in str(e).lower() for e in result.errors)

    def test_non_adjacent_segments_fail(self):
        """Test that non-adjacent segments fail merge validation."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_merge_candidates

        segments = [
            SegmentData(start=0.0, end=1.0, tier="default"),
            SegmentData(start=5.0, end=6.0, tier="default"),  # Large gap
        ]
        result = validate_merge_candidates(segments, gap_threshold=0.5)

        assert result.valid is False
        assert any("gap" in str(e).lower() for e in result.errors)

    def test_different_tiers_fail(self):
        """Test that segments on different tiers fail when same_tier_required=True."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_merge_candidates

        segments = [
            SegmentData(start=0.0, end=1.0, tier="tier1"),
            SegmentData(start=1.0, end=2.0, tier="tier2"),
        ]
        result = validate_merge_candidates(segments, same_tier_required=True)

        assert result.valid is False
        assert any("tier" in str(e).lower() for e in result.errors)

    def test_overlapping_segments_warning(self):
        """Test that overlapping segments produce warning but still valid."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_merge_candidates

        segments = [
            SegmentData(start=0.0, end=1.5, tier="default"),
            SegmentData(start=1.0, end=2.0, tier="default"),  # Overlaps
        ]
        result = validate_merge_candidates(segments)

        assert result.valid is True  # Overlaps are allowed
        assert any("overlap" in w.lower() for w in result.warnings)


class TestSplitValidation:
    """Tests for validate_split_point function."""

    def test_valid_split(self):
        """Test validation of valid split point."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_split_point

        segment = SegmentData(start=0.0, end=2.0, tier="default")
        result = validate_split_point(segment, split_time=1.0)

        assert result.valid is True

    def test_split_before_start_fails(self):
        """Test that split before segment start fails."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_split_point

        segment = SegmentData(start=1.0, end=2.0, tier="default")
        result = validate_split_point(segment, split_time=0.5)

        assert result.valid is False
        assert any("greater than" in str(e) for e in result.errors)

    def test_split_after_end_fails(self):
        """Test that split after segment end fails."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_split_point

        segment = SegmentData(start=0.0, end=1.0, tier="default")
        result = validate_split_point(segment, split_time=1.5)

        assert result.valid is False
        assert any("less than" in str(e) for e in result.errors)

    def test_split_at_boundary_fails(self):
        """Test that split at segment boundary fails."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_split_point

        segment = SegmentData(start=0.0, end=1.0, tier="default")

        # Split at start
        result = validate_split_point(segment, split_time=0.0)
        assert result.valid is False

        # Split at end
        result = validate_split_point(segment, split_time=1.0)
        assert result.valid is False

    def test_resulting_segment_too_short_fails(self):
        """Test that split resulting in too-short segments fails."""
        from sltk.api.models import SegmentData
        from sltk.api.validators import validate_split_point

        segment = SegmentData(start=0.0, end=0.1, tier="default")
        result = validate_split_point(segment, split_time=0.05, min_duration=0.1)

        assert result.valid is False
        # Error message uses "below minimum"
        assert any("below minimum" in str(e).lower() for e in result.errors)


class TestExportPathValidation:
    """Tests for validate_export_path function."""

    def test_valid_path(self):
        """Test validation of valid export path."""
        from sltk.api.validators import validate_export_path

        with tempfile.TemporaryDirectory() as tmpdir:
            path = f"{tmpdir}/output.eaf"
            result = validate_export_path(path, allowed_extensions=[".eaf"])

            assert result.valid is True

    def test_empty_path_fails(self):
        """Test that empty path fails validation."""
        from sltk.api.validators import validate_export_path

        result = validate_export_path("")
        assert result.valid is False
        assert any("required" in str(e).lower() for e in result.errors)

    def test_invalid_extension_fails(self):
        """Test that invalid extension fails validation."""
        from sltk.api.validators import validate_export_path

        with tempfile.TemporaryDirectory() as tmpdir:
            path = f"{tmpdir}/output.txt"
            result = validate_export_path(path, allowed_extensions=[".eaf", ".json"])

            assert result.valid is False
            assert any("extension" in str(e).lower() for e in result.errors)

    def test_nonexistent_parent_fails(self):
        """Test that nonexistent parent directory fails validation."""
        from sltk.api.validators import validate_export_path

        result = validate_export_path("/nonexistent/directory/output.eaf")
        assert result.valid is False
        assert any("parent" in str(e).lower() for e in result.errors)

    def test_path_traversal_fails(self):
        """Test that path traversal attempt fails validation."""
        from sltk.api.validators import validate_export_path

        with tempfile.TemporaryDirectory() as tmpdir:
            path = f"{tmpdir}/../../../etc/output.eaf"
            result = validate_export_path(path)

            assert result.valid is False
            assert any("traversal" in str(e).lower() for e in result.errors)

    def test_null_byte_fails(self):
        """Test that null byte in path fails validation."""
        from sltk.api.validators import validate_export_path

        result = validate_export_path("/tmp/output\x00.eaf")
        assert result.valid is False
        assert any("null" in str(e).lower() for e in result.errors)

    def test_existing_file_warning(self):
        """Test that existing file produces warning."""
        from sltk.api.validators import validate_export_path

        with tempfile.NamedTemporaryFile(suffix=".eaf", delete=False) as f:
            path = f.name

        try:
            result = validate_export_path(path, allowed_extensions=[".eaf"])
            assert result.valid is True
            assert any("overwritten" in w.lower() for w in result.warnings)
        finally:
            Path(path).unlink(missing_ok=True)

    def test_existing_file_fails_when_must_not_exist(self):
        """Test that existing file fails when must_not_exist=True."""
        from sltk.api.validators import validate_export_path

        with tempfile.NamedTemporaryFile(suffix=".eaf", delete=False) as f:
            path = f.name

        try:
            result = validate_export_path(path, allowed_extensions=[".eaf"], must_not_exist=True)
            assert result.valid is False
            assert any("exists" in str(e).lower() for e in result.errors)
        finally:
            Path(path).unlink(missing_ok=True)


class TestPydanticModelValidation:
    """Tests for Pydantic model validators."""

    def test_segment_data_valid(self):
        """Test SegmentData with valid data."""
        from sltk.api.models import SegmentData

        segment = SegmentData(
            start=1.0,
            end=2.0,
            label="TEST",
            confidence=0.95,
            tier="gloss",
        )
        assert segment.start == 1.0
        assert segment.end == 2.0
        assert segment.duration == 1.0

    def test_segment_split_request_validates_bounds(self):
        """Test SegmentSplitRequest validates split within bounds."""
        from sltk.api.models import SegmentData, SegmentSplitRequest

        segment = SegmentData(start=1.0, end=3.0, tier="default")

        # Valid split
        request = SegmentSplitRequest(segment=segment, split_time=2.0)
        assert request.split_time == 2.0

        # Invalid: split before start
        with pytest.raises(ValueError, match="greater than segment start"):
            SegmentSplitRequest(segment=segment, split_time=0.5)

        # Invalid: split after end
        with pytest.raises(ValueError, match="less than segment end"):
            SegmentSplitRequest(segment=segment, split_time=3.5)

    def test_segment_merge_request_requires_two_segments(self):
        """Test SegmentMergeRequest requires at least 2 segments."""
        from sltk.api.models import SegmentData, SegmentMergeRequest

        segment = SegmentData(start=0.0, end=1.0, tier="default")

        # Should fail with only 1 segment
        with pytest.raises(ValueError):
            SegmentMergeRequest(segments=[segment])

    def test_segment_export_request_validates_format(self):
        """Test SegmentExportRequest validates format."""
        from sltk.api.models import SegmentData, SegmentExportRequest

        segment = SegmentData(start=0.0, end=1.0, tier="default")

        # Valid formats
        for fmt in ["eaf", "json", "csv", "EAF", "JSON"]:
            request = SegmentExportRequest(
                segments=[segment], output_path="/tmp/out.eaf", format=fmt
            )
            assert request.format == fmt.lower()

        # Invalid format
        with pytest.raises(ValueError, match="must be one of"):
            SegmentExportRequest(segments=[segment], output_path="/tmp/out.txt", format="txt")


class TestValidationResult:
    """Tests for ValidationResult class."""

    def test_validation_result_success(self):
        """Test ValidationResult success factory."""
        from sltk.api.validators import ValidationResult

        result = ValidationResult.success(warnings=["test warning"])

        assert result.valid is True
        assert len(result.errors) == 0
        assert result.warnings == ["test warning"]
        assert bool(result) is True

    def test_validation_result_failure(self):
        """Test ValidationResult failure factory."""
        from sltk.api.validators import ValidationError, ValidationResult

        errors = [ValidationError(field="test", message="test error", code="test_code")]
        result = ValidationResult.failure(errors, warnings=["warning"])

        assert result.valid is False
        assert len(result.errors) == 1
        assert result.warnings == ["warning"]
        assert bool(result) is False

    def test_validation_result_to_dict(self):
        """Test ValidationResult to_dict method."""
        from sltk.api.validators import ValidationError, ValidationResult

        errors = [ValidationError(field="start", message="invalid", code="invalid_time")]
        result = ValidationResult.failure(errors, warnings=["test warning"])

        d = result.to_dict()
        assert d["valid"] is False
        assert len(d["errors"]) == 1
        assert d["errors"][0]["field"] == "start"
        assert d["errors"][0]["code"] == "invalid_time"
        assert d["warnings"] == ["test warning"]


class TestConvenienceFunctions:
    """Tests for convenience validation functions."""

    def test_require_valid_segment(self):
        """Test require_valid_segment raises HTTPException on invalid."""
        from fastapi import HTTPException

        from sltk.api.models import SegmentData
        from sltk.api.validators import require_valid_segment

        # Valid segment should not raise
        segment = SegmentData(start=0.0, end=1.0, tier="default")
        require_valid_segment(segment)  # Should not raise

    def test_require_valid_export_path(self):
        """Test require_valid_export_path raises HTTPException on invalid."""
        from fastapi import HTTPException

        from sltk.api.validators import require_valid_export_path

        # Invalid path should raise
        with pytest.raises(HTTPException) as exc_info:
            require_valid_export_path("")

        assert exc_info.value.status_code == 422
