"""
Unit tests for UserService — authentication, password hashing, JWT tokens.
"""

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime

from app.modules.user.service import UserService
from app.modules.user.models import User
from app.modules.user.schema import UserCreate
from app.core.exceptions import UnauthorizedError, BadRequestError


class TestPasswordHashing:
    def test_hash_and_verify(self):
        db = AsyncMock()
        service = UserService(db)
        password = "MySecretPass123!"
        hashed = service.hash_password(password)
        assert hashed != password
        assert service.verify_password(password, hashed) is True

    def test_wrong_password_fails(self):
        db = AsyncMock()
        service = UserService(db)
        hashed = service.hash_password("correct_password")
        assert service.verify_password("wrong_password", hashed) is False

    def test_different_hashes_for_same_password(self):
        db = AsyncMock()
        service = UserService(db)
        h1 = service.hash_password("same_password")
        h2 = service.hash_password("same_password")
        assert h1 != h2  # bcrypt generates unique salts


class TestJWTTokens:
    def test_create_access_token(self):
        db = AsyncMock()
        service = UserService(db)
        token = service.create_access_token(user_id=1, username="trader1")
        assert isinstance(token, str)
        assert len(token) > 20  # JWT has header.payload.signature

    def test_token_contains_claims(self):
        from jose import jwt
        from app.config import get_settings
        settings = get_settings()

        db = AsyncMock()
        service = UserService(db)
        token = service.create_access_token(user_id=42, username="luckbot")
        payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
        assert payload["user_id"] == 42
        assert payload["username"] == "luckbot"
        assert "exp" in payload


class TestAuthentication:
    def _make_user(self, password="Test1234!", is_active=True, failed_attempts=0):
        db = AsyncMock()
        service = UserService(db)
        user = MagicMock(spec=User)
        user.id = 1
        user.username = "trader1"
        user.is_active = is_active
        user.failed_login_attempts = failed_attempts
        user.hashed_password = service.hash_password(password)
        return user

    @pytest.mark.asyncio
    async def test_successful_login(self):
        user = self._make_user(password="Test1234!")
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = user
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        result = await service.authenticate("trader1", "Test1234!")
        assert result == user

    @pytest.mark.asyncio
    async def test_login_resets_failed_attempts(self):
        user = self._make_user(password="Test1234!", failed_attempts=2)
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = user
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        await service.authenticate("trader1", "Test1234!")
        assert user.failed_login_attempts == 0

    @pytest.mark.asyncio
    async def test_wrong_password_increments_attempts(self):
        user = self._make_user(password="Test1234!", failed_attempts=0)
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = user
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        with pytest.raises(UnauthorizedError, match="2 attempt"):
            await service.authenticate("trader1", "wrong-password")
        assert user.failed_login_attempts == 1

    @pytest.mark.asyncio
    async def test_account_locks_after_3_attempts(self):
        user = self._make_user(password="Test1234!", failed_attempts=2)
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = user
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        with pytest.raises(UnauthorizedError, match="locked"):
            await service.authenticate("trader1", "wrong-password")
        assert user.is_active is False

    @pytest.mark.asyncio
    async def test_locked_account_rejected(self):
        user = self._make_user(password="Test1234!", is_active=False)
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = user
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        with pytest.raises(UnauthorizedError, match="locked"):
            await service.authenticate("trader1", "Test1234!")

    @pytest.mark.asyncio
    async def test_nonexistent_user(self):
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        with pytest.raises(UnauthorizedError, match="Invalid credentials"):
            await service.authenticate("ghost", "password")


class TestCreateUser:
    @pytest.mark.asyncio
    async def test_create_user_success(self):
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None  # no existing user
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        data = UserCreate(username="newuser", email="new@test.com", password="Test1234!")
        user = await service.create_user(data)
        db.add.assert_called_once()
        db.commit.assert_called()

    @pytest.mark.asyncio
    async def test_duplicate_username_rejected(self):
        db = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = MagicMock()  # existing user
        db.execute = AsyncMock(return_value=mock_result)

        service = UserService(db)
        data = UserCreate(username="existing", email="new@test.com", password="Test1234!")
        with pytest.raises(BadRequestError, match="Username already exists"):
            await service.create_user(data)
