import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError
from sqlalchemy.future import select
from sqlalchemy import func as sql_func # For get_list_status
from datetime import datetime, timezone

from app.crud.list import (
    create_list,
    get_lists_for_user,
    get_list_by_id,
    update_list,
    delete_list,
    check_list_permission,
    get_list_status
)
from app.schemas.list import ListCreate, ListUpdate, ListStatus
from app.models import List as ListModel, User as UserModel, Group as GroupModel, UserGroup as UserGroupModel, Item as ItemModel
from app.core.exceptions import (
    ListNotFoundError,
    ListPermissionError,
    ListCreatorRequiredError,
    DatabaseConnectionError,
    DatabaseIntegrityError,
    DatabaseQueryError,
    DatabaseTransactionError,
    ConflictError
)

# Fixtures
@pytest.fixture
def mock_db_session():
    session = AsyncMock()
    session.begin = AsyncMock()
    session.commit = AsyncMock()
    session.rollback = AsyncMock()
    session.refresh = AsyncMock()
    session.add = MagicMock()
    session.delete = MagicMock()
    session.execute = AsyncMock()
    session.get = AsyncMock() # Used by check_list_permission via get_list_by_id
    session.flush = AsyncMock()
    return session

@pytest.fixture
def list_create_data():
    return ListCreate(name="New Shopping List", description="Groceries for the week")

@pytest.fixture
def list_update_data():
    return ListUpdate(name="Updated Shopping List", description="Weekend Groceries", version=1)

@pytest.fixture
def user_model():
    return UserModel(id=1, name="Test User", email="test@example.com")

@pytest.fixture
def another_user_model():
    return UserModel(id=2, name="Another User", email="another@example.com")

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

@pytest.fixture
def db_list_personal_model(user_model):
    return ListModel(
        id=1, name="Personal List", created_by_id=user_model.id, creator=user_model, 
        version=1, updated_at=datetime.now(timezone.utc), items=[]
    )

@pytest.fixture
def db_list_group_model(user_model, group_model):
    return ListModel(
        id=2, name="Group List", created_by_id=user_model.id, creator=user_model,
        group_id=group_model.id, group=group_model, version=1, updated_at=datetime.now(timezone.utc), items=[]
    )

# --- create_list Tests ---
@pytest.mark.asyncio
async def test_create_list_success(mock_db_session, list_create_data, user_model):
    async def mock_refresh(instance):
        instance.id = 100
        instance.version = 1
        instance.updated_at = datetime.now(timezone.utc)
        return None
    mock_db_session.refresh.return_value = None
    mock_db_session.refresh.side_effect = mock_refresh

    created_list = await create_list(mock_db_session, list_create_data, user_model.id)
    mock_db_session.add.assert_called_once()
    mock_db_session.flush.assert_called_once()
    mock_db_session.refresh.assert_called_once()
    assert created_list.name == list_create_data.name
    assert created_list.created_by_id == user_model.id

# --- get_lists_for_user Tests ---
@pytest.mark.asyncio
async def test_get_lists_for_user_mix(mock_db_session, user_model, db_list_personal_model, db_list_group_model):
    # Simulate user is part of group for db_list_group_model
    mock_group_ids_result = AsyncMock()
    mock_group_ids_result.scalars.return_value.all.return_value = [db_list_group_model.group_id]
    
    mock_lists_result = AsyncMock()
    # Order should be personal list (created by user_id) then group list
    mock_lists_result.scalars.return_value.all.return_value = [db_list_personal_model, db_list_group_model]
    
    mock_db_session.execute.side_effect = [mock_group_ids_result, mock_lists_result]

    lists = await get_lists_for_user(mock_db_session, user_model.id)
    assert len(lists) == 2
    assert db_list_personal_model in lists
    assert db_list_group_model in lists
    assert mock_db_session.execute.call_count == 2

# --- get_list_by_id Tests ---
@pytest.mark.asyncio
async def test_get_list_by_id_found_no_items(mock_db_session, db_list_personal_model):
    mock_result = AsyncMock()
    mock_result.scalars.return_value.first.return_value = db_list_personal_model
    mock_db_session.execute.return_value = mock_result
    
    found_list = await get_list_by_id(mock_db_session, db_list_personal_model.id, load_items=False)
    assert found_list is not None
    assert found_list.id == db_list_personal_model.id
    # query options should not include selectinload for items
    # (difficult to assert directly without inspecting query object in detail)

@pytest.mark.asyncio
async def test_get_list_by_id_found_with_items(mock_db_session, db_list_personal_model):
    # Simulate items loaded for the list
    db_list_personal_model.items = [ItemModel(id=1, name="Test Item")]
    mock_result = AsyncMock()
    mock_result.scalars.return_value.first.return_value = db_list_personal_model
    mock_db_session.execute.return_value = mock_result

    found_list = await get_list_by_id(mock_db_session, db_list_personal_model.id, load_items=True)
    assert found_list is not None
    assert len(found_list.items) == 1
    # query options should include selectinload for items

# --- update_list Tests ---
@pytest.mark.asyncio
async def test_update_list_success(mock_db_session, db_list_personal_model, list_update_data):
    list_update_data.version = db_list_personal_model.version # Match version

    updated_list = await update_list(mock_db_session, db_list_personal_model, list_update_data)
    assert updated_list.name == list_update_data.name
    assert updated_list.version == db_list_personal_model.version # version incremented in db_list_personal_model
    mock_db_session.add.assert_called_once_with(db_list_personal_model)
    mock_db_session.flush.assert_called_once()
    mock_db_session.refresh.assert_called_once_with(db_list_personal_model)

@pytest.mark.asyncio
async def test_update_list_conflict(mock_db_session, db_list_personal_model, list_update_data):
    list_update_data.version = db_list_personal_model.version + 1 # Version mismatch
    with pytest.raises(ConflictError):
        await update_list(mock_db_session, db_list_personal_model, list_update_data)
    mock_db_session.rollback.assert_called_once()

# --- delete_list Tests ---
@pytest.mark.asyncio
async def test_delete_list_success(mock_db_session, db_list_personal_model):
    await delete_list(mock_db_session, db_list_personal_model)
    mock_db_session.delete.assert_called_once_with(db_list_personal_model)
    mock_db_session.commit.assert_called_once() # from async with db.begin()

# --- check_list_permission Tests ---
@pytest.mark.asyncio
async def test_check_list_permission_creator_access_personal_list(mock_db_session, db_list_personal_model, user_model):
    # get_list_by_id (called by check_list_permission) will mock execute
    mock_list_fetch_result = AsyncMock()
    mock_list_fetch_result.scalars.return_value.first.return_value = db_list_personal_model
    mock_db_session.execute.return_value = mock_list_fetch_result

    ret_list = await check_list_permission(mock_db_session, db_list_personal_model.id, user_model.id)
    assert ret_list.id == db_list_personal_model.id

@pytest.mark.asyncio
async def test_check_list_permission_group_member_access_group_list(mock_db_session, db_list_group_model, another_user_model, group_model):
    # User `another_user_model` is not creator but member of the group
    db_list_group_model.creator_id = user_model.id # Original creator is user_model
    db_list_group_model.creator = user_model

    # Mock get_list_by_id internal call
    mock_list_fetch_result = AsyncMock()
    mock_list_fetch_result.scalars.return_value.first.return_value = db_list_group_model
    
    # Mock is_user_member call
    with patch('app.crud.list.is_user_member', new_callable=AsyncMock) as mock_is_member:
        mock_is_member.return_value = True # another_user_model is a member
        mock_db_session.execute.return_value = mock_list_fetch_result

        ret_list = await check_list_permission(mock_db_session, db_list_group_model.id, another_user_model.id)
        assert ret_list.id == db_list_group_model.id
        mock_is_member.assert_called_once_with(mock_db_session, group_id=group_model.id, user_id=another_user_model.id)

@pytest.mark.asyncio
async def test_check_list_permission_non_member_no_access_group_list(mock_db_session, db_list_group_model, another_user_model):
    db_list_group_model.creator_id = user_model.id # Creator is not another_user_model

    mock_list_fetch_result = AsyncMock()
    mock_list_fetch_result.scalars.return_value.first.return_value = db_list_group_model

    with patch('app.crud.list.is_user_member', new_callable=AsyncMock) as mock_is_member:
        mock_is_member.return_value = False # another_user_model is NOT a member
        mock_db_session.execute.return_value = mock_list_fetch_result
        
        with pytest.raises(ListPermissionError):
            await check_list_permission(mock_db_session, db_list_group_model.id, another_user_model.id)

@pytest.mark.asyncio
async def test_check_list_permission_list_not_found(mock_db_session, user_model):
    mock_list_fetch_result = AsyncMock()
    mock_list_fetch_result.scalars.return_value.first.return_value = None # List not found
    mock_db_session.execute.return_value = mock_list_fetch_result
    
    with pytest.raises(ListNotFoundError):
        await check_list_permission(mock_db_session, 999, user_model.id)

# --- get_list_status Tests ---
@pytest.mark.asyncio
async def test_get_list_status_success(mock_db_session, db_list_personal_model):
    list_updated_at = datetime.now(timezone.utc) - timezone.timedelta(hours=1)
    item_updated_at = datetime.now(timezone.utc)
    item_count = 5

    db_list_personal_model.updated_at = list_updated_at

    # Mock for ListModel.updated_at query
    mock_list_updated_result = AsyncMock()
    mock_list_updated_result.scalar_one_or_none.return_value = list_updated_at
    
    # Mock for ItemModel status query
    mock_item_status_result = AsyncMock()
    # SQLAlchemy query for func.max and func.count returns a Row-like object or None
    mock_item_status_row = MagicMock()
    mock_item_status_row.latest_item_updated_at = item_updated_at
    mock_item_status_row.item_count = item_count
    mock_item_status_result.first.return_value = mock_item_status_row

    mock_db_session.execute.side_effect = [mock_list_updated_result, mock_item_status_result]

    status = await get_list_status(mock_db_session, db_list_personal_model.id)
    assert status.list_updated_at == list_updated_at
    assert status.latest_item_updated_at == item_updated_at
    assert status.item_count == item_count
    assert mock_db_session.execute.call_count == 2

@pytest.mark.asyncio
async def test_get_list_status_list_not_found(mock_db_session):
    mock_list_updated_result = AsyncMock()
    mock_list_updated_result.scalar_one_or_none.return_value = None # List not found
    mock_db_session.execute.return_value = mock_list_updated_result
    with pytest.raises(ListNotFoundError):
        await get_list_status(mock_db_session, 999)

# TODO: Add more specific DB error tests (Operational, SQLAlchemyError, IntegrityError) for each function.
# TODO: Test check_list_permission with require_creator=True cases.