"""Tests for SqlAlchemyUnitOfWork transaction boundaries and rollback behavior.

Verifies rollback works correctly when operations fail mid-transaction.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

import pytest

from noteflow.domain.entities.meeting import Meeting
from noteflow.domain.entities.segment import Segment
from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

pytestmark = [pytest.mark.integration, pytest.mark.stress]


class TestExceptionRollback:
    """Test automatic rollback on exception."""

    @pytest.mark.asyncio
    async def test_exception_during_context_rolls_back(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Exception in context manager triggers automatic rollback."""
        meeting = Meeting.create(title="Rollback Test")

        with pytest.raises(RuntimeError, match="Simulated failure"):
            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
                await uow.meetings.create(meeting)
                raise RuntimeError("Simulated failure")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.meetings.get(meeting.id)
            assert result is None

    @pytest.mark.asyncio
    async def test_rollback_after_multiple_operations(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Rollback after multiple operations reverts all changes."""
        meeting = Meeting.create(title="Multi-op Rollback")

        with pytest.raises(ValueError, match=r".*"):
            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
                await uow.meetings.create(meeting)

                segment = Segment(
                    segment_id=0,
                    text="Test segment",
                    start_time=0.0,
                    end_time=1.0,
                    meeting_id=meeting.id,
                )
                await uow.segments.add(meeting.id, segment)

                raise ValueError("Simulated batch failure")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.meetings.get(meeting.id)
            assert result is None

    @pytest.mark.asyncio
    async def test_exception_type_does_not_matter(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Any exception type triggers rollback."""
        meeting = Meeting.create(title="Exception Type Test")

        class CustomError(Exception):
            pass

        with pytest.raises(CustomError, match="Custom error"):
            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
                await uow.meetings.create(meeting)
                raise CustomError("Custom error")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.meetings.get(meeting.id)
            assert result is None


class TestExplicitRollback:
    """Test explicit rollback behavior."""

    @pytest.mark.asyncio
    async def test_explicit_rollback_reverts_changes(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Explicit rollback() call reverts uncommitted changes."""
        meeting = Meeting.create(title="Explicit Rollback")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            await uow.rollback()

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.meetings.get(meeting.id)
            assert result is None

    @pytest.mark.asyncio
    async def test_commit_after_rollback_is_no_op(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Commit after rollback doesn't resurrect rolled-back data."""
        meeting = Meeting.create(title="Commit After Rollback")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            await uow.rollback()
            await uow.commit()

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.meetings.get(meeting.id)
            assert result is None


class TestCommitPersistence:
    """Test that committed data persists."""

    @pytest.mark.asyncio
    async def test_committed_data_visible_in_new_uow(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Data committed in one UoW is visible in subsequent UoW."""
        meeting = Meeting.create(title="Visibility Test")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.meetings.get(meeting.id)
            assert result is not None
            assert result.title == "Visibility Test"

    @pytest.mark.asyncio
    async def test_committed_meeting_and_segment(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Committed meeting and segment both persist."""
        meeting = Meeting.create(title="Meeting With Segment")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        segment = Segment(
            segment_id=0,
            text="Test segment text",
            start_time=0.0,
            end_time=1.5,
            meeting_id=meeting.id,
        )

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.segments.add(meeting.id, segment)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            segments = await uow.segments.get_by_meeting(meeting.id)
            assert len(segments) == 1
            assert segments[0].text == "Test segment text"


class TestBatchOperationRollback:
    """Test rollback behavior with batch operations."""

    @pytest.mark.asyncio
    async def test_batch_segment_add_rollback(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Batch segment operations are fully rolled back on failure."""
        meeting = Meeting.create(title="Batch Rollback Test")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        segments = [
            Segment(
                segment_id=i,
                text=f"Segment {i}",
                start_time=float(i),
                end_time=float(i + 1),
                meeting_id=meeting.id,
            )
            for i in range(10)
        ]

        with pytest.raises(RuntimeError, match="Batch failure"):
            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
                await uow.segments.add_batch(meeting.id, segments)
                raise RuntimeError("Batch failure")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.segments.get_by_meeting(meeting.id)
            assert len(result) == 0

    @pytest.mark.asyncio
    async def test_partial_batch_no_partial_persist(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Failure mid-batch doesn't leave partial data."""
        meeting = Meeting.create(title="Partial Batch Test")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        with pytest.raises(ValueError, match=r".*"):
            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
                for i in range(5):
                    segment = Segment(
                        segment_id=i,
                        text=f"Segment {i}",
                        start_time=float(i),
                        end_time=float(i + 1),
                        meeting_id=meeting.id,
                    )
                    await uow.segments.add(meeting.id, segment)

                raise ValueError("Mid-batch failure")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.segments.get_by_meeting(meeting.id)
            assert len(result) == 0


class TestIsolation:
    """Test transaction isolation between UoW instances."""

    @pytest.mark.asyncio
    async def test_uncommitted_data_not_visible_externally(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Uncommitted data in one UoW not visible in another."""
        meeting = Meeting.create(title="Isolation Test")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow1:
            await uow1.meetings.create(meeting)

            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow2:
                result = await uow2.meetings.get(meeting.id)
                assert result is None

    @pytest.mark.asyncio
    async def test_independent_uow_transactions(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Two UoW instances have independent transactions."""
        meeting1 = Meeting.create(title="Meeting 1")
        meeting2 = Meeting.create(title="Meeting 2")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow1:
            await uow1.meetings.create(meeting1)
            await uow1.commit()

            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow2:
                await uow2.meetings.create(meeting2)
                await uow2.rollback()

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result1 = await uow.meetings.get(meeting1.id)
            result2 = await uow.meetings.get(meeting2.id)

            assert result1 is not None
            assert result2 is None


class TestMeetingStateRollback:
    """Test rollback on meeting state changes."""

    @pytest.mark.asyncio
    async def test_meeting_state_change_rollback(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Meeting state changes are rolled back on failure."""
        meeting = Meeting.create(title="State Rollback")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        original_state = meeting.state

        with pytest.raises(ValueError, match=r".*"):
            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
                m = await uow.meetings.get(meeting.id)
                assert m is not None
                m.start_recording()
                await uow.meetings.update(m)
                raise ValueError("Business logic failure")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            result = await uow.meetings.get(meeting.id)
            assert result is not None
            assert result.state == original_state


class TestRepositoryContextRequirement:
    """Test that repositories require UoW context."""

    @pytest.mark.asyncio
    async def test_repo_access_outside_context_raises(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Accessing repository outside context raises RuntimeError."""
        uow = SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir)

        with pytest.raises(RuntimeError, match="UnitOfWork not in context"):
            _ = uow.meetings

    @pytest.mark.asyncio
    async def test_stress_commit_outside_context_raises(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Calling commit outside context raises RuntimeError (stress variant)."""
        uow = SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir)

        with pytest.raises(RuntimeError, match="UnitOfWork not in context"):
            await uow.commit()

    @pytest.mark.asyncio
    async def test_stress_rollback_outside_context_raises(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Calling rollback outside context raises RuntimeError (stress variant)."""
        uow = SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir)

        with pytest.raises(RuntimeError, match="UnitOfWork not in context"):
            await uow.rollback()


class TestMultipleMeetingOperations:
    """Test transactions spanning multiple meetings."""

    @pytest.mark.asyncio
    async def test_multiple_meetings_atomic(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Multiple meeting creates are atomic."""
        meetings = [Meeting.create(title=f"Meeting {i}") for i in range(5)]

        with pytest.raises(RuntimeError, match="All or nothing"):
            async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
                for meeting in meetings:
                    await uow.meetings.create(meeting)
                raise RuntimeError("All or nothing")

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            for meeting in meetings:
                result = await uow.meetings.get(meeting.id)
                assert result is None

    @pytest.mark.asyncio
    async def test_multiple_meetings_commit_all(
        self, postgressession_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Multiple meetings commit together."""
        meetings = [Meeting.create(title=f"Meeting {i}") for i in range(5)]

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            for meeting in meetings:
                await uow.meetings.create(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(postgressession_factory, meetings_dir) as uow:
            for meeting in meetings:
                result = await uow.meetings.get(meeting.id)
                assert result is not None
                assert meeting.title in result.title
