"""End-to-end integration tests for export functionality.

Tests the complete export workflow with database:
- ExportService with real database
- gRPC ExportTranscript with database
- Markdown export format
- HTML export format
- PDF export format
- File export with correct extensions
- Error handling for export operations
"""

from __future__ import annotations

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

import grpc
import pytest

from noteflow.application.services.export_service import ExportFormat, ExportService
from noteflow.domain.entities import Meeting, Segment
from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer
from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

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

# PDF size thresholds (bytes)
MIN_PDF_CONTENT_SIZE = 500


def _weasyprint_available() -> bool:
    """Check if weasyprint is available (may fail due to missing system libraries)."""
    try:
        import weasyprint as _  # noqa: F401

        return True
    except (ImportError, OSError):
        return False


requires_weasyprint = pytest.mark.skipif(
    not _weasyprint_available(),
    reason="weasyprint not available (missing system libraries: pango, gobject)",
)

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


def _call_infer_format_from_extension(
    service: ExportService, extension: str
) -> ExportFormat:
    """Call infer_format_from_extension method for testing.

    This helper provides a consistent interface for testing the format
    inference method.
    """
    return service.infer_format_from_extension(extension)


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 TestExportServiceDatabase:
    """Integration tests for ExportService with database."""

    async def test_export_markdown_from_database(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test exporting meeting as markdown from database."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Export Markdown Test")
            meeting.start_recording()
            meeting.begin_stopping()
            meeting.stop_recording()
            await uow.meetings.create(meeting)

            segments = [
                Segment(
                    segment_id=0,
                    text="Hello everyone.",
                    start_time=0.0,
                    end_time=2.0,
                    speaker_id="SPEAKER_00",
                ),
                Segment(
                    segment_id=1,
                    text="Welcome to the meeting.",
                    start_time=2.0,
                    end_time=5.0,
                    speaker_id="SPEAKER_01",
                ),
            ]
            for segment in segments:
                await uow.segments.add(meeting.id, segment)
            await uow.commit()

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.MARKDOWN)
        assert isinstance(content, str), "Content should be a string"

        assert "Export Markdown Test" in content, "Should contain meeting title"
        assert "Hello everyone" in content, "Should contain first segment text"
        assert "Welcome to the meeting" in content, "Should contain second segment text"
        assert "SPEAKER_00" in content or "[0:00]" in content or "[0:02]" in content, "Should contain speaker label or timestamps"

    async def test_export_html_from_database(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test exporting meeting as HTML from database."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Export HTML Test")
            await uow.meetings.create(meeting)

            segment = Segment(
                segment_id=0,
                text="HTML content test.",
                start_time=0.0,
                end_time=2.0,
            )
            await uow.segments.add(meeting.id, segment)
            await uow.commit()

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.HTML)
        assert isinstance(content, str), f"HTML export should return string, got {type(content).__name__}"

        assert "<html" in content.lower() or "<!doctype" in content.lower(), (
            "HTML export should contain HTML markup (<html or <!doctype)"
        )
        assert "HTML content test" in content, "HTML export should contain segment text 'HTML content test'"

    @pytest.mark.slow
    @requires_weasyprint
    async def test_export_pdf_from_database(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test exporting meeting as PDF from database with full content verification."""

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Export PDF Test")
            meeting.start_recording()
            meeting.begin_stopping()
            meeting.stop_recording()
            await uow.meetings.create(meeting)

            segments = [
                Segment(
                    segment_id=0,
                    text="First speaker talks here.",
                    start_time=0.0,
                    end_time=3.0,
                    speaker_id="Alice",
                ),
                Segment(
                    segment_id=1,
                    text="Second speaker responds.",
                    start_time=3.0,
                    end_time=6.0,
                    speaker_id="Bob",
                ),
            ]
            for segment in segments:
                await uow.segments.add(meeting.id, segment)
            await uow.commit()

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.PDF)

        assert isinstance(content, bytes), "PDF export should return bytes"
        assert content.startswith(b"%PDF-"), "PDF should have valid magic bytes"
        assert len(content) > 1000, "PDF should have substantial content"

    @pytest.mark.slow
    @requires_weasyprint
    async def test_export_to_file_creates_pdf_file(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test export_to_file writes valid binary PDF file to disk."""

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="PDF File Export Test")
            await uow.meetings.create(meeting)

            segment = Segment(
                segment_id=0,
                text="Content for PDF file.",
                start_time=0.0,
                end_time=2.0,
                speaker_id="Speaker",
            )
            await uow.segments.add(meeting.id, segment)
            await uow.commit()

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "transcript.pdf"

            export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
            result_path = await export_service.export_to_file(
                meeting.id,
                output_path,
                ExportFormat.PDF,
            )

            assert result_path.exists(), "PDF file should be created"
            assert result_path.suffix == ".pdf", "File should have .pdf extension"

            file_bytes = result_path.read_bytes()
            assert file_bytes.startswith(b"%PDF-"), "File should contain valid PDF"
            assert len(file_bytes) > MIN_PDF_CONTENT_SIZE, "PDF file should have content"

    async def test_export_to_file_creates_file(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test export_to_file creates the output file."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="File Export Test")
            await uow.meetings.create(meeting)

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

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "transcript.md"

            export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
            result_path = await export_service.export_to_file(
                meeting.id,
                output_path,
                ExportFormat.MARKDOWN,
            )

            assert result_path.exists(), f"Export file should exist at {result_path}"
            content = result_path.read_text()
            assert "File content" in content, "Exported file should contain segment text 'File content'"

    async def test_export_to_file_infers_format_from_extension(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test export_to_file infers format from file extension."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Infer Format Test")
            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)
            await uow.commit()

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "transcript.html"

            export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
            result_path = await export_service.export_to_file(meeting.id, output_path)

            assert result_path.suffix == ".html", f"File should have .html extension, got {result_path.suffix}"
            content = result_path.read_text()
            assert "<" in content, "HTML file should contain HTML tags (angle brackets)"

    async def test_export_nonexistent_meeting_raises(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test exporting nonexistent meeting raises ValueError."""
        from noteflow.domain.value_objects import MeetingId

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))

        with pytest.raises(ValueError, match="not found"):
            await export_service.export_transcript(
                MeetingId(uuid4()),
                ExportFormat.MARKDOWN,
            )


@pytest.mark.integration
class TestExportGrpcServicer:
    """Integration tests for gRPC ExportTranscript."""

    async def test_export_transcript_markdown_via_grpc(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test ExportTranscript RPC with markdown format."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="gRPC Export Test")
            await uow.meetings.create(meeting)

            segments = [
                Segment(
                    segment_id=0,
                    text="First segment.",
                    start_time=0.0,
                    end_time=5.0,
                ),
                Segment(
                    segment_id=1,
                    text="Second segment.",
                    start_time=5.0,
                    end_time=10.0,
                ),
            ]
            for segment in segments:
                await uow.segments.add(meeting.id, segment)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting.id),
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )
        result = await servicer.ExportTranscript(request, MockContext())

        assert result.content, "Content should not be empty"
        assert "First segment" in result.content, "Should contain first segment"
        assert "Second segment" in result.content, "Should contain second segment"
        assert result.file_extension == ".md", "File extension should be .md"

    async def test_export_transcript_html_via_grpc(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test ExportTranscript RPC with HTML format."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="HTML Export Test")
            await uow.meetings.create(meeting)

            segment = Segment(
                segment_id=0,
                text="HTML export content.",
                start_time=0.0,
                end_time=5.0,
            )
            await uow.segments.add(meeting.id, segment)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting.id),
            format=noteflow_pb2.EXPORT_FORMAT_HTML,
        )
        result = await servicer.ExportTranscript(request, MockContext())

        assert result.content, "gRPC HTML export response should have non-empty content"
        assert "HTML export content" in result.content, (
            "gRPC HTML export should contain segment text 'HTML export content'"
        )
        assert result.file_extension == ".html", (
            f"gRPC HTML export should have .html extension, got {result.file_extension}"
        )

    @pytest.mark.slow
    @requires_weasyprint
    async def test_export_transcript_pdf_via_grpc(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test ExportTranscript RPC with PDF format returns valid base64-encoded PDF."""
        import base64

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="gRPC PDF Export Test")
            await uow.meetings.create(meeting)

            segment = Segment(
                segment_id=0,
                text="PDF via gRPC content.",
                start_time=0.0,
                end_time=5.0,
                speaker_id="TestSpeaker",
            )
            await uow.segments.add(meeting.id, segment)
            await uow.commit()

        servicer = NoteFlowServicer(session_factory=session_factory)

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id=str(meeting.id),
            format=noteflow_pb2.EXPORT_FORMAT_PDF,
        )
        result = await servicer.ExportTranscript(request, MockContext())

        assert result.content, "PDF export should return content"
        assert result.file_extension == ".pdf", "File extension should be .pdf"

        # gRPC returns base64-encoded PDF; verify it decodes to valid PDF
        pdf_bytes = base64.b64decode(result.content)
        assert pdf_bytes.startswith(b"%PDF-"), "Decoded content should be valid PDF"
        assert len(pdf_bytes) > MIN_PDF_CONTENT_SIZE, "PDF should have substantial content"

    async def test_export_transcript_nonexistent_meeting(
        self, session_factory: async_sessionmaker[AsyncSession]
    ) -> None:
        """Test ExportTranscript returns NOT_FOUND for nonexistent meeting."""
        servicer = NoteFlowServicer(session_factory=session_factory)
        context = MockContext()

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

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

        assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
            f"Expected NOT_FOUND status for nonexistent meeting, got {context.abort_code}"
        )

    async def test_export_transcript_invalid_meeting_id(
        self, session_factory: async_sessionmaker[AsyncSession]
    ) -> None:
        """Test ExportTranscript returns INVALID_ARGUMENT for invalid ID."""
        servicer = NoteFlowServicer(session_factory=session_factory)
        context = MockContext()

        request = noteflow_pb2.ExportTranscriptRequest(
            meeting_id="not-valid",
            format=noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
        )

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

        assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, (
            f"Expected INVALID_ARGUMENT status for invalid meeting ID, got {context.abort_code}"
        )


@pytest.mark.integration
class TestExportContent:
    """Integration tests for export content formatting."""

    async def test_export_includes_speaker_labels(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test export includes speaker labels when present."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Speaker Test")
            await uow.meetings.create(meeting)

            segments = [
                Segment(
                    segment_id=0,
                    text="Alice speaking.",
                    start_time=0.0,
                    end_time=2.0,
                    speaker_id="Alice",
                ),
                Segment(
                    segment_id=1,
                    text="Bob speaking.",
                    start_time=2.0,
                    end_time=4.0,
                    speaker_id="Bob",
                ),
            ]
            for segment in segments:
                await uow.segments.add(meeting.id, segment)
            await uow.commit()

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.MARKDOWN)
        assert isinstance(content, str), f"Markdown export should return string, got {type(content).__name__}"

        assert "Alice" in content, "Export should include speaker label 'Alice'"
        assert "Bob" in content, "Export should include speaker label 'Bob'"

    async def test_export_includes_timestamps(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test export includes timestamps."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Timestamp Test")
            await uow.meetings.create(meeting)

            segment = Segment(
                segment_id=0,
                text="Timestamped content.",
                start_time=65.5,
                end_time=70.0,
            )
            await uow.segments.add(meeting.id, segment)
            await uow.commit()

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.MARKDOWN)
        assert isinstance(content, str), f"Markdown export should return string, got {type(content).__name__}"

        assert "01:05" in content or "1:05" in content, (
            "Export should include formatted timestamp for 65.5 seconds (01:05 or 1:05)"
        )

    async def test_export_empty_meeting(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test exporting meeting with no segments."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Empty Meeting")
            await uow.meetings.create(meeting)
            await uow.commit()

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.MARKDOWN)
        assert isinstance(content, str), f"Markdown export should return string, got {type(content).__name__}"

        assert "Empty Meeting" in content, "Export should include meeting title 'Empty Meeting'"

    async def test_export_long_meeting(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test exporting a meeting with many segments."""
        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="Long Meeting")
            await uow.meetings.create(meeting)

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

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.MARKDOWN)
        assert isinstance(content, str), "Content should be a string"

        assert "Segment number 0" in content, "Should contain first segment"
        assert "Segment number 99" in content, "Should contain last segment"

        lines = content.split("\n")
        assert len(lines) > 100, "Should have more than 100 lines"


@pytest.mark.integration
class TestExportFormats:
    """Integration tests for export format handling."""

    async def test_get_supported_formats(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test getting list of supported export formats."""
        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        formats = export_service.get_supported_formats()

        assert len(formats) >= 3, "Should support at least markdown, html, and pdf"

        extensions = [f[1] for f in formats]

        assert ".md" in extensions, "Markdown format should be supported"
        assert ".html" in extensions, "HTML format should be supported"
        assert ".pdf" in extensions, "PDF format should be supported"

    async def test_infer_format_markdown(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test format inference from .md extension."""
        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))

        fmt = _call_infer_format_from_extension(export_service, ".md")
        assert fmt == ExportFormat.MARKDOWN, f"Expected MARKDOWN format for .md extension, got {fmt}"

        fmt = _call_infer_format_from_extension(export_service, ".markdown")
        assert fmt == ExportFormat.MARKDOWN, f"Expected MARKDOWN format for .markdown extension, got {fmt}"

    async def test_infer_format_html(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test format inference from .html extension."""
        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))

        fmt = _call_infer_format_from_extension(export_service, ".html")
        assert fmt == ExportFormat.HTML, f"Expected HTML format for .html extension, got {fmt}"

        fmt = _call_infer_format_from_extension(export_service, ".htm")
        assert fmt == ExportFormat.HTML, f"Expected HTML format for .htm extension, got {fmt}"

    async def test_infer_format_pdf(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test format inference from .pdf extension."""
        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))

        fmt = _call_infer_format_from_extension(export_service, ".pdf")
        assert fmt == ExportFormat.PDF, "Should infer PDF format from .pdf extension"

    async def test_infer_format_unknown_raises(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test format inference raises for unknown extension."""
        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))

        with pytest.raises(ValueError, match="Cannot infer format"):
            _call_infer_format_from_extension(export_service, ".txt")

    @pytest.mark.slow
    @requires_weasyprint
    async def test_export_pdf_returns_bytes(
        self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
    ) -> None:
        """Test PDF export returns bytes with valid PDF magic bytes."""

        async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
            meeting = Meeting.create(title="PDF Export Test")
            await uow.meetings.create(meeting)

            segment = Segment(
                segment_id=0,
                text="PDF content test.",
                start_time=0.0,
                end_time=2.0,
            )
            await uow.segments.add(meeting.id, segment)
            await uow.commit()

        export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
        content = await export_service.export_transcript(meeting.id, ExportFormat.PDF)

        assert isinstance(content, bytes), "PDF export should return bytes"
        assert content.startswith(b"%PDF-"), "PDF should have valid magic bytes"
