"""Tests for summarization service."""

from __future__ import annotations

from collections.abc import Sequence
from datetime import UTC, datetime
from uuid import uuid4

import pytest

from noteflow.application.services import (
    SummarizationMode,
    SummarizationService,
    SummarizationServiceSettings,
)
from noteflow.domain.entities import KeyPoint, Segment, Summary
from noteflow.domain.summarization import (
    CitationVerificationResult,
    ProviderUnavailableError,
    SummarizationRequest,
    SummarizationResult,
)
from noteflow.domain.value_objects import MeetingId


def _segment(segment_id: int, text: str = "Test") -> Segment:
    """Create a test segment."""
    return Segment(
        segment_id=segment_id,
        text=text,
        start_time=segment_id * 5.0,
        end_time=(segment_id + 1) * 5.0,
    )


class MockProvider:
    """Mock summarizer provider for testing."""

    def __init__(
        self,
        name: str = "mock",
        available: bool = True,
        requires_consent: bool = False,
    ) -> None:
        self._name = name
        self._available = available
        self._requires_consent = requires_consent
        self.call_count = 0

    @property
    def provider_name(self) -> str:
        return self._name

    @property
    def is_available(self) -> bool:
        return self._available

    @property
    def requires_cloud_consent(self) -> bool:
        return self._requires_consent

    async def summarize(self, request: SummarizationRequest) -> SummarizationResult:
        self.call_count += 1
        summary = Summary(
            meeting_id=request.meeting_id,
            executive_summary=f"Summary from {self._name}",
            key_points=[KeyPoint(text=f"Point from {self._name}", segment_ids=[0])],
            generated_at=datetime.now(UTC),
        )
        return SummarizationResult(
            summary=summary,
            model_name=f"{self._name}-model",
            provider_name=self._name,
        )


class MockVerifier:
    """Mock citation verifier for testing."""

    def __init__(self, is_valid: bool = True) -> None:
        self._is_valid = is_valid
        self.verify_call_count = 0
        self.filter_call_count = 0

    def verify_citations(
        self, summary: Summary, segments: Sequence[Segment]
    ) -> CitationVerificationResult:
        self.verify_call_count += 1
        if self._is_valid:
            return CitationVerificationResult(is_valid=True)
        return CitationVerificationResult(
            is_valid=False,
            invalid_key_point_indices=(0,),
            missing_segment_ids=(99,),
        )

    def filter_invalid_citations(self, summary: Summary, segments: Sequence[Segment]) -> Summary:
        self.filter_call_count += 1
        # Return summary with empty segment_ids for key points
        return Summary(
            meeting_id=summary.meeting_id,
            executive_summary=summary.executive_summary,
            key_points=[KeyPoint(text=kp.text, segment_ids=[]) for kp in summary.key_points],
            action_items=[],
            generated_at=summary.generated_at,
        )


class TestSummarizationServiceConfiguration:
    """Tests for SummarizationService configuration."""

    def test_register_provider(self) -> None:
        """Provider should be registered for mode."""
        service = SummarizationService()
        provider = MockProvider()

        service.register_provider(SummarizationMode.LOCAL, provider)

        assert SummarizationMode.LOCAL in service.providers

    def test_set_verifier(self) -> None:
        """Verifier should be set."""
        service = SummarizationService()
        verifier = MockVerifier()

        service.set_verifier(verifier)

        assert service.verifier is verifier

    def test_get_available_modes_with_local(self) -> None:
        """Available modes should include local when provider is available."""
        service = SummarizationService()
        service.register_provider(SummarizationMode.LOCAL, MockProvider())

        available = service.get_available_modes()

        assert SummarizationMode.LOCAL in available

    def test_get_available_modes_excludes_unavailable(self) -> None:
        """Unavailable providers should not be in available modes."""
        service = SummarizationService()
        service.register_provider(SummarizationMode.LOCAL, MockProvider(available=False))

        available = service.get_available_modes()

        assert SummarizationMode.LOCAL not in available

    @pytest.mark.asyncio
    async def test_cloud_requires_consent(self) -> None:
        """Cloud mode should require consent to be available."""
        service = SummarizationService()
        service.register_provider(
            SummarizationMode.CLOUD,
            MockProvider(name="cloud", requires_consent=True),
        )

        available_without_consent = service.get_available_modes()
        await service.grant_cloud_consent()
        available_with_consent = service.get_available_modes()

        assert SummarizationMode.CLOUD not in available_without_consent
        assert SummarizationMode.CLOUD in available_with_consent

    @pytest.mark.asyncio
    async def test_revoke_cloud_consent(self) -> None:
        """Revoking consent should remove cloud from available modes."""
        service = SummarizationService()
        service.register_provider(
            SummarizationMode.CLOUD,
            MockProvider(name="cloud", requires_consent=True),
        )
        await service.grant_cloud_consent()

        await service.revoke_cloud_consent()
        available = service.get_available_modes()

        assert SummarizationMode.CLOUD not in available


class TestSummarizationServiceSummarize:
    """Tests for SummarizationService.summarize method."""

    @pytest.mark.asyncio
    async def test_summarize_uses_default_mode(self, meeting_id: MeetingId) -> None:
        """Summarize should use default mode when not specified."""
        provider = MockProvider()
        service = SummarizationService(
            settings=SummarizationServiceSettings(default_mode=SummarizationMode.LOCAL)
        )
        service.register_provider(SummarizationMode.LOCAL, provider)

        segments = [_segment(0)]
        result = await service.summarize(meeting_id, segments)

        assert result.provider_used == "mock"
        assert provider.call_count == 1

    @pytest.mark.asyncio
    async def test_summarize_uses_specified_mode(self, meeting_id: MeetingId) -> None:
        """Summarize should use specified mode."""
        local_provider = MockProvider(name="local")
        mock_provider = MockProvider(name="mock")
        service = SummarizationService()
        service.register_provider(SummarizationMode.LOCAL, local_provider)
        service.register_provider(SummarizationMode.MOCK, mock_provider)

        segments = [_segment(0)]
        result = await service.summarize(meeting_id, segments, mode=SummarizationMode.MOCK)

        assert result.provider_used == "mock"
        assert mock_provider.call_count == 1
        assert local_provider.call_count == 0

    @pytest.mark.asyncio
    async def test_summarize_falls_back_on_unavailable(self, meeting_id: MeetingId) -> None:
        """Should fall back to available provider when primary unavailable."""
        unavailable = MockProvider(name="cloud", available=False)
        fallback = MockProvider(name="local")
        service = SummarizationService(
            settings=SummarizationServiceSettings(
                fallback_to_local=True,
                cloud_consent_granted=True,
            )
        )
        service.register_provider(SummarizationMode.CLOUD, unavailable)
        service.register_provider(SummarizationMode.LOCAL, fallback)

        segments = [_segment(0)]
        result = await service.summarize(meeting_id, segments, mode=SummarizationMode.CLOUD)

        assert result.provider_used == "local"
        assert result.fallback_used is True

    @pytest.mark.asyncio
    async def test_summarize_raises_when_no_fallback(self, meeting_id: MeetingId) -> None:
        """Should raise error when no fallback available."""
        unavailable = MockProvider(name="local", available=False)
        service = SummarizationService(
            settings=SummarizationServiceSettings(fallback_to_local=False)
        )
        service.register_provider(SummarizationMode.LOCAL, unavailable)

        segments = [_segment(0)]
        with pytest.raises(ProviderUnavailableError, match=r".*"):
            await service.summarize(meeting_id, segments, mode=SummarizationMode.LOCAL)

    @pytest.mark.asyncio
    async def test_summarize_verifies_citations(self, meeting_id: MeetingId) -> None:
        """Citations should be verified when enabled."""
        provider = MockProvider()
        verifier = MockVerifier(is_valid=True)
        service = SummarizationService(settings=SummarizationServiceSettings(verify_citations=True))
        service.register_provider(SummarizationMode.LOCAL, provider)
        service.set_verifier(verifier)

        segments = [_segment(0)]
        result = await service.summarize(meeting_id, segments)

        assert verifier.verify_call_count == 1
        assert result.verification is not None
        assert result.verification.is_valid is True

    @pytest.mark.asyncio
    async def test_summarize_filters_invalid_citations(self, meeting_id: MeetingId) -> None:
        """Invalid citations should be filtered when enabled."""
        provider = MockProvider()
        verifier = MockVerifier(is_valid=False)
        service = SummarizationService(
            settings=SummarizationServiceSettings(
                verify_citations=True,
                filter_invalid_citations=True,
            )
        )
        service.register_provider(SummarizationMode.LOCAL, provider)
        service.set_verifier(verifier)

        segments = [_segment(0)]
        result = await service.summarize(meeting_id, segments)

        assert verifier.filter_call_count == 1
        assert result.filtered_summary is not None
        assert result.has_invalid_citations is True

    @pytest.mark.asyncio
    async def test_summarize_passes_max_limits(self, meeting_id: MeetingId) -> None:
        """Max limits should be passed to provider."""
        captured_request: SummarizationRequest | None = None

        class CapturingProvider(MockProvider):
            async def summarize(self, request: SummarizationRequest) -> SummarizationResult:
                nonlocal captured_request
                captured_request = request
                return await super().summarize(request)

        provider = CapturingProvider()
        service = SummarizationService()
        service.register_provider(SummarizationMode.LOCAL, provider)

        segments = [_segment(0)]
        await service.summarize(meeting_id, segments, max_key_points=3, max_action_items=5)

        assert captured_request is not None
        assert captured_request.max_key_points == 3
        assert captured_request.max_action_items == 5

    @pytest.mark.asyncio
    async def test_summarize_passes_style_prompt_to_provider(self, meeting_id: MeetingId) -> None:
        """Style prompt should be passed through to provider request."""
        captured_request: SummarizationRequest | None = None

        class CapturingProvider(MockProvider):
            async def summarize(self, request: SummarizationRequest) -> SummarizationResult:
                nonlocal captured_request
                captured_request = request
                return await super().summarize(request)

        provider = CapturingProvider()
        service = SummarizationService()
        service.register_provider(SummarizationMode.LOCAL, provider)

        style_instruction = "Use formal, business-appropriate language. Present information in bullet points."
        segments = [_segment(0)]
        await service.summarize(meeting_id, segments, style_prompt=style_instruction)

        assert captured_request is not None
        assert captured_request.style_prompt == style_instruction

    @pytest.mark.asyncio
    async def test_summarize_without_style_prompt_passes_none(self, meeting_id: MeetingId) -> None:
        """No style prompt should result in None being passed to provider."""
        captured_request: SummarizationRequest | None = None

        class CapturingProvider(MockProvider):
            async def summarize(self, request: SummarizationRequest) -> SummarizationResult:
                nonlocal captured_request
                captured_request = request
                return await super().summarize(request)

        provider = CapturingProvider()
        service = SummarizationService()
        service.register_provider(SummarizationMode.LOCAL, provider)

        segments = [_segment(0)]
        await service.summarize(meeting_id, segments)

        assert captured_request is not None
        assert captured_request.style_prompt is None

    @pytest.mark.asyncio
    async def test_summarize_requires_cloud_consent(self, meeting_id: MeetingId) -> None:
        """Cloud mode should require consent."""
        cloud = MockProvider(name="cloud", requires_consent=True)
        fallback = MockProvider(name="local")
        service = SummarizationService(
            settings=SummarizationServiceSettings(
                cloud_consent_granted=False, fallback_to_local=True
            )
        )
        service.register_provider(SummarizationMode.CLOUD, cloud)
        service.register_provider(SummarizationMode.LOCAL, fallback)

        segments = [_segment(0)]
        result = await service.summarize(meeting_id, segments, mode=SummarizationMode.CLOUD)

        assert result.provider_used == "local"
        assert result.fallback_used is True
        assert cloud.call_count == 0

    @pytest.mark.asyncio
    async def test_summarize_calls_persist_callback(self, meeting_id: MeetingId) -> None:
        """Persist callback should be called with final summary."""
        persisted: list[Summary] = []

        async def mock_persist(summary: Summary) -> None:
            persisted.append(summary)

        provider = MockProvider()
        service = SummarizationService(on_persist=mock_persist)
        service.register_provider(SummarizationMode.LOCAL, provider)

        segments = [_segment(0)]
        await service.summarize(meeting_id, segments)

        assert len(persisted) == 1
        assert persisted[0].meeting_id == meeting_id

    @pytest.mark.asyncio
    async def test_summarize_persist_callback_receives_filtered_summary(
        self, meeting_id: MeetingId
    ) -> None:
        """Persist callback should receive filtered summary when available."""
        persisted: list[Summary] = []

        async def mock_persist(summary: Summary) -> None:
            persisted.append(summary)

        provider = MockProvider()
        verifier = MockVerifier(is_valid=False)
        service = SummarizationService(
            settings=SummarizationServiceSettings(
                verify_citations=True,
                filter_invalid_citations=True,
            ),
            on_persist=mock_persist,
        )
        service.register_provider(SummarizationMode.LOCAL, provider)
        service.set_verifier(verifier)

        segments = [_segment(0)]
        result = await service.summarize(meeting_id, segments)

        assert len(persisted) == 1
        # Should persist the filtered summary, not original
        assert persisted[0] is result.filtered_summary


class TestSummarizationServiceResult:
    """Tests for SummarizationServiceResult."""

    def test_summary_returns_filtered_when_available(self) -> None:
        """summary property should return filtered_summary if available."""
        from noteflow.application.services import SummarizationServiceResult

        original = Summary(
            meeting_id=MeetingId(uuid4()),
            executive_summary="Original",
            key_points=[KeyPoint(text="Point", segment_ids=[99])],
        )
        filtered = Summary(
            meeting_id=original.meeting_id,
            executive_summary="Original",
            key_points=[KeyPoint(text="Point", segment_ids=[])],
        )
        result = SummarizationServiceResult(
            result=SummarizationResult(
                summary=original,
                model_name="test",
                provider_name="test",
            ),
            filtered_summary=filtered,
        )

        assert result.summary is filtered

    def test_summary_returns_original_when_no_filter(self) -> None:
        """summary property should return original when no filter applied."""
        from noteflow.application.services import SummarizationServiceResult

        original = Summary(
            meeting_id=MeetingId(uuid4()),
            executive_summary="Original",
            key_points=[],
        )
        result = SummarizationServiceResult(
            result=SummarizationResult(
                summary=original,
                model_name="test",
                provider_name="test",
            ),
        )

        assert result.summary is original

    def test_has_invalid_citations_true(self) -> None:
        """has_invalid_citations should be True when verification fails."""
        from noteflow.application.services import SummarizationServiceResult

        result = SummarizationServiceResult(
            result=SummarizationResult(
                summary=Summary(
                    meeting_id=MeetingId(uuid4()),
                    executive_summary="Test",
                    key_points=[],
                ),
                model_name="test",
                provider_name="test",
            ),
            verification=CitationVerificationResult(is_valid=False, invalid_key_point_indices=(0,)),
        )

        assert result.has_invalid_citations is True

    def test_has_invalid_citations_false_when_valid(self) -> None:
        """has_invalid_citations should be False when verification passes."""
        from noteflow.application.services import SummarizationServiceResult

        result = SummarizationServiceResult(
            result=SummarizationResult(
                summary=Summary(
                    meeting_id=MeetingId(uuid4()),
                    executive_summary="Test",
                    key_points=[],
                ),
                model_name="test",
                provider_name="test",
            ),
            verification=CitationVerificationResult(is_valid=True),
        )

        assert result.has_invalid_citations is False


class TestSummarizationServiceAdditionalBranches:
    """Additional branch and utility coverage."""

    def test_is_mode_available_false_when_not_registered(self) -> None:
        """is_mode_available should respect registered providers."""
        service = SummarizationService()
        assert service.is_mode_available(SummarizationMode.LOCAL) is False

    @pytest.mark.asyncio
    async def test_cloud_without_consent_and_no_fallback_raises(
        self, meeting_id: MeetingId
    ) -> None:
        """Requesting cloud without consent should raise when fallback disabled."""
        provider = MockProvider(name="cloud", available=True)
        service = SummarizationService(
            providers={SummarizationMode.CLOUD: provider},
            settings=SummarizationServiceSettings(
                default_mode=SummarizationMode.CLOUD,
                cloud_consent_granted=False,
                fallback_to_local=False,
            ),
        )

        with pytest.raises(ProviderUnavailableError, match=r".*"):
            await service.summarize(meeting_id, [_segment(0)], mode=SummarizationMode.CLOUD)

    @pytest.mark.asyncio
    async def test_no_fallback_provider_available_raises(self, meeting_id: MeetingId) -> None:
        """When no fallback provider exists, provider selection should fail."""
        unavailable = MockProvider(name="cloud", available=False)
        service = SummarizationService(
            providers={SummarizationMode.CLOUD: unavailable},
            settings=SummarizationServiceSettings(fallback_to_local=True),
        )

        with pytest.raises(ProviderUnavailableError, match=r".*"):
            await service.summarize(meeting_id, [_segment(0)], mode=SummarizationMode.CLOUD)

    def test_filter_citations_returns_summary_when_no_verifier(self) -> None:
        """filter_citations should return original summary when verifier is absent."""
        summary = Summary(
            meeting_id=MeetingId(uuid4()),
            executive_summary="Exec",
            generated_at=datetime.now(UTC),
        )
        service = SummarizationService()

        result = service.filter_citations(summary, [])

        assert result is summary

    def test_set_default_mode_updates_settings(self) -> None:
        """set_default_mode should update default mode."""
        service = SummarizationService()
        service.set_default_mode(SummarizationMode.MOCK)
        assert service.settings.default_mode == SummarizationMode.MOCK

    def test_set_persist_callback_updates_callback(self) -> None:
        """set_persist_callback should update on_persist field."""

        async def callback(summary: Summary) -> None:
            pass

        service = SummarizationService()
        assert service.on_persist is None

        service.set_persist_callback(callback)
        assert service.on_persist is callback

        service.set_persist_callback(None)
        assert service.on_persist is None
