"""
Unit tests for JWT authentication utilities

Tests cover token creation, verification, expiration, and error handling.
"""
import pytest
from jose import jwt, JWTError
import pendulum
from app.utils.auth import create_token, verify_token
from app.config import get_settings


class TestTokenCreation:
    """Tests for JWT token creation"""

    def test_create_token_basic(self):
        """Test creating a token with valid user data"""
        user_data = {"user_id": "123", "username": "testuser"}
        token = create_token(user_data)

        assert token is not None
        assert isinstance(token, str)
        assert len(token) > 0

    def test_create_token_includes_user_data(self):
        """Test token contains all user data"""
        user_data = {
            "user_id": "123",
            "username": "testuser",
            "discord_id": "456789"
        }
        token = create_token(user_data)
        payload = verify_token(token)

        assert payload["user_id"] == "123"
        assert payload["username"] == "testuser"
        assert payload["discord_id"] == "456789"

    def test_create_token_includes_expiration(self):
        """Test token has expiration timestamp"""
        user_data = {"user_id": "123"}
        token = create_token(user_data)
        payload = verify_token(token)

        assert "exp" in payload
        assert isinstance(payload["exp"], int)

        # Verify expiration is ~7 days from now
        exp_time = pendulum.from_timestamp(payload["exp"])
        now = pendulum.now('UTC')
        diff = exp_time - now

        assert diff.days >= 6  # Allow for timing variance
        assert diff.days <= 8

    def test_create_token_with_empty_user_data(self):
        """Test creating token with empty user data"""
        user_data = {}
        token = create_token(user_data)

        assert token is not None
        payload = verify_token(token)
        assert "exp" in payload  # Should still have expiration

    def test_create_token_with_complex_data(self):
        """Test creating token with nested/complex user data"""
        user_data = {
            "user_id": "123",
            "roles": ["player", "admin"],
            "metadata": {"league": "sba", "team_id": 5}
        }
        token = create_token(user_data)
        payload = verify_token(token)

        assert payload["user_id"] == "123"
        assert payload["roles"] == ["player", "admin"]
        assert payload["metadata"]["league"] == "sba"


class TestTokenVerification:
    """Tests for JWT token verification"""

    def test_verify_valid_token(self):
        """Test verifying a valid token"""
        user_data = {"user_id": "123", "username": "testuser"}
        token = create_token(user_data)

        payload = verify_token(token)

        assert payload["user_id"] == "123"
        assert payload["username"] == "testuser"

    def test_verify_invalid_token_raises_error(self):
        """Test verifying an invalid token raises JWTError"""
        invalid_token = "invalid.token.here"

        with pytest.raises(JWTError):
            verify_token(invalid_token)

    def test_verify_malformed_token(self):
        """Test verifying malformed tokens"""
        malformed_tokens = [
            "",
            "notatoken",
            "a.b",  # Missing part
            "header.payload",  # Missing signature
            "a.b.c.d",  # Too many parts
        ]

        for token in malformed_tokens:
            with pytest.raises(JWTError):
                verify_token(token)

    def test_verify_token_wrong_signature(self):
        """Test verifying token with tampered signature"""
        user_data = {"user_id": "123"}
        token = create_token(user_data)

        # Tamper with signature (last part of JWT)
        parts = token.split('.')
        parts[2] = parts[2][:-5] + "WRONG"
        tampered_token = '.'.join(parts)

        with pytest.raises(JWTError):
            verify_token(tampered_token)

    def test_verify_token_wrong_algorithm(self):
        """Test token signed with different algorithm fails"""
        settings = get_settings()
        user_data = {"user_id": "123"}

        # Create token with different algorithm
        payload = {
            **user_data,
            "exp": pendulum.now('UTC').add(days=7).int_timestamp
        }
        # Try to decode HS512 token as HS256 (should fail)
        wrong_alg_token = jwt.encode(payload, settings.secret_key, algorithm="HS512")

        with pytest.raises(JWTError):
            verify_token(wrong_alg_token)

    def test_verify_token_wrong_secret_key(self):
        """Test token signed with different secret fails"""
        user_data = {"user_id": "123"}

        # Create token with different secret
        payload = {
            **user_data,
            "exp": pendulum.now('UTC').add(days=7).int_timestamp
        }
        wrong_secret_token = jwt.encode(payload, "wrong-secret-key", algorithm="HS256")

        with pytest.raises(JWTError):
            verify_token(wrong_secret_token)


class TestTokenExpiration:
    """Tests for token expiration behavior"""

    def test_expired_token_raises_error(self):
        """Test that expired token raises JWTError"""
        settings = get_settings()
        user_data = {"user_id": "123"}

        # Create already-expired token (expired 1 day ago)
        payload = {
            **user_data,
            "exp": pendulum.now('UTC').subtract(days=1).int_timestamp
        }
        expired_token = jwt.encode(payload, settings.secret_key, algorithm="HS256")

        with pytest.raises(JWTError):
            verify_token(expired_token)

    def test_token_expiration_boundary(self):
        """Test token expiration at exact boundary"""
        settings = get_settings()
        user_data = {"user_id": "123"}

        # Create token that expires in 1 second
        payload = {
            **user_data,
            "exp": pendulum.now('UTC').add(seconds=1).int_timestamp
        }
        short_lived_token = jwt.encode(payload, settings.secret_key, algorithm="HS256")

        # Should work now
        result = verify_token(short_lived_token)
        assert result["user_id"] == "123"

        # After waiting 2 seconds, should fail
        import time
        time.sleep(2)

        with pytest.raises(JWTError):
            verify_token(short_lived_token)


class TestEdgeCases:
    """Tests for edge cases and error conditions"""

    def test_create_token_with_none_value(self):
        """Test creating token with None as a value"""
        user_data = {"user_id": "123", "optional_field": None}
        token = create_token(user_data)
        payload = verify_token(token)

        assert payload["user_id"] == "123"
        assert payload["optional_field"] is None

    def test_create_token_with_numeric_values(self):
        """Test creating token with various numeric types"""
        user_data = {
            "user_id": 123,  # int
            "team_id": 5,
            "rating": 3.14  # float
        }
        token = create_token(user_data)
        payload = verify_token(token)

        assert payload["user_id"] == 123
        assert payload["team_id"] == 5
        assert payload["rating"] == 3.14

    def test_create_token_with_boolean(self):
        """Test creating token with boolean values"""
        user_data = {"user_id": "123", "is_admin": True, "is_banned": False}
        token = create_token(user_data)
        payload = verify_token(token)

        assert payload["is_admin"] is True
        assert payload["is_banned"] is False

    def test_token_roundtrip(self):
        """Test complete create -> verify -> create -> verify roundtrip"""
        original_data = {"user_id": "123", "username": "test"}

        # First token
        token1 = create_token(original_data)
        payload1 = verify_token(token1)

        # Create new token from payload (excluding exp)
        payload1.pop("exp")
        token2 = create_token(payload1)
        payload2 = verify_token(token2)

        # Should have same user data
        assert payload2["user_id"] == original_data["user_id"]
        assert payload2["username"] == original_data["username"]

    def test_verify_token_missing_exp(self):
        """Test token without exp field (invalid)"""
        settings = get_settings()
        user_data = {"user_id": "123"}

        # Create token without exp (manually)
        token_no_exp = jwt.encode(user_data, settings.secret_key, algorithm="HS256")

        # Jose will still decode it (exp is optional in JWT spec)
        # But our tokens should always have exp
        payload = verify_token(token_no_exp)
        assert "user_id" in payload
