from datetime import datetime, timedelta
from unittest.mock import patch

import pytest
from fastapi import HTTPException
from jose import jwt

from app.core.security import (
    create_access_token,
    create_refresh_token,
    decode_token,
    get_password_hash,
    verify_password,
)


class TestJWTTokens:
    """Test JWT token creation and validation"""

    def test_create_access_token_default_expiry(self):
        """Test creating access token with default expiry"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            mock_settings.jwt_access_ttl_min = 15
            
            token = create_access_token("user123")
            
            # Decode to verify contents
            payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
            assert payload["sub"] == "user123"
            assert "exp" in payload
            assert "type" not in payload  # Access tokens don't have type

    def test_create_access_token_custom_expiry(self):
        """Test creating access token with custom expiry"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            
            custom_delta = timedelta(minutes=30)
            token = create_access_token("user123", expires_delta=custom_delta)
            
            payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
            
            # Check expiry is approximately 30 minutes from now
            exp_time = datetime.utcfromtimestamp(payload["exp"])
            expected_exp = datetime.utcnow() + custom_delta
            assert abs((exp_time - expected_exp).total_seconds()) < 5  # 5 second tolerance

    def test_create_refresh_token_default_expiry(self):
        """Test creating refresh token with default expiry"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            mock_settings.jwt_refresh_ttl_days = 7
            
            token = create_refresh_token("user123")
            
            payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
            assert payload["sub"] == "user123"
            assert payload["type"] == "refresh"
            assert "exp" in payload

    def test_create_refresh_token_custom_expiry(self):
        """Test creating refresh token with custom expiry"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            
            custom_delta = timedelta(days=14)
            token = create_refresh_token("user123", expires_delta=custom_delta)
            
            payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
            assert payload["type"] == "refresh"
            
            # Check expiry is approximately 14 days from now
            exp_time = datetime.utcfromtimestamp(payload["exp"])
            expected_exp = datetime.utcnow() + custom_delta
            assert abs((exp_time - expected_exp).total_seconds()) < 5

    def test_decode_token_valid(self):
        """Test decoding valid JWT token"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            
            # Create a token first
            original_payload = {"sub": "user123", "exp": datetime.utcnow() + timedelta(minutes=15)}
            token = jwt.encode(original_payload, "test_secret", algorithm="HS256")
            
            # Decode it
            payload = decode_token(token)
            assert payload["sub"] == "user123"

    def test_decode_token_invalid(self):
        """Test decoding invalid JWT token"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            
            with pytest.raises(HTTPException) as exc_info:
                decode_token("invalid.jwt.token")
            
            assert exc_info.value.status_code == 401
            assert "Could not validate credentials" in str(exc_info.value.detail)

    def test_decode_token_expired(self):
        """Test decoding expired JWT token"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            
            # Create expired token
            expired_payload = {"sub": "user123", "exp": datetime.utcnow() - timedelta(minutes=1)}
            expired_token = jwt.encode(expired_payload, "test_secret", algorithm="HS256")
            
            with pytest.raises(HTTPException) as exc_info:
                decode_token(expired_token)
            
            assert exc_info.value.status_code == 401

    def test_decode_token_wrong_secret(self):
        """Test decoding token with wrong secret"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "wrong_secret"
            mock_settings.jwt_alg = "HS256"
            
            # Create token with different secret
            payload = {"sub": "user123", "exp": datetime.utcnow() + timedelta(minutes=15)}
            token = jwt.encode(payload, "correct_secret", algorithm="HS256")
            
            with pytest.raises(HTTPException) as exc_info:
                decode_token(token)
            
            assert exc_info.value.status_code == 401


class TestPasswordHashing:
    """Test password hashing and verification"""

    def test_get_password_hash(self):
        """Test password hashing"""
        password = "test_password_123"
        hashed = get_password_hash(password)
        
        # Hash should be different from original password
        assert hashed != password
        
        # Hash should start with bcrypt identifier
        assert hashed.startswith("$2b$")

    def test_verify_password_correct(self):
        """Test password verification with correct password"""
        password = "test_password_123"
        hashed = get_password_hash(password)
        
        assert verify_password(password, hashed) is True

    def test_verify_password_incorrect(self):
        """Test password verification with incorrect password"""
        password = "test_password_123"
        wrong_password = "wrong_password"
        hashed = get_password_hash(password)
        
        assert verify_password(wrong_password, hashed) is False

    def test_password_hash_uniqueness(self):
        """Test that same password generates different hashes (due to salt)"""
        password = "test_password_123"
        hash1 = get_password_hash(password)
        hash2 = get_password_hash(password)
        
        # Hashes should be different due to salt
        assert hash1 != hash2
        
        # But both should verify correctly
        assert verify_password(password, hash1) is True
        assert verify_password(password, hash2) is True

    def test_verify_empty_password(self):
        """Test password verification with empty password"""
        hashed = get_password_hash("real_password")
        
        assert verify_password("", hashed) is False

    def test_verify_empty_hash(self):
        """Test password verification with empty hash"""
        # This should handle gracefully without crashing
        assert verify_password("test_password", "") is False


class TestTokenSecurity:
    """Test token security properties"""

    def test_token_contains_no_sensitive_data(self):
        """Test that tokens don't contain sensitive information"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            mock_settings.jwt_access_ttl_min = 15

            token = create_access_token("user123")
            payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
            
            # Token should only contain safe fields
            assert "password" not in payload
            assert "hashed_password" not in payload
            assert "email" not in payload
            assert payload["sub"] == "user123"

    def test_refresh_token_has_type_field(self):
        """Test that refresh tokens have type field"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            mock_settings.jwt_refresh_ttl_days = 7
            
            token = create_refresh_token("user123")
            payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
            
            assert payload["type"] == "refresh"

    def test_access_token_has_no_type_field(self):
        """Test that access tokens don't have type field"""
        with patch('app.core.security.settings') as mock_settings:
            mock_settings.jwt_secret = "test_secret"
            mock_settings.jwt_alg = "HS256"
            mock_settings.jwt_access_ttl_min = 15
            
            token = create_access_token("user123")
            payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
            
            assert "type" not in payload