"""Unit tests for Settings configuration and validation.

Tests cover:
- Environment variable parsing
- Default values application
- Settings validation
- CORS origins parsing
- Secret generation
- Configuration properties
"""
import os
from unittest.mock import patch

import pytest
from pydantic import ValidationError

from src.config.settings import Settings, settings


class TestSettingsDefaults:
    """Tests for default settings values."""
    
    def test_jwt_secret_key_generated(self):
        """Test that JWT secret key is auto-generated if not provided."""
        s = Settings()
        assert s.jwt_secret_key is not None
        assert len(s.jwt_secret_key) > 0
    
    def test_jwt_secret_key_unique(self):
        """Test that each Settings instance gets unique JWT key."""
        s1 = Settings()
        s2 = Settings()
        assert s1.jwt_secret_key != s2.jwt_secret_key
    
    def test_password_salt_default(self):
        """Test default password salt."""
        s = Settings()
        assert s.password_salt == "default-salt"
    
    def test_master_password_hash_default_none(self):
        """Test master password hash defaults to None."""
        s = Settings()
        assert s.master_password_hash is None
    
    def test_master_password_default_none(self):
        """Test master password defaults to None."""
        s = Settings()
        assert s.master_password is None
    
    def test_token_expiry_hours_default(self):
        """Test default token expiry hours."""
        s = Settings()
        assert s.token_expiry_hours == 24
    
    def test_anime_directory_default(self):
        """Test anime directory defaults to empty string."""
        s = Settings()
        assert s.anime_directory == ""
    
    def test_log_level_default(self):
        """Test default log level."""
        s = Settings()
        assert s.log_level == "INFO"
    
    def test_database_url_default(self):
        """Test default database URL."""
        s = Settings()
        assert s.database_url == "sqlite:///./data/aniworld.db"
    
    def test_cors_origins_default(self):
        """Test default CORS origins."""
        s = Settings()
        assert s.cors_origins == "http://localhost:3000"
    
    def test_api_rate_limit_default(self):
        """Test default API rate limit."""
        s = Settings()
        assert s.api_rate_limit == 100
    
    def test_default_provider_default(self):
        """Test default provider."""
        s = Settings()
        assert s.default_provider == "aniworld.to"
    
    def test_provider_timeout_default(self):
        """Test default provider timeout."""
        s = Settings()
        assert s.provider_timeout == 30
    
    def test_retry_attempts_default(self):
        """Test default retry attempts."""
        s = Settings()
        assert s.retry_attempts == 3


class TestNFOSettingsDefaults:
    """Tests for NFO-related settings defaults."""
    
    def test_tmdb_api_key_default_none(self):
        """Test TMDB API key defaults to None."""
        s = Settings()
        assert s.tmdb_api_key is None
    
    def test_nfo_auto_create_default(self):
        """Test NFO auto-create defaults to False."""
        s = Settings()
        assert s.nfo_auto_create is False
    
    def test_nfo_update_on_scan_default(self):
        """Test NFO update on scan defaults to False."""
        s = Settings()
        assert s.nfo_update_on_scan is False
    
    def test_nfo_download_poster_default(self):
        """Test NFO download poster defaults to True."""
        s = Settings()
        assert s.nfo_download_poster is True
    
    def test_nfo_download_logo_default(self):
        """Test NFO download logo defaults to True."""
        s = Settings()
        assert s.nfo_download_logo is True
    
    def test_nfo_download_fanart_default(self):
        """Test NFO download fanart defaults to True."""
        s = Settings()
        assert s.nfo_download_fanart is True
    
    def test_nfo_image_size_default(self):
        """Test NFO image size defaults to 'original'."""
        s = Settings()
        assert s.nfo_image_size == "original"
    
    def test_nfo_prefer_fsk_rating_default(self):
        """Test NFO prefer FSK rating defaults to True."""
        s = Settings()
        assert s.nfo_prefer_fsk_rating is True


class TestEnvironmentVariableParsing:
    """Tests for environment variable parsing."""
    
    def test_jwt_secret_key_from_env(self):
        """Test loading JWT secret key from environment."""
        with patch.dict(os.environ, {"JWT_SECRET_KEY": "test-secret-key"}):
            s = Settings()
            assert s.jwt_secret_key == "test-secret-key"
    
    def test_password_salt_from_env(self):
        """Test loading password salt from environment."""
        with patch.dict(os.environ, {"PASSWORD_SALT": "custom-salt"}):
            s = Settings()
            assert s.password_salt == "custom-salt"
    
    def test_master_password_hash_from_env(self):
        """Test loading master password hash from environment."""
        with patch.dict(os.environ, {"MASTER_PASSWORD_HASH": "hash123"}):
            s = Settings()
            assert s.master_password_hash == "hash123"
    
    def test_master_password_from_env(self):
        """Test loading master password from environment."""
        with patch.dict(os.environ, {"MASTER_PASSWORD": "dev-password"}):
            s = Settings()
            assert s.master_password == "dev-password"
    
    def test_token_expiry_hours_from_env(self):
        """Test loading token expiry from environment."""
        with patch.dict(os.environ, {"SESSION_TIMEOUT_HOURS": "48"}):
            s = Settings()
            assert s.token_expiry_hours == 48
    
    def test_anime_directory_from_env(self):
        """Test loading anime directory from environment."""
        with patch.dict(os.environ, {"ANIME_DIRECTORY": "/media/anime"}):
            s = Settings()
            assert s.anime_directory == "/media/anime"
    
    def test_log_level_from_env(self):
        """Test loading log level from environment."""
        with patch.dict(os.environ, {"LOG_LEVEL": "DEBUG"}):
            s = Settings()
            assert s.log_level == "DEBUG"
    
    def test_database_url_from_env(self):
        """Test loading database URL from environment."""
        with patch.dict(os.environ, {"DATABASE_URL": "postgresql://localhost/aniworld"}):
            s = Settings()
            assert s.database_url == "postgresql://localhost/aniworld"
    
    def test_cors_origins_from_env(self):
        """Test loading CORS origins from environment."""
        with patch.dict(os.environ, {"CORS_ORIGINS": "http://example.com"}):
            s = Settings()
            assert s.cors_origins == "http://example.com"
    
    def test_api_rate_limit_from_env(self):
        """Test loading API rate limit from environment."""
        with patch.dict(os.environ, {"API_RATE_LIMIT": "200"}):
            s = Settings()
            assert s.api_rate_limit == 200
    
    def test_default_provider_from_env(self):
        """Test loading default provider from environment."""
        with patch.dict(os.environ, {"DEFAULT_PROVIDER": "custom.provider"}):
            s = Settings()
            assert s.default_provider == "custom.provider"
    
    def test_provider_timeout_from_env(self):
        """Test loading provider timeout from environment."""
        with patch.dict(os.environ, {"PROVIDER_TIMEOUT": "60"}):
            s = Settings()
            assert s.provider_timeout == 60
    
    def test_retry_attempts_from_env(self):
        """Test loading retry attempts from environment."""
        with patch.dict(os.environ, {"RETRY_ATTEMPTS": "5"}):
            s = Settings()
            assert s.retry_attempts == 5
    
    def test_tmdb_api_key_from_env(self):
        """Test loading TMDB API key from environment."""
        with patch.dict(os.environ, {"TMDB_API_KEY": "tmdb-key-123"}):
            s = Settings()
            assert s.tmdb_api_key == "tmdb-key-123"


class TestNFOEnvironmentVariables:
    """Tests for NFO settings from environment."""
    
    def test_nfo_auto_create_from_env_true(self):
        """Test loading NFO auto create as true from environment."""
        with patch.dict(os.environ, {"NFO_AUTO_CREATE": "true"}):
            s = Settings()
            assert s.nfo_auto_create is True
    
    def test_nfo_auto_create_from_env_false(self):
        """Test loading NFO auto create as false from environment."""
        with patch.dict(os.environ, {"NFO_AUTO_CREATE": "false"}):
            s = Settings()
            assert s.nfo_auto_create is False
    
    def test_nfo_update_on_scan_from_env(self):
        """Test loading NFO update on scan from environment."""
        with patch.dict(os.environ, {"NFO_UPDATE_ON_SCAN": "true"}):
            s = Settings()
            assert s.nfo_update_on_scan is True
    
    def test_nfo_download_poster_from_env_false(self):
        """Test loading NFO download poster as false."""
        with patch.dict(os.environ, {"NFO_DOWNLOAD_POSTER": "false"}):
            s = Settings()
            assert s.nfo_download_poster is False
    
    def test_nfo_download_logo_from_env_false(self):
        """Test loading NFO download logo as false."""
        with patch.dict(os.environ, {"NFO_DOWNLOAD_LOGO": "false"}):
            s = Settings()
            assert s.nfo_download_logo is False
    
    def test_nfo_download_fanart_from_env_false(self):
        """Test loading NFO download fanart as false."""
        with patch.dict(os.environ, {"NFO_DOWNLOAD_FANART": "false"}):
            s = Settings()
            assert s.nfo_download_fanart is False
    
    def test_nfo_image_size_from_env(self):
        """Test loading NFO image size from environment."""
        with patch.dict(os.environ, {"NFO_IMAGE_SIZE": "w500"}):
            s = Settings()
            assert s.nfo_image_size == "w500"
    
    def test_nfo_prefer_fsk_rating_from_env_false(self):
        """Test loading NFO prefer FSK rating as false."""
        with patch.dict(os.environ, {"NFO_PREFER_FSK_RATING": "false"}):
            s = Settings()
            assert s.nfo_prefer_fsk_rating is False


class TestAllowedOriginsProperty:
    """Tests for allowed_origins property."""
    
    def test_allowed_origins_single(self):
        """Test parsing single CORS origin."""
        with patch.dict(os.environ, {"CORS_ORIGINS": "http://localhost:3000"}):
            s = Settings()
            origins = s.allowed_origins
            assert origins == ["http://localhost:3000"]
    
    def test_allowed_origins_multiple(self):
        """Test parsing multiple CORS origins."""
        with patch.dict(os.environ, {"CORS_ORIGINS": "http://localhost:3000,http://example.com"}):
            s = Settings()
            origins = s.allowed_origins
            assert len(origins) == 2
            assert "http://localhost:3000" in origins
            assert "http://example.com" in origins
    
    def test_allowed_origins_with_spaces(self):
        """Test parsing CORS origins with extra spaces."""
        with patch.dict(os.environ, {"CORS_ORIGINS": "http://localhost:3000 , http://example.com "}):
            s = Settings()
            origins = s.allowed_origins
            assert len(origins) == 2
            assert "http://localhost:3000" in origins
            assert "http://example.com" in origins
    
    def test_allowed_origins_wildcard_safe_fallback(self):
        """Test that wildcard falls back to safe defaults."""
        with patch.dict(os.environ, {"CORS_ORIGINS": "*"}):
            s = Settings()
            origins = s.allowed_origins
            assert "http://localhost:3000" in origins
            assert "http://localhost:8000" in origins
            # Should not allow all origins
            assert "*" not in origins
    
    def test_allowed_origins_empty_string(self):
        """Test parsing empty CORS origins."""
        with patch.dict(os.environ, {"CORS_ORIGINS": ""}):
            s = Settings()
            origins = s.allowed_origins
            assert origins == []
    
    def test_allowed_origins_whitespace_only(self):
        """Test parsing whitespace-only CORS origins."""
        with patch.dict(os.environ, {"CORS_ORIGINS": "   "}):
            s = Settings()
            origins = s.allowed_origins
            assert origins == []
    
    def test_allowed_origins_filters_empty_items(self):
        """Test that empty items are filtered from comma-separated list."""
        with patch.dict(os.environ, {"CORS_ORIGINS": "http://localhost:3000,,http://example.com,"}):
            s = Settings()
            origins = s.allowed_origins
            assert len(origins) == 2
            assert "http://localhost:3000" in origins
            assert "http://example.com" in origins


class TestSettingsValidation:
    """Tests for settings validation."""
    
    def test_invalid_token_expiry_hours_type(self):
        """Test that invalid token expiry type raises validation error."""
        with patch.dict(os.environ, {"SESSION_TIMEOUT_HOURS": "not-a-number"}):
            with pytest.raises(ValidationError):
                Settings()
    
    def test_invalid_api_rate_limit_type(self):
        """Test that invalid rate limit type raises validation error."""
        with patch.dict(os.environ, {"API_RATE_LIMIT": "not-a-number"}):
            with pytest.raises(ValidationError):
                Settings()
    
    def test_invalid_provider_timeout_type(self):
        """Test that invalid timeout type raises validation error."""
        with patch.dict(os.environ, {"PROVIDER_TIMEOUT": "not-a-number"}):
            with pytest.raises(ValidationError):
                Settings()
    
    def test_invalid_retry_attempts_type(self):
        """Test that invalid retry attempts type raises validation error."""
        with patch.dict(os.environ, {"RETRY_ATTEMPTS": "not-a-number"}):
            with pytest.raises(ValidationError):
                Settings()
    
    def test_invalid_boolean_value(self):
        """Test that invalid boolean raises validation error."""
        with patch.dict(os.environ, {"NFO_AUTO_CREATE": "not-a-boolean"}):
            with pytest.raises(ValidationError):
                Settings()


class TestGlobalSettingsInstance:
    """Tests for global settings instance."""
    
    def test_settings_instance_exists(self):
        """Test that global settings instance is created."""
        assert settings is not None
        assert isinstance(settings, Settings)
    
    def test_settings_has_jwt_secret_key(self):
        """Test that global settings has JWT secret key."""
        assert settings.jwt_secret_key is not None
        assert len(settings.jwt_secret_key) > 0
    
    def test_settings_has_default_database_url(self):
        """Test that global settings has default database URL."""
        # Will be default unless overridden
        assert settings.database_url is not None


class TestSettingsExtraIgnored:
    """Tests for extra fields handling."""
    
    def test_unknown_env_vars_ignored(self):
        """Test that unknown environment variables are ignored."""
        with patch.dict(os.environ, {"UNKNOWN_SETTING": "value", "JWT_SECRET_KEY": "test-key"}):
            s = Settings()
            assert s.jwt_secret_key == "test-key"
            # Should not raise error for unknown setting
            assert not hasattr(s, "UNKNOWN_SETTING")
            assert not hasattr(s, "unknown_setting")


class TestSettingsEdgeCases:
    """Tests for edge cases in settings."""
    
    def test_numeric_string_values(self):
        """Test that numeric strings are correctly parsed."""
        with patch.dict(os.environ, {
            "API_RATE_LIMIT": "250",
            "PROVIDER_TIMEOUT": "45",
            "RETRY_ATTEMPTS": "7"
        }):
            s = Settings()
            assert s.api_rate_limit == 250
            assert s.provider_timeout == 45
            assert s.retry_attempts == 7
    
    def test_boolean_string_variations(self):
        """Test different boolean string representations."""
        # Test true variations
        with patch.dict(os.environ, {"NFO_AUTO_CREATE": "1"}):
            s = Settings()
            assert s.nfo_auto_create is True
        
        with patch.dict(os.environ, {"NFO_AUTO_CREATE": "yes"}):
            s = Settings()
            assert s.nfo_auto_create is True
        
        # Test false variations
        with patch.dict(os.environ, {"NFO_AUTO_CREATE": "0"}):
            s = Settings()
            assert s.nfo_auto_create is False
        
        with patch.dict(os.environ, {"NFO_AUTO_CREATE": "no"}):
            s = Settings()
            assert s.nfo_auto_create is False
    
    def test_empty_anime_directory_valid(self):
        """Test that empty anime directory is valid."""
        with patch.dict(os.environ, {"ANIME_DIRECTORY": ""}):
            s = Settings()
            assert s.anime_directory == ""
    
    def test_path_with_spaces(self):
        """Test that paths with spaces are preserved."""
        with patch.dict(os.environ, {"ANIME_DIRECTORY": "/media/my anime"}):
            s = Settings()
            assert s.anime_directory == "/media/my anime"
    
    def test_url_formats(self):
        """Test various database URL formats."""
        urls = [
            "sqlite:///./data/aniworld.db",
            "postgresql://user:pass@localhost/db",
            "mysql://localhost:3306/aniworld",
        ]
        
        for url in urls:
            with patch.dict(os.environ, {"DATABASE_URL": url}):
                s = Settings()
                assert s.database_url == url
    
    def test_log_level_case_insensitive(self):
        """Test that log level accepts different cases."""
        levels = ["DEBUG", "debug", "Info", "WARNING", "ERROR"]
        
        for level in levels:
            with patch.dict(os.environ, {"LOG_LEVEL": level}):
                s = Settings()
                assert s.log_level == level


class TestSecurityConsiderations:
    """Tests for security-related settings behavior."""
    
    def test_jwt_secret_is_different_each_time(self):
        """Test that JWT secret is not predictable."""
        secrets_generated = set()
        
        for _ in range(10):
            s = Settings()
            secrets_generated.add(s.jwt_secret_key)
        
        # All should be unique
        assert len(secrets_generated) == 10
    
    def test_master_password_warning_documented(self):
        """Test that master_password field has warning documentation."""
        # Check that the field metadata includes warning
        field_info = Settings.model_fields["master_password"]
        description = field_info.description
        assert description is not None
        assert "DEVELOPMENT ONLY" in description
        assert "NEVER" in description.upper()
    
    def test_both_password_fields_can_be_none(self):
        """Test that both password fields being None is valid."""
        s = Settings()
        assert s.master_password is None
        assert s.master_password_hash is None
        # This is valid - password will be set during setup
