"""End-to-end integration tests for annotation operations.

Tests the complete annotation CRUD workflow via gRPC with database:
- Adding annotations
- Retrieving annotations
- Listing annotations with filters
- Updating annotations
- Deleting annotations
- Error handling for database-required operations
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, cast
from uuid import UUID, uuid4

import grpc
import pytest

from noteflow.domain.entities import Meeting
from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer
from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork
from tests.conftest import approx_float

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

# ============================================================================
# Test Constants
# ============================================================================

# Annotation timestamps
ANNOTATION_START_TIME = 10.5
ANNOTATION_END_TIME_SECONDS = 15.0


class MockContext:
    """Mock gRPC context for testing."""

    def __init__(self) -> None:
        """Initialize mock context."""
        self.aborted = False
        self.abort_code: grpc.StatusCode | None = None
        self.abort_details: str | None = None

    async def abort(self, code: grpc.StatusCode, details: str) -> None:
        """Record abort and raise to simulate gRPC behavior."""
        self.aborted = True
        self.abort_code = code
        self.abort_details = details
        raise grpc.RpcError()


@pytest.mark.integration
class TestAnnotationCRUD:
    """Integration tests for annotation CRUD operations."""

    async def test_add_annotation_persists_to_database(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test adding an annotation persists it to database."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Annotation Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="Important point discussed",
            start_time=ANNOTATION_START_TIME,
            end_time=ANNOTATION_END_TIME_SECONDS,
            segment_ids=[0, 1, 2],
        )
        result: noteflow_pb2.Annotation = await servicer.AddAnnotation(request, MockContext())

        assert result.id, "annotation ID should be assigned"
        assert result.text == "Important point discussed", f"expected annotation text 'Important point discussed', got {result.text!r}"
        assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_NOTE, f"expected annotation type ANNOTATION_TYPE_NOTE, got {result.annotation_type}"
        assert result.start_time == approx_float(ANNOTATION_START_TIME), f"expected start_time {ANNOTATION_START_TIME}, got {result.start_time}"
        assert result.end_time == approx_float(ANNOTATION_END_TIME_SECONDS), f"expected end_time {ANNOTATION_END_TIME_SECONDS}, got {result.end_time}"
        segment_ids_list: list[int] = list(cast(list[int], result.segment_ids))
        assert segment_ids_list == [0, 1, 2], f"expected segment_ids [0, 1, 2], got {segment_ids_list}"

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            from noteflow.domain.value_objects import AnnotationId

            saved = await uow.annotations.get(AnnotationId(UUID(result.id)))
            assert saved is not None, "annotation should be persisted in database"
            assert saved.text == "Important point discussed", f"expected persisted text 'Important point discussed', got {saved.text!r}"

    async def test_get_annotation_retrieves_from_database(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test getting an annotation by ID retrieves from database."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Get Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        add_request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM,
            text="Follow up on this",
            start_time=5.0,
            end_time=10.0,
        )
        added: noteflow_pb2.Annotation = await servicer.AddAnnotation(add_request, MockContext())

        get_request = noteflow_pb2.GetAnnotationRequest(annotation_id=added.id)
        result: noteflow_pb2.Annotation = await servicer.GetAnnotation(get_request, MockContext())

        assert result.id == added.id, f"expected annotation ID {added.id}, got {result.id}"
        assert result.text == "Follow up on this", f"expected text 'Follow up on this', got {result.text!r}"
        assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM, f"expected annotation type ANNOTATION_TYPE_ACTION_ITEM, got {result.annotation_type}"

    async def test_list_annotations_for_meeting(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test listing all annotations for a meeting."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="List Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        for i in range(3):
            request = noteflow_pb2.AddAnnotationRequest(
                meeting_id=str(meeting.id),
                annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
                text=f"Annotation {i}",
                start_time=float(i * 10),
                end_time=float((i + 1) * 10),
            )
            await servicer.AddAnnotation(request, MockContext())

        list_request = noteflow_pb2.ListAnnotationsRequest(meeting_id=str(meeting.id))
        result: noteflow_pb2.ListAnnotationsResponse = await servicer.ListAnnotations(list_request, MockContext())

        annotations_count: int = len(cast(list[noteflow_pb2.Annotation], result.annotations))
        assert annotations_count == 3, f"expected 3 annotations, got {annotations_count}"

    async def test_list_annotations_with_time_range_filter(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test listing annotations filtered by time range.

        Time range filter uses overlap logic - annotations are included if
        they overlap with the query range in any way.
        """
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Time Range Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        # Create annotations with clear separation from query boundaries
        for start, end in [(0, 8), (15, 25), (30, 40), (55, 60)]:
            request = noteflow_pb2.AddAnnotationRequest(
                meeting_id=str(meeting.id),
                annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
                text=f"Annotation at {start}-{end}",
                start_time=float(start),
                end_time=float(end),
            )
            await servicer.AddAnnotation(request, MockContext())

        # Query range 10-50 should include (15-25) and (30-40)
        list_request = noteflow_pb2.ListAnnotationsRequest(
            meeting_id=str(meeting.id),
            start_time=10.0,
            end_time=50.0,
        )
        result: noteflow_pb2.ListAnnotationsResponse = await servicer.ListAnnotations(list_request, MockContext())

        annotations_count: int = len(cast(list[noteflow_pb2.Annotation], result.annotations))
        assert annotations_count == 2, f"expected 2 annotations in time range 10-50, got {annotations_count}"

    async def test_update_annotation_modifies_database(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test updating an annotation modifies the database record."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Update Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        add_request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="Original text",
            start_time=5.0,
            end_time=10.0,
        )
        added: noteflow_pb2.Annotation = await servicer.AddAnnotation(add_request, MockContext())

        update_request = noteflow_pb2.UpdateAnnotationRequest(
            annotation_id=added.id,
            text="Updated text",
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM,
        )
        result: noteflow_pb2.Annotation = await servicer.UpdateAnnotation(update_request, MockContext())

        assert result.text == "Updated text", f"expected updated text 'Updated text', got {result.text!r}"
        assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM, f"expected updated type ANNOTATION_TYPE_ACTION_ITEM, got {result.annotation_type}"

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            from noteflow.domain.value_objects import AnnotationId

            saved = await uow.annotations.get(AnnotationId(UUID(added.id)))
            assert saved is not None, "updated annotation should exist in database"
            assert saved.text == "Updated text", f"expected database text 'Updated text', got {saved.text!r}"

    async def test_delete_annotation_removes_from_database(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test deleting an annotation removes it from database."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Delete Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        add_request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="To be deleted",
            start_time=0.0,
            end_time=5.0,
        )
        added: noteflow_pb2.Annotation = await servicer.AddAnnotation(add_request, MockContext())

        delete_request = noteflow_pb2.DeleteAnnotationRequest(annotation_id=added.id)
        result: noteflow_pb2.DeleteAnnotationResponse = await servicer.DeleteAnnotation(delete_request, MockContext())

        assert result.success is True, "delete operation should return success=True"

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            from noteflow.domain.value_objects import AnnotationId

            deleted = await uow.annotations.get(AnnotationId(UUID(added.id)))
            assert deleted is None, "annotation should be removed from database after deletion"


@pytest.mark.integration
class TestAnnotationTypes:
    """Integration tests for different annotation types."""

    async def test_note_annotation_type(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test creating a NOTE type annotation."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create()
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="A note",
            start_time=0.0,
            end_time=1.0,
        )
        result: noteflow_pb2.Annotation = await servicer.AddAnnotation(request, MockContext())

        assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_NOTE, f"expected ANNOTATION_TYPE_NOTE, got {result.annotation_type}"

    async def test_action_item_annotation_type(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test creating an ACTION_ITEM type annotation."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create()
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM,
            text="An action item",
            start_time=0.0,
            end_time=1.0,
        )
        result: noteflow_pb2.Annotation = await servicer.AddAnnotation(request, MockContext())

        assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM, f"expected ANNOTATION_TYPE_ACTION_ITEM, got {result.annotation_type}"

    async def test_decision_annotation_type(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test creating a DECISION type annotation."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create()
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_DECISION,
            text="A decision",
            start_time=0.0,
            end_time=1.0,
        )
        result: noteflow_pb2.Annotation = await servicer.AddAnnotation(request, MockContext())

        assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_DECISION, f"expected ANNOTATION_TYPE_DECISION, got {result.annotation_type}"


@pytest.mark.integration
class TestAnnotationErrors:
    """Integration tests for annotation error handling."""

    async def test_add_annotation_invalid_meeting_id(
        self, session_factory: async_sessionmaker[AsyncSession]
    ) -> None:
        """Test adding annotation with invalid meeting ID fails."""
        servicer = NoteFlowServicer(session_factory=session_factory)
        context = MockContext()

        request = noteflow_pb2.AddAnnotationRequest(
            meeting_id="invalid",
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="Test",
            start_time=0.0,
            end_time=1.0,
        )

        with pytest.raises(grpc.RpcError, match=r".*"):
            await servicer.AddAnnotation(request, context)

        assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, f"expected INVALID_ARGUMENT status code, got {context.abort_code}"

    async def test_get_annotation_not_found(
        self, session_factory: async_sessionmaker[AsyncSession]
    ) -> None:
        """Test getting nonexistent annotation returns NOT_FOUND."""
        servicer = NoteFlowServicer(session_factory=session_factory)
        context = MockContext()

        request = noteflow_pb2.GetAnnotationRequest(annotation_id=str(uuid4()))

        with pytest.raises(grpc.RpcError, match=r".*"):
            await servicer.GetAnnotation(request, context)

        assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status code for nonexistent annotation, got {context.abort_code}"

    async def test_update_annotation_not_found(
        self, session_factory: async_sessionmaker[AsyncSession]
    ) -> None:
        """Test updating nonexistent annotation returns NOT_FOUND."""
        servicer = NoteFlowServicer(session_factory=session_factory)
        context = MockContext()

        request = noteflow_pb2.UpdateAnnotationRequest(
            annotation_id=str(uuid4()),
            text="Updated",
        )

        with pytest.raises(grpc.RpcError, match=r".*"):
            await servicer.UpdateAnnotation(request, context)

        assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status code for updating nonexistent annotation, got {context.abort_code}"

    async def test_delete_annotation_not_found_e2e(
        self, session_factory: async_sessionmaker[AsyncSession]
    ) -> None:
        """Test deleting nonexistent annotation returns NOT_FOUND."""
        servicer = NoteFlowServicer(session_factory=session_factory)
        context = MockContext()

        request = noteflow_pb2.DeleteAnnotationRequest(annotation_id=str(uuid4()))

        with pytest.raises(grpc.RpcError, match=r".*"):
            await servicer.DeleteAnnotation(request, context)

        assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status code for deleting nonexistent annotation, got {context.abort_code}"


@pytest.mark.integration
class TestAnnotationIsolation:
    """Integration tests for annotation meeting isolation."""

    async def test_annotations_isolated_between_meetings(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test annotations from one meeting don't appear in another."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting1 = Meeting.create(title="Meeting 1")
            meeting2 = Meeting.create(title="Meeting 2")
            await uow.meetings.create(meeting1)
            await uow.meetings.create(meeting2)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request1 = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting1.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="Meeting 1 annotation",
            start_time=0.0,
            end_time=1.0,
        )
        await servicer.AddAnnotation(request1, MockContext())

        request2 = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting2.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="Meeting 2 annotation",
            start_time=0.0,
            end_time=1.0,
        )
        await servicer.AddAnnotation(request2, MockContext())

        list_request = noteflow_pb2.ListAnnotationsRequest(meeting_id=str(meeting1.id))
        result: noteflow_pb2.ListAnnotationsResponse = await servicer.ListAnnotations(list_request, MockContext())

        annotations: list[noteflow_pb2.Annotation] = cast(list[noteflow_pb2.Annotation], result.annotations)
        annotations_count: int = len(annotations)
        assert annotations_count == 1, f"expected 1 annotation for meeting 1, got {annotations_count}"
        first_annotation: noteflow_pb2.Annotation = annotations[0]
        assert first_annotation.text == "Meeting 1 annotation", f"expected 'Meeting 1 annotation', got {first_annotation.text!r}"

    async def test_annotations_deleted_with_meeting(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test annotations are cascade deleted when meeting is deleted."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Cascade Test")
            await uow.meetings.create(meeting)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.AddAnnotationRequest(
            meeting_id=str(meeting.id),
            annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
            text="Will be deleted",
            start_time=0.0,
            end_time=1.0,
        )
        added: noteflow_pb2.Annotation = await servicer.AddAnnotation(request, MockContext())
        annotation_id: str = added.id

        delete_request = noteflow_pb2.DeleteMeetingRequest(meeting_id=str(meeting.id))
        await servicer.DeleteMeeting(delete_request, MockContext())

        context = MockContext()
        get_request = noteflow_pb2.GetAnnotationRequest(annotation_id=annotation_id)

        with pytest.raises(grpc.RpcError, match=r".*"):
            await servicer.GetAnnotation(get_request, context)

        assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND for annotation after meeting deletion, got {context.abort_code}"
