"""Tests for ExportMixin gRPC endpoints.

Tests cover:
- ExportTranscript with markdown format
- ExportTranscript with html format
- ExportTranscript with pdf format
- ExportTranscript meeting not found error
- ExportTranscript with segments containing transcript data
"""

from __future__ import annotations

import base64
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest

from noteflow.application.services.export_service import ExportFormat
from noteflow.domain.entities import Meeting, Segment
from noteflow.domain.value_objects import MeetingId
from noteflow.grpc._mixins._types import GrpcContext
from noteflow.grpc._mixins.export import ExportMixin
from noteflow.grpc.proto import noteflow_pb2


class MockRepositoryProvider:
    """Mock repository provider as async context manager."""

    def __init__(
        self,
        meetings_repo: AsyncMock,
        segments_repo: AsyncMock,
    ) -> None:
        """Initialize with mock repositories."""
        self.meetings = meetings_repo
        self.segments = segments_repo
        self.commit = AsyncMock()

    async def __aenter__(self) -> MockRepositoryProvider:
        """Enter async context."""
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: object,
    ) -> None:
        """Exit async context."""


class MockServicerHost(ExportMixin):
    """Mock servicer host implementing required protocol."""

    def __init__(
        self,
        meetings_repo: AsyncMock,
        segments_repo: AsyncMock,
    ) -> None:
        """Initialize with mock repositories."""
        self._meetings_repo = meetings_repo
        self._segments_repo = segments_repo

    def create_repository_provider(self) -> MockRepositoryProvider:
        """Create mock repository provider context manager."""
        return MockRepositoryProvider(self._meetings_repo, self._segments_repo)

    # Type stub for mixin method to fix type inference
    if TYPE_CHECKING:
        async def ExportTranscript(
            self,
            request: noteflow_pb2.ExportTranscriptRequest,
            context: GrpcContext,
        ) -> noteflow_pb2.ExportTranscriptResponse: ...


def create_test_meeting(
    meeting_id: MeetingId | None = None,
    title: str = "Test Meeting",
) -> Meeting:
    """Create a test meeting instance."""
    if meeting_id is None:
        meeting_id = MeetingId(uuid4())
    return Meeting(
        id=meeting_id,
        title=title,
    )


def create_test_segment(
    segment_id: int,
    text: str,
    start_time: float,
    end_time: float,
    meeting_id: MeetingId | None = None,
) -> Segment:
    """Create a test segment instance."""
    return Segment(
        segment_id=segment_id,
        text=text,
        start_time=start_time,
        end_time=end_time,
        meeting_id=meeting_id,
        language="en",
    )


@pytest.fixture
def mock_segments_repo() -> AsyncMock:
    """Create mock segments repository."""
    repo = AsyncMock()
    repo.get_by_meeting = AsyncMock(return_value=[])
    return repo


@pytest.fixture
def export_servicer(
    mock_meetings_repo: AsyncMock,
    mock_segments_repo: AsyncMock,
) -> MockServicerHost:
    """Create servicer with mock repositories for export tests."""
    return MockServicerHost(mock_meetings_repo, mock_segments_repo)


class TestExportTranscriptMarkdown:
    """Tests for ExportTranscript with markdown format."""

    async def test_exports_markdown_successfully(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript returns markdown content for valid meeting."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Team Standup")
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = []

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(
                return_value="# Team Standup\n\nNo segments."
            )
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert response.content == "# Team Standup\n\nNo segments.", "response content should match expected markdown output"
        assert response.format_name == "Markdown", "format_name should be 'Markdown'"
        assert response.file_extension == ".md", "file_extension should be '.md' for markdown"

    async def test_exports_markdown_with_segments(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript includes segment text in markdown output."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Sprint Planning")
        segments = [
            create_test_segment(0, "Welcome everyone.", 0.0, 2.5, meeting_id),
            create_test_segment(1, "Let's discuss the backlog.", 2.5, 5.0, meeting_id),
        ]
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = segments

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

        expected_content = """# Sprint Planning

[00:00] Welcome everyone.
[00:02] Let's discuss the backlog."""

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=expected_content)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert response.content == expected_content, "response content should include all segment text"
        assert response.format_name == "Markdown", "format_name should be 'Markdown'"

    async def test_exports_markdown_as_default_format(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript defaults to markdown when format is unspecified."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id)
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = []

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_UNSPECIFIED,
        )

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value="# Meeting")
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        mock_service.export_transcript.assert_called_once()
        call_args = mock_service.export_transcript.call_args
        assert call_args[0][1] == ExportFormat.MARKDOWN, "unspecified format should default to MARKDOWN"
        assert response.format_name == "Markdown", "format_name should be 'Markdown' for default format"


class TestExportTranscriptHtml:
    """Tests for ExportTranscript with HTML format."""

    async def test_exports_html_successfully(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript returns HTML content for valid meeting."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Design Review")
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = []

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_HTML,
        )

        html_content = "<html><body><h1>Design Review</h1></body></html>"

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=html_content)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert response.content == html_content, "response content should match expected HTML output"
        assert response.format_name == "HTML", "format_name should be 'HTML'"
        assert response.file_extension == ".html", "file_extension should be '.html' for HTML"

    async def test_exports_html_with_segments(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript includes segment text in HTML output."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Code Review")
        segments = [
            create_test_segment(0, "The pull request looks good.", 0.0, 3.0, meeting_id),
        ]
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = segments

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_HTML,
        )

        html_content = """<html>
<body>
<h1>Code Review</h1>
<p><span class="timestamp">00:00</span> The pull request looks good.</p>
</body>
</html>"""

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=html_content)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert "Code Review" in response.content, "response content should include meeting title"
        assert response.format_name == "HTML", "format_name should be 'HTML'"


class TestExportTranscriptPdf:
    """Tests for ExportTranscript with PDF format."""

    async def test_exports_pdf_as_base64(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript returns base64-encoded PDF content."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Quarterly Report")
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = []

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_PDF,
        )

        # Simulate PDF bytes
        pdf_bytes = b"%PDF-1.4 fake pdf content"
        expected_base64 = base64.b64encode(pdf_bytes).decode("ascii")

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=pdf_bytes)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert response.content == expected_base64, "response content should be base64-encoded PDF"
        assert response.format_name == "PDF", "format_name should be 'PDF'"
        assert response.file_extension == ".pdf", "file_extension should be '.pdf' for PDF"

    async def test_pdf_content_is_decodable(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript PDF content can be decoded back to bytes."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id)
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = []

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_PDF,
        )

        original_pdf_bytes = b"%PDF-1.4\nReal PDF binary data here"

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=original_pdf_bytes)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        decoded_bytes = base64.b64decode(response.content)
        assert decoded_bytes == original_pdf_bytes, "decoded base64 content should match original PDF bytes"


class TestExportTranscriptMeetingNotFound:
    """Tests for ExportTranscript error handling."""

    async def test_export_aborts_when_meeting_not_found(
        self,
        export_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript aborts with NOT_FOUND when meeting does not exist."""
        nonexistent_id = str(uuid4())

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=nonexistent_id,
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(
                side_effect=ValueError(f"Meeting {nonexistent_id} not found")
            )
            mock_export_service_cls.return_value = mock_service

            with pytest.raises(AssertionError, match="Unreachable"):
                await export_servicer.ExportTranscript(request, mock_grpc_context)

        mock_grpc_context.abort.assert_called_once()

    async def test_aborts_with_invalid_meeting_id(
        self,
        export_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript aborts with INVALID_ARGUMENT for malformed meeting ID."""
        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id="not-a-valid-uuid",
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

        with pytest.raises(AssertionError, match="Unreachable"):
            await export_servicer.ExportTranscript(request, mock_grpc_context)

        mock_grpc_context.abort.assert_called_once()

    async def test_aborts_with_empty_meeting_id(
        self,
        export_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript aborts with INVALID_ARGUMENT for empty meeting ID."""
        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id="",
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

        with pytest.raises(AssertionError, match="Unreachable"):
            await export_servicer.ExportTranscript(request, mock_grpc_context)

        mock_grpc_context.abort.assert_called_once()


class TestExportTranscriptWithSegments:
    """Tests for ExportTranscript with various segment configurations."""

    async def test_exports_meeting_with_multiple_speakers(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript handles segments with speaker labels."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Interview")
        segment1 = create_test_segment(
            0, "Tell me about your experience.", 0.0, 3.0, meeting_id
        )
        segment1.speaker_id = "SPEAKER_00"
        segment2 = create_test_segment(
            1, "I have five years of experience.", 3.0, 6.0, meeting_id
        )
        segment2.speaker_id = "SPEAKER_01"

        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = [segment1, segment2]

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

        expected = """# Interview

**SPEAKER_00** [00:00]: Tell me about your experience.
**SPEAKER_01** [00:03]: I have five years of experience."""

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=expected)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert "SPEAKER_00" in response.content, "response content should include first speaker label"
        assert "SPEAKER_01" in response.content, "response content should include second speaker label"

    async def test_exports_empty_transcript(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript handles meetings with no segments."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Empty Meeting")
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = []

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(
                return_value="# Empty Meeting\n\nNo transcript available."
            )
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert response.content == "# Empty Meeting\n\nNo transcript available.", "response content should indicate no transcript available"

    async def test_exports_long_transcript(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
    ) -> None:
        """ExportTranscript handles meetings with many segments."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id, title="Long Meeting")

        # Create 100 segments
        segments = [
            create_test_segment(
                i,
                f"This is segment number {i}.",
                i * 5.0,
                (i + 1) * 5.0,
                meeting_id,
            )
            for i in range(100)
        ]

        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = segments

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=noteflow_pb2.EXPORT_FORMAT_HTML,
        )

        long_content = "<html><body>" + "".join(
            f"<p>{s.text}</p>" for s in segments
        ) + "</body></html>"

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=long_content)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert "segment number 0" in response.content, "response content should include first segment"
        assert "segment number 99" in response.content, "response content should include last segment"


class TestExportFormatMetadata:
    """Tests for export format metadata in responses."""

    @pytest.mark.parametrize(
        ("proto_format", "expected_name", "expected_ext"),
        [
            (noteflow_pb2.EXPORT_FORMAT_MARKDOWN, "Markdown", ".md"),
            (noteflow_pb2.EXPORT_FORMAT_HTML, "HTML", ".html"),
            (noteflow_pb2.EXPORT_FORMAT_PDF, "PDF", ".pdf"),
            (noteflow_pb2.EXPORT_FORMAT_UNSPECIFIED, "Markdown", ".md"),
        ],
        ids=["markdown", "html", "pdf", "unspecified-defaults-to-markdown"],
    )
    async def test_returns_correct_format_metadata(
        self,
        export_servicer: MockServicerHost,
        mock_meetings_repo: AsyncMock,
        mock_segments_repo: AsyncMock,
        mock_grpc_context: MagicMock,
        proto_format: int,
        expected_name: str,
        expected_ext: str,
    ) -> None:
        """ExportTranscript returns correct format name and extension."""
        meeting_id = MeetingId(uuid4())
        meeting = create_test_meeting(meeting_id=meeting_id)
        mock_meetings_repo.get.return_value = meeting
        mock_segments_repo.get_by_meeting.return_value = []

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting_id),
            format=proto_format,
        )

        # Return bytes for PDF, string for others
        is_pdf = proto_format == noteflow_pb2.EXPORT_FORMAT_PDF
        export_result = b"pdf content" if is_pdf else "text content"

        with patch(
            "noteflow.grpc._mixins.export.ExportService"
        ) as mock_export_service_cls:
            mock_service = MagicMock()
            mock_service.export_transcript = AsyncMock(return_value=export_result)
            mock_export_service_cls.return_value = mock_service

            response = await export_servicer.ExportTranscript(request, mock_grpc_context)

        assert response.format_name == expected_name, f"format_name should be '{expected_name}'"
        assert response.file_extension == expected_ext, f"file_extension should be '{expected_ext}'"
