import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError # Assuming these might be raised
from datetime import datetime, timedelta, timezone
import secrets

from app.crud.invite import (
    create_invite,
    get_active_invite_by_code,
    deactivate_invite,
    MAX_CODE_GENERATION_ATTEMPTS
)
from app.models import Invite as InviteModel, User as UserModel, Group as GroupModel # For context
# No specific schemas for invite CRUD usually, but models are used.

# Fixtures
@pytest.fixture
def mock_db_session():
    session = AsyncMock()
    session.commit = AsyncMock()
    session.rollback = AsyncMock()
    session.refresh = AsyncMock()
    session.add = MagicMock()
    session.execute = AsyncMock()
    return session

@pytest.fixture
def group_model():
    return GroupModel(id=1, name="Test Group")

@pytest.fixture
def user_model(): # Creator
    return UserModel(id=1, name="Creator User")

@pytest.fixture
def db_invite_model(group_model, user_model):
    return InviteModel(
        id=1,
        code="test_invite_code_123",
        group_id=group_model.id,
        created_by_id=user_model.id,
        expires_at=datetime.now(timezone.utc) + timedelta(days=7),
        is_active=True
    )

# --- create_invite Tests ---
@pytest.mark.asyncio
@patch('app.crud.invite.secrets.token_urlsafe') # Patch secrets.token_urlsafe
async def test_create_invite_success_first_attempt(mock_token_urlsafe, mock_db_session, group_model, user_model):
    generated_code = "unique_code_123"
    mock_token_urlsafe.return_value = generated_code
    
    # Mock DB execute for checking existing code (first attempt, no existing code)
    mock_existing_check_result = AsyncMock()
    mock_existing_check_result.scalar_one_or_none.return_value = None 
    mock_db_session.execute.return_value = mock_existing_check_result

    invite = await create_invite(mock_db_session, group_model.id, user_model.id, expires_in_days=5)

    mock_token_urlsafe.assert_called_once_with(16)
    mock_db_session.execute.assert_called_once() # For the uniqueness check
    mock_db_session.add.assert_called_once()
    mock_db_session.commit.assert_called_once()
    mock_db_session.refresh.assert_called_once_with(invite)
    
    assert invite is not None
    assert invite.code == generated_code
    assert invite.group_id == group_model.id
    assert invite.created_by_id == user_model.id
    assert invite.is_active is True
    assert invite.expires_at > datetime.now(timezone.utc) + timedelta(days=4) # Check expiry is roughly correct

@pytest.mark.asyncio
@patch('app.crud.invite.secrets.token_urlsafe')
async def test_create_invite_success_after_collision(mock_token_urlsafe, mock_db_session, group_model, user_model):
    colliding_code = "colliding_code"
    unique_code = "finally_unique_code"
    mock_token_urlsafe.side_effect = [colliding_code, unique_code] # First call collides, second is unique

    # Mock DB execute for checking existing code
    mock_collision_check_result = AsyncMock()
    mock_collision_check_result.scalar_one_or_none.return_value = 1 # Simulate collision (ID found)
    
    mock_no_collision_check_result = AsyncMock()
    mock_no_collision_check_result.scalar_one_or_none.return_value = None # No collision

    mock_db_session.execute.side_effect = [mock_collision_check_result, mock_no_collision_check_result]

    invite = await create_invite(mock_db_session, group_model.id, user_model.id)

    assert mock_token_urlsafe.call_count == 2
    assert mock_db_session.execute.call_count == 2
    assert invite is not None
    assert invite.code == unique_code

@pytest.mark.asyncio
@patch('app.crud.invite.secrets.token_urlsafe')
async def test_create_invite_fails_after_max_attempts(mock_token_urlsafe, mock_db_session, group_model, user_model):
    mock_token_urlsafe.return_value = "always_colliding_code"
    
    mock_collision_check_result = AsyncMock()
    mock_collision_check_result.scalar_one_or_none.return_value = 1 # Always collide
    mock_db_session.execute.return_value = mock_collision_check_result

    invite = await create_invite(mock_db_session, group_model.id, user_model.id)
    
    assert invite is None
    assert mock_token_urlsafe.call_count == MAX_CODE_GENERATION_ATTEMPTS
    assert mock_db_session.execute.call_count == MAX_CODE_GENERATION_ATTEMPTS
    mock_db_session.add.assert_not_called()

# --- get_active_invite_by_code Tests ---
@pytest.mark.asyncio
async def test_get_active_invite_by_code_found_active(mock_db_session, db_invite_model):
    db_invite_model.is_active = True
    db_invite_model.expires_at = datetime.now(timezone.utc) + timedelta(days=1)
    
    mock_result = AsyncMock()
    mock_result.scalars.return_value.first.return_value = db_invite_model
    mock_db_session.execute.return_value = mock_result

    invite = await get_active_invite_by_code(mock_db_session, db_invite_model.code)
    assert invite is not None
    assert invite.code == db_invite_model.code
    mock_db_session.execute.assert_called_once()

@pytest.mark.asyncio
async def test_get_active_invite_by_code_not_found(mock_db_session):
    mock_result = AsyncMock()
    mock_result.scalars.return_value.first.return_value = None
    mock_db_session.execute.return_value = mock_result
    invite = await get_active_invite_by_code(mock_db_session, "non_existent_code")
    assert invite is None

@pytest.mark.asyncio
async def test_get_active_invite_by_code_inactive(mock_db_session, db_invite_model):
    db_invite_model.is_active = False # Inactive
    db_invite_model.expires_at = datetime.now(timezone.utc) + timedelta(days=1)

    mock_result = AsyncMock()
    mock_result.scalars.return_value.first.return_value = None # Should not be found by query
    mock_db_session.execute.return_value = mock_result

    invite = await get_active_invite_by_code(mock_db_session, db_invite_model.code)
    assert invite is None

@pytest.mark.asyncio
async def test_get_active_invite_by_code_expired(mock_db_session, db_invite_model):
    db_invite_model.is_active = True
    db_invite_model.expires_at = datetime.now(timezone.utc) - timedelta(days=1) # Expired

    mock_result = AsyncMock()
    mock_result.scalars.return_value.first.return_value = None # Should not be found by query
    mock_db_session.execute.return_value = mock_result

    invite = await get_active_invite_by_code(mock_db_session, db_invite_model.code)
    assert invite is None

# --- deactivate_invite Tests ---
@pytest.mark.asyncio
async def test_deactivate_invite_success(mock_db_session, db_invite_model):
    db_invite_model.is_active = True # Ensure it starts active
    
    deactivated_invite = await deactivate_invite(mock_db_session, db_invite_model)

    mock_db_session.add.assert_called_once_with(db_invite_model)
    mock_db_session.commit.assert_called_once()
    mock_db_session.refresh.assert_called_once_with(db_invite_model)
    assert deactivated_invite.is_active is False

# It might be useful to test DB error cases (OperationalError, etc.) for each function
# if they have specific try-except blocks, but invite.py seems to rely on caller/framework for some of that.
# create_invite has its own DB interaction within the loop, so that's covered.
# get_active_invite_by_code and deactivate_invite are simpler DB ops.