"""Integration tests for 100% code coverage.

Tests cover:
- Pipeline execution and optimization
- AsyncIterator collection
- Operations with real database
- Edge cases with no primary key
- Update operations with edge cases
"""

from typing import Any
from unittest.mock import MagicMock, patch

import pytest
import pytest_asyncio
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool

from haolib.pipelines import filtero, mapo, reduceo, transformo
from haolib.pipelines.operations import CreateOperation, DeleteOperation, PatchOperation, UpdateOperation
from haolib.storages.indexes.params import ParamIndex
from haolib.storages.indexes.sql import SQLQueryIndex
from haolib.storages.operations.sqlalchemy import SQLAlchemyOperationsHandler
from haolib.storages.sqlalchemy import SQLAlchemyStorage
from tests.integration.conftest import MockAppConfig
from tests.integration.storages.conftest import Base, User, UserModel

# Constants for test values
MIN_AGE = 18
MIN_NAME_LENGTH = 5
UPDATED_AGE = 100


class TestCoverage:
    """Tests to achieve 100% code coverage."""

    @pytest_asyncio.fixture
    async def real_sqlalchemy_storage(self, app_config: MockAppConfig, registry: Any) -> Any:
        """Create SQLAlchemy storage with real database."""
        # Try to use real database, fallback to SQLite if PostgreSQL driver not available
        db_url = str(app_config.sqlalchemy.url)
        try:
            engine = create_async_engine(db_url, echo=False)
            # Test connection
            async with engine.begin() as conn:
                await conn.execute(text("SELECT 1"))
        except Exception:
            # Fallback to SQLite for testing
            engine = create_async_engine(
                "sqlite+aiosqlite:///:memory:",
                echo=False,
                poolclass=StaticPool,
                connect_args={"check_same_thread": False},
            )
        async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

        # Create tables
        async with engine.begin() as conn:
            await conn.run_sync(Base.metadata.create_all)

        storage = SQLAlchemyStorage(
            engine=engine,
            session_maker=async_session,
            data_type_registry=registry,
            relationship_load_depth=2,
        )

        async with storage:
            yield storage

        # Cleanup - drop tables before engine disposal
        async with engine.begin() as conn:
            await conn.run_sync(Base.metadata.drop_all)
        # Note: Engine is automatically disposed by storage context manager

    # Transaction fixture removed - transactions are now automatic

    @pytest.mark.asyncio
    async def test_execute_pipeline_directly(self, real_sqlalchemy_storage: Any) -> None:
        """Test executing pipeline directly (line 96)."""
        users = [User(name="PipelineUser", age=25, email="pipeline@example.com")]
        await real_sqlalchemy_storage.create(users).execute()

        index = ParamIndex(data_type=User)
        pipeline = real_sqlalchemy_storage.read(index).returning() | filtero(lambda u: u.age >= MIN_AGE)

        result = await real_sqlalchemy_storage.execute(pipeline)

        assert isinstance(result, list)
        assert len(result) >= 1

    @pytest.mark.asyncio
    async def test_execute_nested_pipeline(self, real_sqlalchemy_storage: Any) -> None:
        """Test executing nested pipeline (line 143)."""
        users = [User(name="NestedUser", age=30, email="nested@example.com")]
        await real_sqlalchemy_storage.create(users).execute()

        index = ParamIndex(data_type=User)
        inner_pipeline = real_sqlalchemy_storage.read(index).returning() | filtero(lambda u: u.age >= MIN_AGE)
        outer_pipeline = inner_pipeline | mapo(lambda u, _idx: u.name)

        result = await real_sqlalchemy_storage.execute(outer_pipeline)

        assert isinstance(result, list)
        assert len(result) >= 1

    @pytest.mark.asyncio
    async def test_collect_async_iterator(self, real_sqlalchemy_storage: Any) -> None:
        """Test _collect_async_iterator method (line 212)."""
        users = [User(name="IteratorUser", age=25, email="iterator@example.com")]
        await real_sqlalchemy_storage.create(users).execute()

        index = ParamIndex(data_type=User)
        await real_sqlalchemy_storage.read(index).returning().execute()

        # This should trigger _collect_async_iterator when used in pipeline
        # Pipeline composition with AsyncIterator
        pipeline = real_sqlalchemy_storage.read(index).returning() | filtero(lambda u: u.age >= MIN_AGE)
        result = await real_sqlalchemy_storage.execute(pipeline)

        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_execute_operation_with_async_iterator_collection(self, real_sqlalchemy_storage: Any) -> None:
        """Test _execute_operation with AsyncIterator collection (lines 178, 187, 196, 205)."""
        users = [User(name="AsyncIterUser", age=25, email="asynciter@example.com")]
        await real_sqlalchemy_storage.create(users).execute()

        index = ParamIndex(data_type=User)
        read_result = await real_sqlalchemy_storage.read(index).returning().execute()

        # Filter with AsyncIterator - should trigger collection
        txn = real_sqlalchemy_storage._begin_transaction()
        async with txn:
            filter_op = filtero(lambda u: u.age >= MIN_AGE)
            result = await real_sqlalchemy_storage._executor._execute_operation(
                filter_op, txn, previous_result=read_result
            )

        assert isinstance(result, list)

        # Map with AsyncIterator
        map_op = mapo(lambda u, _idx: u.name)
        result = await real_sqlalchemy_storage._executor._execute_operation(map_op, txn, previous_result=read_result)

        assert isinstance(result, list)

        # Reduce with AsyncIterator
        reduce_op = reduceo(reducer=lambda acc, u: acc + u.age, initial=0)
        result = await real_sqlalchemy_storage._executor._execute_operation(reduce_op, txn, previous_result=read_result)

        assert isinstance(result, int)

        # Transform with list (AsyncIterator is now collected inside transaction)
        # Re-read for transform test
        read_result2 = await real_sqlalchemy_storage.read(index).returning().execute()
        # read_result2 is now a list (collected inside transaction)
        users_list = read_result2
        txn2 = real_sqlalchemy_storage._begin_transaction()
        async with txn2:
            transform_op = transformo(transformer=lambda users: [u.email for u in users])
            result = await real_sqlalchemy_storage._executor._execute_operation(
                transform_op, txn2, previous_result=users_list
            )

        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_build_optimized_operation_no_filters(self, real_sqlalchemy_storage: Any) -> None:
        """Test _build_optimized_operation_if_needed when no filters (lines 278-285)."""
        users = [User(name="OptimizeUser", age=25, email="optimize@example.com")]
        await real_sqlalchemy_storage.create(users).execute()

        index = ParamIndex(data_type=User)
        # Single read operation - no filters, should not trigger optimized query building
        result = await real_sqlalchemy_storage.read(index).returning().execute()

        # Result is now a list (AsyncIterator is collected inside transaction)
        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_build_optimized_operation_single_operation(self, real_sqlalchemy_storage: Any) -> None:
        """Test _build_optimized_operation_if_needed with single operation (line 276)."""
        users = [User(name="SingleOpUser", age=25, email="single@example.com")]
        await real_sqlalchemy_storage.create(users).execute()

        index = ParamIndex(data_type=User)
        # Single operation - len(sql_operations) == 1, should not trigger optimization
        result = await real_sqlalchemy_storage.read(index).returning().execute()

        # Result is now a list (AsyncIterator is collected inside transaction)
        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_build_optimized_operation_no_optimized_result(self, real_sqlalchemy_storage: Any) -> None:
        """Test _build_optimized_operation_if_needed when optimized is None (line 284)."""
        users = [User(name="NoOptUser", age=25, email="noopt@example.com")]
        await real_sqlalchemy_storage.create(users).execute()

        index = ParamIndex(data_type=User)

        # Use a filter that cannot be converted to SQL
        def complex_predicate(u: User) -> bool:
            return len(u.name) > MIN_NAME_LENGTH  # Complex predicate that can't be converted

        pipeline = real_sqlalchemy_storage.read(index).returning() | filtero(complex_predicate)

        # This should execute in Python (filter can't be converted)
        result = await real_sqlalchemy_storage.execute(pipeline)

        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_execute_update_no_primary_key(self, real_sqlalchemy_storage: Any) -> None:
        """Test execute_update when model has no primary key (lines 286-294)."""
        # Get session from storage's internal transaction
        # Note: This test accesses internal implementation details

        # Create a temporary transaction to get session for raw SQL
        txn = real_sqlalchemy_storage._begin_transaction()
        async with txn:
            session = await txn.get_session()

        # Create table without primary key using raw SQL
        await session.execute(text("CREATE TABLE IF NOT EXISTS no_pk_table (name TEXT, value INTEGER)"))
        await session.flush()

        # Insert test data
        await session.execute(text("INSERT INTO no_pk_table (name, value) VALUES ('test1', 10), ('test2', 20)"))
        await session.flush()

        handler = SQLAlchemyOperationsHandler(
            registry=real_sqlalchemy_storage.data_type_registry,
            relationship_load_depth=2,
            storage=real_sqlalchemy_storage,
        )

        # For testing no-PK update, we need to use UserModel but mock its primary_key
        # to be empty. This will trigger the else branch at line 284.
        # First, create test data
        user = User(name="test1", age=25, email="test1@example.com")
        txn2 = real_sqlalchemy_storage._begin_transaction()
        async with txn2:
            await handler.execute_create(CreateOperation(data=[user]), txn2)

        # Create a mock primary key with empty columns
        mock_pk = MagicMock()
        mock_pk.columns = []

        # Patch UserModel.__table__.primary_key.columns when accessed in handler
        # The handler accesses model.__table__.primary_key.columns at line 237
        original_table = UserModel.__table__  # type: ignore[attr-defined]
        original_pk = original_table.primary_key

        # Temporarily replace primary_key
        original_table.primary_key = mock_pk  # type: ignore[assignment]

        try:
            # Now test update - should hit the else branch at line 284
            query = select(UserModel).where(UserModel.name == "test1")
            index = SQLQueryIndex(query=query)
            patch_op = PatchOperation(search_index=index, patch={"age": UPDATED_AGE})

            txn3 = real_sqlalchemy_storage._begin_transaction()
            async with txn3:
                result = await handler.execute_patch(patch_op, txn3)

            assert len(result) == 1
            assert result[0].age == UPDATED_AGE
        finally:
            # Restore original primary key
            original_table.primary_key = original_pk  # type: ignore[assignment]

        # Cleanup
        await session.execute(text("DROP TABLE IF EXISTS no_pk_table"))
        await session.flush()

    @pytest.mark.asyncio
    async def test_execute_delete_no_primary_key(self, real_sqlalchemy_storage: Any) -> None:
        """Test execute_delete when model has no primary key (lines 437-455)."""
        handler = SQLAlchemyOperationsHandler(
            registry=real_sqlalchemy_storage.data_type_registry,
            relationship_load_depth=2,
            storage=real_sqlalchemy_storage,
        )

        # Create user first
        user = User(name="DeleteTest", age=25, email="deletetest@example.com")
        txn = real_sqlalchemy_storage._begin_transaction()
        async with txn:
            await handler.execute_create(CreateOperation(data=[user]), txn)

        # Mock primary_key to be empty to test no-PK path
        mock_pk = MagicMock()
        mock_pk.columns = []

        original_table = UserModel.__table__  # type: ignore[attr-defined]
        original_pk = original_table.primary_key
        original_table.primary_key = mock_pk  # type: ignore[assignment]

        try:
            # Delete using SQLQueryIndex (no PK means we use count query path)
            query = select(UserModel).where(UserModel.name == "DeleteTest")
            index = SQLQueryIndex(query=query)
            delete_op = DeleteOperation(search_index=index)

            txn = real_sqlalchemy_storage._begin_transaction()
            async with txn:
                deleted_count = await handler.execute_delete(delete_op, txn)

            assert deleted_count == 1
        finally:
            # Restore original primary key
            original_table.primary_key = original_pk  # type: ignore[assignment]

    @pytest.mark.asyncio
    async def test_execute_delete_no_primary_key_real(self, real_sqlalchemy_storage: Any) -> None:
        """Test execute_delete when model has no primary key (lines 437-455)."""
        handler = SQLAlchemyOperationsHandler(
            registry=real_sqlalchemy_storage.data_type_registry,
            relationship_load_depth=2,
            storage=real_sqlalchemy_storage,
        )

        # Create user first
        user = User(name="DeleteTest", age=25, email="deletetest@example.com")
        txn = real_sqlalchemy_storage._begin_transaction()
        async with txn:
            await handler.execute_create(CreateOperation(data=[user]), txn)

        # Mock primary_key to be empty to test no-PK path
        mock_pk = MagicMock()
        mock_pk.columns = []

        original_user_table = UserModel.__table__  # type: ignore[attr-defined]
        original_user_pk = original_user_table.primary_key
        original_user_table.primary_key = mock_pk  # type: ignore[assignment]

        try:
            # Delete using SQLQueryIndex (no PK means we use count query path)
            query = select(UserModel).where(UserModel.name == "DeleteTest")
            index = SQLQueryIndex(query=query)
            delete_op = DeleteOperation(search_index=index)

            txn = real_sqlalchemy_storage._begin_transaction()
            async with txn:
                deleted_count = await handler.execute_delete(delete_op, txn)

            assert deleted_count == 1
        finally:
            # Restore original primary key
            original_user_table.primary_key = original_user_pk  # type: ignore[assignment]

    @pytest.mark.asyncio
    async def test_execute_update_else_branch_no_registration(self, real_sqlalchemy_storage: Any) -> None:
        """Test execute_update else branch when registration is None (line 302)."""
        handler = SQLAlchemyOperationsHandler(
            registry=real_sqlalchemy_storage.data_type_registry,
            relationship_load_depth=2,
            storage=real_sqlalchemy_storage,
        )

        # Create user
        user = User(name="TestUser", age=25, email="test@example.com")
        txn = real_sqlalchemy_storage._begin_transaction()
        async with txn:
            created = await handler.execute_create(CreateOperation(data=[user]), txn)
        user_id = created[0].id

        # The else branch at line 302 checks if registration is None
        # Registration is set at line 217 from get_for_user_type
        # Since registration is always set (or ValueError is raised), this else branch
        # is defensive code that's extremely difficult to test without code modification

        # Test normal operation - the else branch is defensive code
        index = ParamIndex(data_type=User, id=user_id)
        patch_op = PatchOperation(search_index=index, patch={"name": "UpdatedUser"})

        txn = real_sqlalchemy_storage._begin_transaction()
        async with txn:
            result = await handler.execute_patch(patch_op, txn)

        assert len(result) == 1
        assert result[0].name == "UpdatedUser"
        # Note: The else branch at 302 is defensive code that would only execute
        # if registration somehow becomes None after being set at line 217.
        # This is covered by code review and is extremely difficult to test.

    @pytest.mark.asyncio
    async def test_execute_update_callable_patch_else_branch_353(self, real_sqlalchemy_storage: Any) -> None:
        """Test execute_update callable patch else branch (line 353)."""
        handler = SQLAlchemyOperationsHandler(
            registry=real_sqlalchemy_storage.data_type_registry,
            relationship_load_depth=2,
            storage=real_sqlalchemy_storage,
        )

        # Create user
        user = User(name="TestUser2", age=30, email="test2@example.com")
        txn = real_sqlalchemy_storage._begin_transaction()
        async with txn:
            created = await handler.execute_create(CreateOperation(data=[user]), txn)
        user_id = created[0].id

        def update_func(u: User) -> User:
            u.name = "UpdatedUser2"
            return u

        # Mock get_for_storage_type to return None in conversion step (line 349)
        # This will trigger the else branch at 353
        original_method = handler._registry.get_for_storage_type

        call_count = {"count": 0}

        def mock_get_storage(storage_type: type, user_type: type | None = None) -> Any:
            call_count["count"] += 1
            # Return None on second call (in conversion step at line 349) to trigger else branch at 353
            if call_count["count"] > 1 and storage_type == UserModel:
                return None
            return original_method(storage_type, user_type)

        with patch.object(handler._registry, "get_for_storage_type", side_effect=mock_get_storage):
            index = ParamIndex(data_type=User, id=user_id)
            # For callable, use UpdateOperation with data
            update_op = UpdateOperation(search_index=index, data=update_func)

            txn3 = real_sqlalchemy_storage._begin_transaction()
            async with txn3:
                result = await handler.execute_update(update_op, txn3)

            assert len(result) == 1
            # In else branch at 353, should return model directly (not converted)
            assert isinstance(result[0], UserModel)
            assert result[0].name == "UpdatedUser2"
