"""
Unit tests for data validation utilities.

Tests the validators module in src/server/utils/validators.py.
"""

import pytest

from src.server.utils.validators import (
    ValidatorMixin,
    sanitize_filename,
    validate_anime_url,
    validate_backup_name,
    validate_config_data,
    validate_download_priority,
    validate_download_quality,
    validate_episode_range,
    validate_ip_address,
    validate_jwt_token,
    validate_language,
    validate_series_key,
    validate_series_key_or_folder,
    validate_series_name,
    validate_websocket_message,
)


class TestValidateSeriesKey:
    """Tests for validate_series_key function."""

    def test_valid_simple_key(self):
        """Test valid simple key."""
        assert validate_series_key("naruto") == "naruto"

    def test_valid_hyphenated_key(self):
        """Test valid hyphenated key."""
        assert validate_series_key("attack-on-titan") == "attack-on-titan"

    def test_valid_numeric_key(self):
        """Test valid key with numbers."""
        assert validate_series_key("one-piece-2024") == "one-piece-2024"

    def test_valid_key_starting_with_number(self):
        """Test valid key starting with number."""
        assert validate_series_key("86-eighty-six") == "86-eighty-six"

    def test_strips_whitespace(self):
        """Test that whitespace is stripped."""
        assert validate_series_key("  naruto  ") == "naruto"

    def test_empty_string_raises(self):
        """Test empty string raises ValueError."""
        with pytest.raises(ValueError, match="non-empty string"):
            validate_series_key("")

    def test_none_raises(self):
        """Test None raises ValueError."""
        with pytest.raises(ValueError, match="non-empty string"):
            validate_series_key(None)

    def test_whitespace_only_raises(self):
        """Test whitespace-only string raises ValueError."""
        with pytest.raises(ValueError, match="cannot be empty"):
            validate_series_key("   ")

    def test_uppercase_raises(self):
        """Test uppercase letters raise ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            validate_series_key("Attack-On-Titan")

    def test_spaces_raises(self):
        """Test spaces raise ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            validate_series_key("attack on titan")

    def test_underscores_raises(self):
        """Test underscores raise ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            validate_series_key("attack_on_titan")

    def test_special_characters_raises(self):
        """Test special characters raise ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            validate_series_key("attack@titan")

    def test_leading_hyphen_raises(self):
        """Test leading hyphen raises ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            validate_series_key("-attack-on-titan")

    def test_trailing_hyphen_raises(self):
        """Test trailing hyphen raises ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            validate_series_key("attack-on-titan-")

    def test_consecutive_hyphens_raises(self):
        """Test consecutive hyphens raise ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            validate_series_key("attack--on--titan")

    def test_key_too_long_raises(self):
        """Test key exceeding 255 chars raises ValueError."""
        long_key = "a" * 256
        with pytest.raises(ValueError, match="255 characters"):
            validate_series_key(long_key)

    def test_max_length_key(self):
        """Test key at exactly 255 chars is valid."""
        max_key = "a" * 255
        assert validate_series_key(max_key) == max_key


class TestValidateSeriesKeyOrFolder:
    """Tests for validate_series_key_or_folder function."""

    def test_valid_key_returns_key_true(self):
        """Test valid key returns (key, True)."""
        result = validate_series_key_or_folder("attack-on-titan")
        assert result == ("attack-on-titan", True)

    def test_valid_folder_returns_folder_false(self):
        """Test valid folder returns (folder, False)."""
        result = validate_series_key_or_folder("Attack on Titan (2013)")
        assert result == ("Attack on Titan (2013)", False)

    def test_empty_string_raises(self):
        """Test empty string raises ValueError."""
        with pytest.raises(ValueError, match="non-empty string"):
            validate_series_key_or_folder("")

    def test_none_raises(self):
        """Test None raises ValueError."""
        with pytest.raises(ValueError, match="non-empty string"):
            validate_series_key_or_folder(None)

    def test_whitespace_only_raises(self):
        """Test whitespace-only string raises ValueError."""
        with pytest.raises(ValueError, match="cannot be empty"):
            validate_series_key_or_folder("   ")

    def test_folder_not_allowed_raises(self):
        """Test folder format raises when not allowed."""
        with pytest.raises(ValueError, match="Invalid series key format"):
            validate_series_key_or_folder(
                "Attack on Titan (2013)", allow_folder=False
            )

    def test_key_allowed_when_folder_disabled(self):
        """Test valid key works when folder is disabled."""
        result = validate_series_key_or_folder(
            "attack-on-titan", allow_folder=False
        )
        assert result == ("attack-on-titan", True)

    def test_strips_whitespace(self):
        """Test that whitespace is stripped."""
        result = validate_series_key_or_folder("  attack-on-titan  ")
        assert result == ("attack-on-titan", True)

    def test_folder_too_long_raises(self):
        """Test folder exceeding 1000 chars raises ValueError."""
        long_folder = "A" * 1001
        with pytest.raises(ValueError, match="too long"):
            validate_series_key_or_folder(long_folder)


class TestValidateSeriesName:
    """Tests for validate_series_name function."""

    def test_valid_name(self):
        """Test valid series name."""
        assert validate_series_name("Attack on Titan") == "Attack on Titan"

    def test_strips_whitespace(self):
        """Test whitespace is stripped."""
        assert validate_series_name("  Naruto  ") == "Naruto"

    def test_empty_raises(self):
        """Test empty name raises ValueError."""
        with pytest.raises(ValueError, match="cannot be empty"):
            validate_series_name("")

    def test_too_long_raises(self):
        """Test name over 200 chars raises ValueError."""
        with pytest.raises(ValueError, match="too long"):
            validate_series_name("A" * 201)

    def test_invalid_chars_raises(self):
        """Test invalid characters raise ValueError."""
        with pytest.raises(ValueError, match="invalid character"):
            validate_series_name("Attack: Titan")


class TestValidateEpisodeRange:
    """Tests for validate_episode_range function."""

    def test_valid_range(self):
        """Test valid episode range."""
        assert validate_episode_range(1, 10) == (1, 10)

    def test_same_start_end(self):
        """Test start equals end is valid."""
        assert validate_episode_range(5, 5) == (5, 5)

    def test_start_less_than_one_raises(self):
        """Test start less than 1 raises ValueError."""
        with pytest.raises(ValueError, match="at least 1"):
            validate_episode_range(0, 10)

    def test_end_less_than_start_raises(self):
        """Test end less than start raises ValueError."""
        with pytest.raises(ValueError, match="greater than or equal"):
            validate_episode_range(10, 5)

    def test_range_too_large_raises(self):
        """Test range over 1000 raises ValueError."""
        with pytest.raises(ValueError, match="too large"):
            validate_episode_range(1, 1002)


class TestValidateDownloadQuality:
    """Tests for validate_download_quality function."""

    @pytest.mark.parametrize("quality", [
        "360p", "480p", "720p", "1080p", "best", "worst"
    ])
    def test_valid_qualities(self, quality):
        """Test all valid quality values."""
        assert validate_download_quality(quality) == quality

    def test_invalid_quality_raises(self):
        """Test invalid quality raises ValueError."""
        with pytest.raises(ValueError, match="Invalid quality"):
            validate_download_quality("4k")


class TestValidateLanguage:
    """Tests for validate_language function."""

    @pytest.mark.parametrize("language", [
        "ger-sub", "ger-dub", "eng-sub", "eng-dub", "jpn"
    ])
    def test_valid_languages(self, language):
        """Test all valid language values."""
        assert validate_language(language) == language

    def test_invalid_language_raises(self):
        """Test invalid language raises ValueError."""
        with pytest.raises(ValueError, match="Invalid language"):
            validate_language("spanish")


class TestValidateDownloadPriority:
    """Tests for validate_download_priority function."""

    def test_valid_priority_min(self):
        """Test minimum priority is valid."""
        assert validate_download_priority(0) == 0

    def test_valid_priority_max(self):
        """Test maximum priority is valid."""
        assert validate_download_priority(10) == 10

    def test_negative_priority_raises(self):
        """Test negative priority raises ValueError."""
        with pytest.raises(ValueError, match="between 0 and 10"):
            validate_download_priority(-1)

    def test_priority_too_high_raises(self):
        """Test priority over 10 raises ValueError."""
        with pytest.raises(ValueError, match="between 0 and 10"):
            validate_download_priority(11)


class TestValidateAnimeUrl:
    """Tests for validate_anime_url function."""

    def test_valid_aniworld_url(self):
        """Test valid aniworld.to URL."""
        url = "https://aniworld.to/anime/stream/attack-on-titan"
        assert validate_anime_url(url) == url

    def test_valid_s_to_url(self):
        """Test valid s.to URL."""
        url = "https://s.to/serie/stream/naruto"
        assert validate_anime_url(url) == url

    def test_empty_url_raises(self):
        """Test empty URL raises ValueError."""
        with pytest.raises(ValueError, match="cannot be empty"):
            validate_anime_url("")

    def test_invalid_domain_raises(self):
        """Test invalid domain raises ValueError."""
        with pytest.raises(ValueError, match="aniworld.to or s.to"):
            validate_anime_url("https://example.com/anime")


class TestValidateBackupName:
    """Tests for validate_backup_name function."""

    def test_valid_backup_name(self):
        """Test valid backup name."""
        assert validate_backup_name("backup-2024.json") == "backup-2024.json"

    def test_empty_raises(self):
        """Test empty name raises ValueError."""
        with pytest.raises(ValueError, match="cannot be empty"):
            validate_backup_name("")

    def test_invalid_chars_raises(self):
        """Test invalid characters raise ValueError."""
        with pytest.raises(ValueError, match="only contain"):
            validate_backup_name("backup name.json")

    def test_no_json_extension_raises(self):
        """Test missing .json raises ValueError."""
        with pytest.raises(ValueError, match="end with .json"):
            validate_backup_name("backup.txt")


class TestValidateConfigData:
    """Tests for validate_config_data function."""

    def test_valid_config(self):
        """Test valid config data."""
        data = {
            "download_directory": "/downloads",
            "concurrent_downloads": 3
        }
        assert validate_config_data(data) == data

    def test_missing_keys_raises(self):
        """Test missing required keys raises ValueError."""
        with pytest.raises(ValueError, match="missing required keys"):
            validate_config_data({"download_directory": "/downloads"})

    def test_invalid_concurrent_downloads_raises(self):
        """Test invalid concurrent_downloads raises ValueError."""
        with pytest.raises(ValueError, match="between 1 and 10"):
            validate_config_data({
                "download_directory": "/downloads",
                "concurrent_downloads": 15
            })


class TestSanitizeFilename:
    """Tests for sanitize_filename function."""

    def test_valid_filename(self):
        """Test valid filename unchanged."""
        assert sanitize_filename("episode-01.mp4") == "episode-01.mp4"

    def test_removes_invalid_chars(self):
        """Test invalid characters are replaced."""
        result = sanitize_filename("file<>name.mp4")
        assert "<" not in result
        assert ">" not in result

    def test_strips_dots_spaces(self):
        """Test leading/trailing dots and spaces removed."""
        assert sanitize_filename("  .filename.  ") == "filename"

    def test_empty_becomes_unnamed(self):
        """Test empty filename becomes 'unnamed'."""
        assert sanitize_filename("") == "unnamed"


class TestValidateJwtToken:
    """Tests for validate_jwt_token function."""

    def test_valid_token_format(self):
        """Test valid JWT token format."""
        token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"  # noqa: E501
        assert validate_jwt_token(token) == token

    def test_empty_raises(self):
        """Test empty token raises ValueError."""
        with pytest.raises(ValueError, match="non-empty string"):
            validate_jwt_token("")

    def test_invalid_format_raises(self):
        """Test invalid format raises ValueError."""
        with pytest.raises(ValueError, match="Invalid JWT"):
            validate_jwt_token("not-a-jwt-token")


class TestValidateIpAddress:
    """Tests for validate_ip_address function."""

    def test_valid_ipv4(self):
        """Test valid IPv4 address."""
        assert validate_ip_address("192.168.1.1") == "192.168.1.1"

    def test_valid_ipv4_localhost(self):
        """Test localhost IPv4."""
        assert validate_ip_address("127.0.0.1") == "127.0.0.1"

    def test_empty_raises(self):
        """Test empty IP raises ValueError."""
        with pytest.raises(ValueError, match="non-empty string"):
            validate_ip_address("")

    def test_invalid_ip_raises(self):
        """Test invalid IP raises ValueError."""
        with pytest.raises(ValueError, match="Invalid IP"):
            validate_ip_address("not-an-ip")


class TestValidateWebsocketMessage:
    """Tests for validate_websocket_message function."""

    def test_valid_message(self):
        """Test valid WebSocket message."""
        msg = {"type": "download_progress", "data": {}}
        assert validate_websocket_message(msg) == msg

    def test_missing_type_raises(self):
        """Test missing type raises ValueError."""
        with pytest.raises(ValueError, match="missing required keys"):
            validate_websocket_message({"data": {}})

    def test_invalid_type_raises(self):
        """Test invalid type raises ValueError."""
        with pytest.raises(ValueError, match="Invalid message type"):
            validate_websocket_message({"type": "invalid_type"})


class TestValidatorMixin:
    """Tests for ValidatorMixin class methods."""

    def test_validate_password_strength_valid(self):
        """Test valid password passes."""
        password = "SecurePass123!"
        assert ValidatorMixin.validate_password_strength(password) == password

    def test_validate_password_too_short_raises(self):
        """Test short password raises ValueError."""
        with pytest.raises(ValueError, match="8 characters"):
            ValidatorMixin.validate_password_strength("Short1!")

    def test_validate_password_no_uppercase_raises(self):
        """Test no uppercase raises ValueError."""
        with pytest.raises(ValueError, match="uppercase"):
            ValidatorMixin.validate_password_strength("lowercase123!")

    def test_validate_password_no_lowercase_raises(self):
        """Test no lowercase raises ValueError."""
        with pytest.raises(ValueError, match="lowercase"):
            ValidatorMixin.validate_password_strength("UPPERCASE123!")

    def test_validate_password_no_digit_raises(self):
        """Test no digit raises ValueError."""
        with pytest.raises(ValueError, match="digit"):
            ValidatorMixin.validate_password_strength("NoDigitsHere!")

    def test_validate_password_no_special_raises(self):
        """Test no special char raises ValueError."""
        with pytest.raises(ValueError, match="special character"):
            ValidatorMixin.validate_password_strength("NoSpecial123")

    def test_validate_url_valid(self):
        """Test valid URL."""
        url = "https://example.com/path"
        assert ValidatorMixin.validate_url(url) == url

    def test_validate_url_invalid_raises(self):
        """Test invalid URL raises ValueError."""
        with pytest.raises(ValueError, match="Invalid URL"):
            ValidatorMixin.validate_url("not-a-url")

    def test_validate_port_valid(self):
        """Test valid port."""
        assert ValidatorMixin.validate_port(8080) == 8080

    def test_validate_port_invalid_raises(self):
        """Test invalid port raises ValueError."""
        with pytest.raises(ValueError, match="between 1 and 65535"):
            ValidatorMixin.validate_port(70000)

    def test_validate_positive_integer_valid(self):
        """Test valid positive integer."""
        assert ValidatorMixin.validate_positive_integer(5) == 5

    def test_validate_positive_integer_zero_raises(self):
        """Test zero raises ValueError."""
        with pytest.raises(ValueError, match="must be positive"):
            ValidatorMixin.validate_positive_integer(0)

    def test_validate_non_negative_integer_valid(self):
        """Test valid non-negative integer."""
        assert ValidatorMixin.validate_non_negative_integer(0) == 0

    def test_validate_non_negative_integer_negative_raises(self):
        """Test negative raises ValueError."""
        with pytest.raises(ValueError, match="cannot be negative"):
            ValidatorMixin.validate_non_negative_integer(-1)

    def test_validate_string_length_valid(self):
        """Test valid string length."""
        result = ValidatorMixin.validate_string_length("test", 1, 10)
        assert result == "test"

    def test_validate_string_length_too_short_raises(self):
        """Test too short raises ValueError."""
        with pytest.raises(ValueError, match="at least"):
            ValidatorMixin.validate_string_length("ab", min_length=5)

    def test_validate_string_length_too_long_raises(self):
        """Test too long raises ValueError."""
        with pytest.raises(ValueError, match="at most"):
            ValidatorMixin.validate_string_length("abcdefgh", max_length=5)

    def test_validate_choice_valid(self):
        """Test valid choice."""
        result = ValidatorMixin.validate_choice("a", ["a", "b", "c"])
        assert result == "a"

    def test_validate_choice_invalid_raises(self):
        """Test invalid choice raises ValueError."""
        with pytest.raises(ValueError, match="must be one of"):
            ValidatorMixin.validate_choice("d", ["a", "b", "c"])

    def test_validate_dict_keys_valid(self):
        """Test valid dict with required keys."""
        data = {"a": 1, "b": 2}
        result = ValidatorMixin.validate_dict_keys(data, ["a", "b"])
        assert result == data

    def test_validate_dict_keys_missing_raises(self):
        """Test missing keys raises ValueError."""
        with pytest.raises(ValueError, match="missing required keys"):
            ValidatorMixin.validate_dict_keys({"a": 1}, ["a", "b"])
