"""Advanced integration tests for SqlAlchemyUnitOfWork.

Tests edge cases and advanced scenarios not covered by basic tests:
- Nested transaction behavior
- Concurrent UoW instances
- Repository feature flags
- Factory methods
- Cross-repository operations
- Connection pool behavior
"""

from __future__ import annotations

import asyncio
from pathlib import Path
from typing import TYPE_CHECKING
from uuid import uuid4

import pytest

from noteflow.domain.entities import Meeting, Segment, Summary
from noteflow.domain.value_objects import MeetingState
from noteflow.infrastructure.persistence.repositories import DiarizationJob, StreamingTurn
from noteflow.infrastructure.persistence.repositories.diarization_job_repo import (
    JOB_STATUS_QUEUED,
    JOB_STATUS_RUNNING,
)
from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

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

from support.async_helpers import yield_control


@pytest.mark.integration
class TestUnitOfWorkFeatureFlags:
    """Integration tests for UoW feature flag properties."""

    async def test_supports_annotations_true(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test database UoW supports annotations."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert uow.supports_annotations is True, "database UoW should support annotations"

    async def test_supports_diarization_jobs_true(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test database UoW supports diarization jobs."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert uow.supports_diarization_jobs is True, "database UoW should support diarization jobs"

    async def test_supports_preferences_true(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test database UoW supports preferences."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert uow.supports_preferences is True, "database UoW should support preferences"


@pytest.mark.integration
class TestUnitOfWorkCrossRepositoryOperations:
    """Integration tests for operations spanning multiple repositories."""

    async def test_meeting_with_segments_and_summary_atomic(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test creating meeting with segments and summary is atomic."""
        meeting = Meeting.create(title="Full Meeting")

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

            for i in range(3):
                segment = Segment(
                    segment_id=i,
                    text=f"Segment {i}",
                    start_time=float(i),
                    end_time=float(i + 1),
                )
                await uow.segments.add(meeting.id, segment)

            summary = Summary(
                meeting_id=meeting.id,
                executive_summary="Test summary",
            )
            await uow.summaries.save(summary)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            m = await uow.meetings.get(meeting.id)
            segments = await uow.segments.get_by_meeting(meeting.id)
            s = await uow.summaries.get_by_meeting(meeting.id)

            assert m is not None, "meeting should be retrievable"
            assert len(segments) == 3, "all 3 segments should be persisted"
            assert s is not None, "summary should be retrievable"
            assert s.executive_summary == "Test summary", "summary content should match"

    async def test_meeting_deletion_cascades_to_segments_and_summary(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test deleting meeting cascades to related data."""
        meeting = Meeting.create()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting)
            segment = Segment(segment_id=0, text="Test", start_time=0.0, end_time=1.0)
            await uow.segments.add(meeting.id, segment)
            summary = Summary(meeting_id=meeting.id, executive_summary="Test")
            await uow.summaries.save(summary)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            await uow.meetings.delete(meeting.id)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert await uow.meetings.get(meeting.id) is None, "meeting should be deleted"
            assert await uow.segments.get_by_meeting(meeting.id) == [], "segments should cascade delete"
            assert await uow.summaries.get_by_meeting(meeting.id) is None, "summary should cascade delete"

    async def test_meeting_deletion_cascades_todiarization_jobs(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test deleting meeting cascades to diarization jobs and turns."""
        meeting = Meeting.create()

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

            job = DiarizationJob(
                job_id=str(uuid4()),
                meeting_id=str(meeting.id),
                status=JOB_STATUS_QUEUED,
            )
            await uow.diarization_jobs.create(job)

            turns = [StreamingTurn(speaker="S1", start_time=0.0, end_time=1.0)]
            await uow.diarization_jobs.add_streaming_turns(str(meeting.id), turns)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            await uow.meetings.delete(meeting.id)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert await uow.diarization_jobs.get(job.job_id) is None, "diarization job should cascade delete"
            assert await uow.diarization_jobs.get_streaming_turns(str(meeting.id)) == [], "streaming turns should cascade delete"


@pytest.mark.integration
class TestUnitOfWorkConcurrency:
    """Integration tests for concurrent UoW usage."""

    async def test_concurrent_uow_instances_isolated(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test concurrent UoW instances have isolated transactions."""
        meeting1 = Meeting.create(title="Meeting 1")
        meeting2 = Meeting.create(title="Meeting 2")

        async def create_meeting1() -> None:
            async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
                await uow.meetings.create(meeting1)
                await yield_control()  # Ensure concurrent execution overlap
                await uow.commit()

        async def create_meeting2() -> None:
            async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
                await uow.meetings.create(meeting2)
                await yield_control()  # Ensure concurrent execution overlap
                await uow.commit()

        await asyncio.gather(create_meeting1(), create_meeting2())

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            m1 = await uow.meetings.get(meeting1.id)
            m2 = await uow.meetings.get(meeting2.id)
            assert m1 is not None, "meeting1 should be retrievable"
            assert m2 is not None, "meeting2 should be retrievable"
            assert m1.title == "Meeting 1", "meeting1 title should match"
            assert m2.title == "Meeting 2", "meeting2 title should match"

    async def test_concurrent_updates_to_different_meetings(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test concurrent updates to different meetings succeed."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            m1 = Meeting.create(title="M1")
            m2 = Meeting.create(title="M2")
            await uow.meetings.create(m1)
            await uow.meetings.create(m2)
            await uow.commit()

        async def update_m1() -> None:
            async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
                meeting = await uow.meetings.get(m1.id)
                assert meeting is not None, "m1 should exist for update"
                meeting.start_recording()
                await uow.meetings.update(meeting)
                await uow.commit()

        async def update_m2() -> None:
            async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
                meeting = await uow.meetings.get(m2.id)
                assert meeting is not None, "m2 should exist for update"
                meeting.start_recording()
                await uow.meetings.update(meeting)
                await uow.commit()

        await asyncio.gather(update_m1(), update_m2())

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            final_m1 = await uow.meetings.get(m1.id)
            final_m2 = await uow.meetings.get(m2.id)
            assert final_m1 is not None and final_m1.state == MeetingState.RECORDING, "m1 recording"
            assert final_m2 is not None and final_m2.state == MeetingState.RECORDING, "m2 recording"


@pytest.mark.integration
class TestUnitOfWorkRollbackScenarios:
    """Integration tests for various rollback scenarios."""

    async def test_rollback_after_partial_work(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test explicit rollback after partial work discards all changes."""
        meeting = Meeting.create(title="Rollback Test")

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

            segment1 = Segment(segment_id=0, text="S1", start_time=0.0, end_time=1.0)
            segment2 = Segment(segment_id=1, text="S2", start_time=1.0, end_time=2.0)
            await uow.segments.add(meeting.id, segment1)
            await uow.segments.add(meeting.id, segment2)

            await uow.rollback()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert await uow.meetings.get(meeting.id) is None, "meeting should not exist after rollback"

    async def test_exception_during_segment_add_rolls_back_meeting(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test exception during operation rolls back entire transaction."""
        meeting = Meeting.create(title="Exception Test")

        class TestError(Exception):
            pass

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

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

                raise TestError("Simulated failure")

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert await uow.meetings.get(meeting.id) is None, "meeting should not exist after exception rollback"

    async def test_rollback_then_new_work_in_same_context(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test work after rollback in same UoW context can still commit."""
        meeting1 = Meeting.create(title="Rolled Back")
        meeting2 = Meeting.create(title="Committed")

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            await uow.meetings.create(meeting1)
            await uow.rollback()

            await uow.meetings.create(meeting2)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            assert await uow.meetings.get(meeting1.id) is None, "rolled back meeting should not exist"
            m2 = await uow.meetings.get(meeting2.id)
            assert m2 is not None, "committed meeting should be retrievable"
            assert m2.title == "Committed", f"expected title 'Committed', got {m2.title!r}"


@pytest.mark.integration
class TestUnitOfWorkRepositoryCaching:
    """Integration tests for repository instance caching."""

    async def test_repository_instances_cached_within_context(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test repository instances are cached within a UoW context."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meetings1 = uow.meetings
            meetings2 = uow.meetings
            segments1 = uow.segments
            segments2 = uow.segments
            summaries1 = uow.summaries
            summaries2 = uow.summaries
            annotations1 = uow.annotations
            annotations2 = uow.annotations
            preferences1 = uow.preferences
            preferences2 = uow.preferences
            jobs1 = uow.diarization_jobs
            jobs2 = uow.diarization_jobs

            assert meetings1 is meetings2, "meetings repository should be cached"
            assert segments1 is segments2, "segments repository should be cached"
            assert summaries1 is summaries2, "summaries repository should be cached"
            assert annotations1 is annotations2, "annotations repository should be cached"
            assert preferences1 is preferences2, "preferences repository should be cached"
            assert jobs1 is jobs2, "diarization_jobs repository should be cached"

    async def test_repository_instances_new_per_context(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test each UoW context creates new repository instances."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow1:
            meetings1 = uow1.meetings

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow2:
            meetings2 = uow2.meetings

        assert meetings1 is not meetings2, "repository instances should differ across UoW contexts"


@pytest.mark.integration
class TestUnitOfWorkContextErrors:
    """Integration tests for error handling with context management."""

    async def test_accessing_repo_outside_context_raises(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test accessing repository outside context raises RuntimeError."""
        uow = SqlAlchemyUnitOfWork(session_factory, meetings_dir)

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

    async def test_commit_outside_context_raises(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test commit outside context raises RuntimeError."""
        uow = SqlAlchemyUnitOfWork(session_factory, meetings_dir)

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

    async def test_rollback_outside_context_raises(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test rollback outside context raises RuntimeError."""
        uow = SqlAlchemyUnitOfWork(session_factory, meetings_dir)

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


@pytest.mark.integration
class TestUnitOfWorkComplexWorkflows:
    """Integration tests for complex multi-step workflows."""

    async def test_meeting_lifecycle_workflow(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test complete meeting lifecycle through database."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Lifecycle Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = await uow.meetings.get(meeting.id)
            assert meeting is not None, "meeting should exist after create"
            assert meeting.state == MeetingState.CREATED, f"got {meeting.state}"
            meeting.start_recording()
            await uow.meetings.update(meeting)
            await uow.commit()
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = await uow.meetings.get(meeting.id)
            assert meeting is not None, "meeting should exist after update"
            assert meeting.state == MeetingState.RECORDING, f"got {meeting.state}"
            for i in range(5):
                segment = Segment(
                    segment_id=i,
                    text=f"Text {i}",
                    start_time=float(i),
                    end_time=float(i + 1),
                )
                await uow.segments.add(meeting.id, segment)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = await uow.meetings.get(meeting.id)
            assert meeting is not None, "should exist"
            meeting.begin_stopping()
            meeting.stop_recording()
            await uow.meetings.update(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = await uow.meetings.get(meeting.id)
            assert meeting is not None, "meeting should exist after stopping"
            assert meeting.state == MeetingState.STOPPED, f"got {meeting.state}"
            await uow.summaries.save(Summary(meeting_id=meeting.id, executive_summary="Done"))
            await uow.commit()
            meeting_id = meeting.id

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            final = await uow.meetings.get(meeting_id)
            segments = await uow.segments.get_by_meeting(meeting_id)
            summary = await uow.summaries.get_by_meeting(meeting_id)
            assert final is not None and final.state == MeetingState.STOPPED, "should be STOPPED"
            assert len(segments) == 5, f"got {len(segments)}"
            assert summary is not None, "should have summary"

    async def test_diarization_job_workflow(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test diarization job lifecycle through database."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create()
            await uow.meetings.create(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            job = DiarizationJob(
                job_id=str(uuid4()),
                meeting_id=str(meeting.id),
                status=JOB_STATUS_QUEUED,
            )
            await uow.diarization_jobs.create(job)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            await uow.diarization_jobs.update_status(job.job_id, JOB_STATUS_RUNNING)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            from noteflow.infrastructure.persistence.repositories.diarization_job_repo import (
                JOB_STATUS_COMPLETED,
            )

            await uow.diarization_jobs.update_status(
                job.job_id,
                JOB_STATUS_COMPLETED,
                segments_updated=10,
                speaker_ids=["SPEAKER_00", "SPEAKER_01"],
            )
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            final_job = await uow.diarization_jobs.get(job.job_id)
            assert final_job is not None, "completed job should be retrievable"
            assert final_job.status == JOB_STATUS_COMPLETED, f"expected COMPLETED status, got {final_job.status}"
            assert final_job.segments_updated == 10, f"expected 10 segments updated, got {final_job.segments_updated}"
            assert final_job.speaker_ids == ["SPEAKER_00", "SPEAKER_01"], f"unexpected speaker_ids: {final_job.speaker_ids}"


@pytest.mark.integration
class TestUnitOfWorkPreferencesWorkflow:
    """Integration tests for preferences workflow."""

    async def test_cloud_consent_persistence_workflow(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test cloud consent workflow as used by server startup."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            consent = await uow.preferences.get_bool("cloud_consent_granted", False)
            assert consent is False, "initial consent should be False"

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            await uow.preferences.set("cloud_consent_granted", True)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            consent = await uow.preferences.get_bool("cloud_consent_granted", False)
            assert consent is True, "consent should be True after granting"

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            await uow.preferences.set("cloud_consent_granted", False)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            consent = await uow.preferences.get_bool("cloud_consent_granted", False)
            assert consent is False, "consent should be False after revoking"
