import pytest
from unittest.mock import AsyncMock, MagicMock
from sqlalchemy.exc import IntegrityError, OperationalError

from app.crud.user import get_user_by_email, create_user
from app.schemas.user import UserCreate
from app.models import User as UserModel
from app.core.exceptions import (
    UserCreationError,
    EmailAlreadyRegisteredError,
    DatabaseConnectionError,
    DatabaseIntegrityError,
    DatabaseQueryError,
    DatabaseTransactionError
)

# Fixtures
@pytest.fixture
def mock_db_session():
    return AsyncMock()

@pytest.fixture
def user_create_data():
    return UserCreate(email="test@example.com", password="password123", name="Test User")

@pytest.fixture
def existing_user_data():
    return UserModel(id=1, email="exists@example.com", password_hash="hashed_password", name="Existing User")

# Tests for get_user_by_email
@pytest.mark.asyncio
async def test_get_user_by_email_found(mock_db_session, existing_user_data):
    mock_db_session.execute.return_value.scalars.return_value.first.return_value = existing_user_data
    user = await get_user_by_email(mock_db_session, "exists@example.com")
    assert user is not None
    assert user.email == "exists@example.com"
    mock_db_session.execute.assert_called_once()

@pytest.mark.asyncio
async def test_get_user_by_email_not_found(mock_db_session):
    mock_db_session.execute.return_value.scalars.return_value.first.return_value = None
    user = await get_user_by_email(mock_db_session, "nonexistent@example.com")
    assert user is None
    mock_db_session.execute.assert_called_once()

@pytest.mark.asyncio
async def test_get_user_by_email_db_connection_error(mock_db_session):
    mock_db_session.execute.side_effect = OperationalError("mock_op_error", "params", "orig")
    with pytest.raises(DatabaseConnectionError):
        await get_user_by_email(mock_db_session, "test@example.com")

@pytest.mark.asyncio
async def test_get_user_by_email_db_query_error(mock_db_session):
    # Simulate a generic SQLAlchemyError that is not OperationalError
    mock_db_session.execute.side_effect = IntegrityError("mock_sql_error", "params", "orig") # Using IntegrityError as an example of SQLAlchemyError
    with pytest.raises(DatabaseQueryError):
        await get_user_by_email(mock_db_session, "test@example.com")


# Tests for create_user
@pytest.mark.asyncio
async def test_create_user_success(mock_db_session, user_create_data):
    # The actual user object returned would be created by SQLAlchemy based on db_user
    # We mock the process: db.add is called, then db.flush, then db.refresh updates db_user
    async def mock_refresh(user_model_instance):
        user_model_instance.id = 1 # Simulate DB assigning an ID
        # Simulate other db-generated fields if necessary
        return None

    mock_db_session.refresh = AsyncMock(side_effect=mock_refresh)
    mock_db_session.flush = AsyncMock()
    mock_db_session.add = MagicMock()

    created_user = await create_user(mock_db_session, user_create_data)

    mock_db_session.add.assert_called_once()
    mock_db_session.flush.assert_called_once()
    mock_db_session.refresh.assert_called_once()

    assert created_user is not None
    assert created_user.email == user_create_data.email
    assert created_user.name == user_create_data.name
    assert hasattr(created_user, 'id') # Check if ID was assigned (simulated by mock_refresh)
    # Password hash check would be more involved, ensure hash_password was called correctly
    # For now, we assume hash_password works as intended and is tested elsewhere.

@pytest.mark.asyncio
async def test_create_user_email_already_registered(mock_db_session, user_create_data):
    mock_db_session.flush.side_effect = IntegrityError("mock error (unique constraint)", "params", "orig")
    with pytest.raises(EmailAlreadyRegisteredError):
        await create_user(mock_db_session, user_create_data)

@pytest.mark.asyncio
async def test_create_user_db_integrity_error_not_unique(mock_db_session, user_create_data):
    # Simulate an IntegrityError that is not related to a unique constraint
    mock_db_session.flush.side_effect = IntegrityError("mock error (not unique constraint)", "params", "orig")
    with pytest.raises(DatabaseIntegrityError):
        await create_user(mock_db_session, user_create_data)

@pytest.mark.asyncio
async def test_create_user_db_connection_error(mock_db_session, user_create_data):
    mock_db_session.begin.side_effect = OperationalError("mock_op_error", "params", "orig")
    with pytest.raises(DatabaseConnectionError):
        await create_user(mock_db_session, user_create_data)
    # also test OperationalError on flush
    mock_db_session.begin.side_effect = None # reset side effect
    mock_db_session.flush.side_effect = OperationalError("mock_op_error", "params", "orig")
    with pytest.raises(DatabaseConnectionError):
         await create_user(mock_db_session, user_create_data)


@pytest.mark.asyncio
async def test_create_user_db_transaction_error(mock_db_session, user_create_data):
    # Simulate a generic SQLAlchemyError on flush that is not IntegrityError or OperationalError
    mock_db_session.flush.side_effect = UserCreationError("Simulated non-specific SQLAlchemyError") # Or any other SQLAlchemyError
    with pytest.raises(DatabaseTransactionError):
        await create_user(mock_db_session, user_create_data)