"""
Tests for the SLTK API security module.

Covers path validation, input sanitization, and error message handling.
"""

import tempfile
from pathlib import Path

import pytest
from fastapi import HTTPException

from sltk.api import security


class TestPathSafety:
    """Tests for path safety validation."""

    def test_normal_path_is_safe(self):
        """Test that normal paths are marked as safe."""
        safe, msg = security.is_path_safe("/home/user/file.txt")
        assert safe is True
        assert msg is None

    def test_relative_path_is_safe(self):
        """Test that relative paths without traversal are safe."""
        safe, msg = security.is_path_safe("data/file.txt")
        assert safe is True
        assert msg is None

    def test_path_traversal_blocked(self):
        """Test that path traversal attempts are blocked."""
        test_cases = [
            "../../../etc/passwd",
            "/home/user/../../../etc/passwd",
            "data/../../secret",
        ]
        for path in test_cases:
            safe, msg = security.is_path_safe(path)
            assert safe is False, f"Path should be blocked: {path}"
            assert "traversal" in msg.lower()

    def test_null_byte_blocked(self):
        """Test that null bytes in paths are blocked."""
        safe, msg = security.is_path_safe("/home/user/file.txt\x00.jpg")
        assert safe is False
        assert "null" in msg.lower()

    def test_url_encoded_traversal_blocked(self):
        """Test that URL-encoded traversal attempts are blocked."""
        test_cases = [
            "/home/%2e%2e/etc",
            "/home/%2E%2E/etc",
            "/home/user%2f..%2fetc",
        ]
        for path in test_cases:
            safe, msg = security.is_path_safe(path)
            assert safe is False, f"Path should be blocked: {path}"
            assert "encoded" in msg.lower()

    def test_very_long_path_blocked(self):
        """Test that excessively long paths are blocked."""
        long_path = "/home/user/" + "a" * 5000
        safe, msg = security.is_path_safe(long_path)
        assert safe is False
        assert "length" in msg.lower()


class TestFilenameSanitization:
    """Tests for filename sanitization."""

    def test_normal_filename_preserved(self):
        """Test that normal filenames are preserved."""
        filename = security.sanitize_filename("document.pdf")
        assert filename == "document.pdf"

    def test_path_components_removed(self):
        """Test that path components are stripped."""
        filename = security.sanitize_filename("/path/to/document.pdf")
        assert "/" not in filename
        assert filename == "document.pdf"

    def test_traversal_removed(self):
        """Test that traversal attempts in filename are removed."""
        filename = security.sanitize_filename("../../../etc/passwd")
        assert ".." not in filename
        assert "/" not in filename

    def test_hidden_files_prevented(self):
        """Test that leading dots are removed."""
        filename = security.sanitize_filename(".hidden")
        assert not filename.startswith(".")

    def test_dangerous_characters_replaced(self):
        """Test that dangerous characters are replaced."""
        filename = security.sanitize_filename('file<>:"|?*.txt')
        assert "<" not in filename
        assert ">" not in filename
        assert ":" not in filename
        assert '"' not in filename
        assert "|" not in filename
        assert "?" not in filename
        assert "*" not in filename

    def test_empty_filename_handled(self):
        """Test that empty filenames get a default."""
        filename = security.sanitize_filename("")
        assert filename == "unnamed_file"

    def test_long_filename_truncated(self):
        """Test that long filenames are truncated."""
        long_name = "a" * 500 + ".txt"
        filename = security.sanitize_filename(long_name)
        assert len(filename) <= 255
        assert filename.endswith(".txt")


class TestErrorSanitization:
    """Tests for error message sanitization."""

    def test_path_removed_from_error(self):
        """Test that full paths are removed from error messages."""
        error = Exception("Failed to read /home/user/secret/config.json")
        sanitized = security.sanitize_error_message(error)
        assert "/home/user/secret" not in sanitized
        assert "config.json" in sanitized

    def test_exception_type_included(self):
        """Test that exception type is included by default."""
        error = ValueError("Invalid value")
        sanitized = security.sanitize_error_message(error)
        assert "ValueError" in sanitized

    def test_exception_type_excluded(self):
        """Test that exception type can be excluded."""
        error = ValueError("Invalid value")
        sanitized = security.sanitize_error_message(error, include_type=False)
        assert "ValueError" not in sanitized
        assert "Invalid value" in sanitized

    def test_credentials_redacted(self):
        """Test that potential credentials are redacted."""
        error = Exception("Connection failed with password=secret123")
        sanitized = security.sanitize_error_message(error)
        assert "secret123" not in sanitized
        assert "[REDACTED]" in sanitized

    def test_long_message_truncated(self):
        """Test that very long messages are truncated."""
        error = Exception("x" * 1000)
        sanitized = security.sanitize_error_message(error)
        assert len(sanitized) < 600
        assert "..." in sanitized


class TestSegmentValidation:
    """Tests for segment count and data validation."""

    def test_segment_count_within_limit(self):
        """Test that valid segment counts pass."""
        segments = ["seg"] * 100
        # Should not raise
        security.validate_segment_count(segments)

    def test_segment_count_exceeds_limit(self):
        """Test that excessive segment counts are rejected."""
        segments = ["seg"] * (security.MAX_SEGMENT_COUNT + 1)
        with pytest.raises(HTTPException) as exc_info:
            security.validate_segment_count(segments)
        assert exc_info.value.status_code == 400
        assert "exceeds maximum" in str(exc_info.value.detail).lower()

    def test_custom_segment_limit(self):
        """Test that custom limits can be enforced."""
        segments = ["seg"] * 11
        with pytest.raises(HTTPException):
            security.validate_segment_count(segments, max_count=10)


class TestSegmentDataValidation:
    """Tests for individual segment data validation."""

    def test_valid_segment(self):
        """Test that valid segment data passes."""
        segment = {"start": 0.0, "end": 1.0, "label": "HELLO", "tier": "gloss"}
        validated = security.validate_segment_data(segment)
        assert validated["start"] == 0.0
        assert validated["end"] == 1.0
        assert validated["label"] == "HELLO"

    def test_missing_required_fields(self):
        """Test that missing required fields are rejected."""
        with pytest.raises(HTTPException):
            security.validate_segment_data({"label": "HELLO"})

    def test_negative_times_rejected(self):
        """Test that negative times are rejected."""
        with pytest.raises(HTTPException):
            security.validate_segment_data({"start": -1.0, "end": 1.0})

    def test_start_after_end_rejected(self):
        """Test that start > end is rejected."""
        with pytest.raises(HTTPException):
            security.validate_segment_data({"start": 5.0, "end": 1.0})

    def test_long_label_truncated(self):
        """Test that excessively long labels are truncated."""
        long_label = "A" * 1000
        segment = {"start": 0.0, "end": 1.0, "label": long_label}
        validated = security.validate_segment_data(segment)
        assert len(validated["label"]) == security.MAX_LABEL_LENGTH

    def test_long_tier_name_rejected(self):
        """Test that excessively long tier names are rejected."""
        long_tier = "A" * 200
        segment = {"start": 0.0, "end": 1.0, "tier": long_tier}
        with pytest.raises(HTTPException):
            security.validate_segment_data(segment)


class TestProtectedDirectories:
    """Tests for protected directory detection."""

    def test_root_is_protected(self):
        """Test that root directory is protected."""
        assert security.is_in_protected_directory(Path("/"))

    def test_system_dirs_protected(self):
        """Test that system directories are protected."""
        protected = ["/bin", "/etc", "/usr", "/sbin", "/boot"]
        for p in protected:
            assert security.is_in_protected_directory(Path(p))
            assert security.is_in_protected_directory(Path(f"{p}/subdir"))

    def test_home_not_protected(self):
        """Test that home directories are accessible."""
        assert not security.is_in_protected_directory(Path("/home/user"))
        assert not security.is_in_protected_directory(Path("/home/user/documents"))

    def test_tmp_not_protected(self):
        """Test that /tmp is accessible."""
        assert not security.is_in_protected_directory(Path("/tmp"))
        assert not security.is_in_protected_directory(Path("/tmp/uploads"))


class TestSecurityConstants:
    """Tests for security constants and limits."""

    def test_constants_are_reasonable(self):
        """Test that security constants have reasonable values."""
        assert security.MAX_SEGMENT_COUNT >= 1000
        assert security.MAX_FILE_PATH_LENGTH >= 256
        assert security.MAX_LABEL_LENGTH >= 100
        assert security.MAX_TIER_NAME_LENGTH >= 50
        assert security.MAX_H5_PATHS_PER_REQUEST >= 10
        assert security.MAX_UPLOAD_SIZE_MB >= 100

    def test_allowed_extensions_defined(self):
        """Test that allowed extension sets are defined."""
        assert ".mp4" in security.ALLOWED_VIDEO_EXTENSIONS
        assert ".eaf" in security.ALLOWED_ELAN_EXTENSIONS
        assert ".h5" in security.ALLOWED_POSE_EXTENSIONS


class TestCreateSafeErrorResponse:
    """Tests for safe error response creation."""

    def test_creates_http_exception(self):
        """Test that create_safe_error_response returns HTTPException."""
        error = ValueError("test error")
        result = security.create_safe_error_response(error, 400)
        assert isinstance(result, HTTPException)
        assert result.status_code == 400

    def test_includes_context(self):
        """Test that context is included in error."""
        error = ValueError("test error")
        result = security.create_safe_error_response(error, 500, "Processing failed")
        assert "Processing failed" in str(result.detail)

    def test_sanitizes_message(self):
        """Test that error message is sanitized."""
        error = Exception("Failed at /home/user/secret/file.txt")
        result = security.create_safe_error_response(error, 500)
        assert "/home/user/secret" not in str(result.detail)
