"""Tests for MeetingService application service."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import pytest

from noteflow.application.services.meeting_service import MeetingService, SegmentData
from noteflow.domain.entities import Annotation, Meeting, Segment, Summary
from noteflow.domain.value_objects import AnnotationId, AnnotationType, MeetingId, MeetingState

if TYPE_CHECKING:
    from collections.abc import Sequence


class TestMeetingServiceCreation:
    """Tests for meeting creation operations."""

    async def test_create_meeting_success(self, mock_uow: MagicMock) -> None:
        """Test successful meeting creation."""
        created_meeting = Meeting.create(title="Test Meeting")
        mock_uow.meetings.create = AsyncMock(return_value=created_meeting)

        service = MeetingService(mock_uow)
        result = await service.create_meeting(title="Test Meeting")

        assert result.title == "Test Meeting"
        mock_uow.meetings.create.assert_called_once()
        mock_uow.commit.assert_called_once()

    async def test_create_meeting_with_metadata(self, mock_uow: MagicMock) -> None:
        """Test meeting creation with metadata."""
        metadata = {"project": "NoteFlow"}
        created_meeting = Meeting.create(title="Test", metadata=metadata)
        mock_uow.meetings.create = AsyncMock(return_value=created_meeting)

        service = MeetingService(mock_uow)
        result = await service.create_meeting(title="Test", metadata=metadata)

        assert result.metadata == metadata


class TestMeetingServiceRetrieval:
    """Tests for meeting retrieval operations."""

    async def test_get_meeting_found(self, mock_uow: MagicMock) -> None:
        """Test retrieving existing meeting."""
        meeting_id = MeetingId(uuid4())
        expected_meeting = Meeting.create(title="Found")
        mock_uow.meetings.get = AsyncMock(return_value=expected_meeting)

        service = MeetingService(mock_uow)
        result = await service.get_meeting(meeting_id)

        assert result is not None
        assert result.title == "Found"

    async def test_get_meeting_not_found(self, mock_uow: MagicMock) -> None:
        """Test retrieving non-existent meeting."""
        meeting_id = MeetingId(uuid4())
        mock_uow.meetings.get = AsyncMock(return_value=None)

        service = MeetingService(mock_uow)
        result = await service.get_meeting(meeting_id)

        assert result is None

    async def test_list_meetings(self, mock_uow: MagicMock) -> None:
        """Test listing meetings with pagination."""
        meetings: Sequence[Meeting] = [
            Meeting.create(title="Meeting 1"),
            Meeting.create(title="Meeting 2"),
        ]
        mock_uow.meetings.list_all = AsyncMock(return_value=(meetings, 10))

        service = MeetingService(mock_uow)
        result, total = await service.list_meetings(limit=2, offset=0)

        assert len(result) == 2
        assert total == 10
        mock_uow.meetings.list_all.assert_called_once_with(
            states=None, limit=2, offset=0, sort_desc=True
        )


class TestMeetingServiceStateTransitions:
    """Tests for meeting state transition operations."""

    @pytest.mark.parametrize(
        ("initial_state", "action_method", "expected_state"),
        [
            pytest.param(MeetingState.CREATED, "start_recording", MeetingState.RECORDING, id="created-to-recording"),
            pytest.param(MeetingState.RECORDING, "stop_meeting", MeetingState.STOPPED, id="recording-to-stopped"),
            pytest.param(MeetingState.STOPPED, "complete_meeting", MeetingState.COMPLETED, id="stopped-to-completed"),
        ],
    )
    async def test_valid_state_transitions(
        self,
        initial_state: MeetingState,
        action_method: str,
        expected_state: MeetingState,
        mock_uow: MagicMock,
    ) -> None:
        """Test valid state transitions succeed and commit."""
        meeting = Meeting.create(title="Test")
        meeting.state = initial_state

        mock_uow.meetings.get = AsyncMock(return_value=meeting)
        mock_uow.meetings.update = AsyncMock(return_value=meeting)

        service = MeetingService(mock_uow)
        result = await getattr(service, action_method)(meeting.id)

        assert result is not None, f"Expected result for {action_method}"
        assert result.state == expected_state, f"Expected state {expected_state} after {action_method}"
        mock_uow.commit.assert_called_once()

    @pytest.mark.parametrize(
        ("initial_state", "action_method"),
        [
            pytest.param(MeetingState.CREATED, "stop_meeting", id="cannot-stop-created"),
            pytest.param(MeetingState.COMPLETED, "start_recording", id="cannot-start-completed"),
        ],
    )
    async def test_invalid_state_transitions_raise(
        self,
        initial_state: MeetingState,
        action_method: str,
        mock_uow: MagicMock,
    ) -> None:
        """Test invalid state transitions raise ValueError and do not commit."""
        meeting = Meeting.create(title="Test")
        meeting.state = initial_state

        mock_uow.meetings.get = AsyncMock(return_value=meeting)
        mock_uow.meetings.update = AsyncMock(return_value=meeting)

        service = MeetingService(mock_uow)

        with pytest.raises(ValueError, match="Cannot"):
            await getattr(service, action_method)(meeting.id)
        mock_uow.commit.assert_not_called()

    async def test_start_recording_not_found(self, mock_uow: MagicMock) -> None:
        """Test starting recording on non-existent meeting."""
        meeting_id = MeetingId(uuid4())
        mock_uow.meetings.get = AsyncMock(return_value=None)

        service = MeetingService(mock_uow)
        result = await service.start_recording(meeting_id)

        assert result is None
        mock_uow.commit.assert_not_called()


class TestMeetingServiceDeletion:
    """Tests for meeting deletion operations."""

    async def test_delete_meeting_success(self, mock_uow: MagicMock) -> None:
        """Test successful meeting deletion."""
        meeting_id = MeetingId(uuid4())
        mock_meeting = Meeting.create(title="Test Meeting")
        mock_uow.meetings.get = AsyncMock(return_value=mock_meeting)
        mock_uow.meetings.delete = AsyncMock(return_value=True)
        mock_uow.assets.delete_meeting_assets = AsyncMock()

        service = MeetingService(mock_uow)
        result = await service.delete_meeting(meeting_id)

        assert result is True
        mock_uow.assets.delete_meeting_assets.assert_called_once_with(
            meeting_id, mock_meeting.asset_path
        )
        mock_uow.meetings.delete.assert_called_once_with(meeting_id)
        mock_uow.commit.assert_called_once()

    async def test_delete_meeting_not_found(self, mock_uow: MagicMock) -> None:
        """Test deleting non-existent meeting returns False."""
        meeting_id = MeetingId(uuid4())
        mock_uow.meetings.get = AsyncMock(return_value=None)

        service = MeetingService(mock_uow)
        result = await service.delete_meeting(meeting_id)

        assert result is False
        mock_uow.assets.delete_meeting_assets.assert_not_called()
        mock_uow.meetings.delete.assert_not_called()
        mock_uow.commit.assert_not_called()


class TestMeetingServiceSegments:
    """Tests for segment operations."""

    async def test_add_segment_success(self, mock_uow: MagicMock) -> None:
        """Test adding a segment to meeting."""
        meeting_id = MeetingId(uuid4())
        segment = Segment(
            segment_id=0, text="Hello", start_time=0.0, end_time=1.0, meeting_id=meeting_id
        )
        mock_uow.segments.add = AsyncMock(return_value=segment)

        service = MeetingService(mock_uow)
        segment_data = SegmentData(
            segment_id=0,
            text="Hello",
            start_time=0.0,
            end_time=1.0,
        )
        result = await service.add_segment(meeting_id=meeting_id, data=segment_data)

        assert result.text == "Hello"
        mock_uow.segments.add.assert_called_once()
        mock_uow.commit.assert_called_once()

    async def test_get_segments(self, mock_uow: MagicMock) -> None:
        """Test retrieving segments for meeting."""
        meeting_id = MeetingId(uuid4())
        segments: Sequence[Segment] = [
            Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0),
            Segment(segment_id=1, text="Second", start_time=1.0, end_time=2.0),
        ]
        mock_uow.segments.get_by_meeting = AsyncMock(return_value=segments)

        service = MeetingService(mock_uow)
        result = await service.get_segments(meeting_id)

        assert len(result) == 2
        mock_uow.segments.get_by_meeting.assert_called_once_with(meeting_id, include_words=True)

    async def test_add_segments_batch(self, mock_uow: MagicMock) -> None:
        """Test batch adding segments commits once."""
        meeting_id = MeetingId(uuid4())
        segments = [
            Segment(segment_id=0, text="A", start_time=0.0, end_time=1.0),
            Segment(segment_id=1, text="B", start_time=1.0, end_time=2.0),
        ]
        mock_uow.segments.add_batch = AsyncMock(return_value=segments)

        service = MeetingService(mock_uow)
        result = await service.add_segments_batch(meeting_id=meeting_id, segments=segments)

        assert len(result) == 2
        mock_uow.segments.add_batch.assert_called_once_with(meeting_id, segments)
        mock_uow.commit.assert_called_once()


class TestMeetingServiceSummaries:
    """Tests for summary operations."""

    async def test_save_summary_success(self, mock_uow: MagicMock) -> None:
        """Test saving a meeting summary."""
        meeting_id = MeetingId(uuid4())
        summary = Summary(
            meeting_id=meeting_id,
            executive_summary="Test summary",
            generated_at=datetime.now(UTC),
            provider_name="test",
            model_name="v1",
        )
        mock_uow.summaries.save = AsyncMock(return_value=summary)

        service = MeetingService(mock_uow)
        result = await service.save_summary(
            meeting_id=meeting_id,
            executive_summary="Test summary",
            provider_name="test",
            model_name="v1",
        )

        assert result.executive_summary == "Test summary"
        mock_uow.summaries.save.assert_called_once()
        mock_uow.commit.assert_called_once()

    async def test_get_summary_found(self, mock_uow: MagicMock) -> None:
        """Test retrieving existing summary."""
        meeting_id = MeetingId(uuid4())
        summary = Summary(meeting_id=meeting_id, executive_summary="Found")
        mock_uow.summaries.get_by_meeting = AsyncMock(return_value=summary)

        service = MeetingService(mock_uow)
        result = await service.get_summary(meeting_id)

        assert result is not None
        assert result.executive_summary == "Found"

    async def test_get_summary_not_found(self, mock_uow: MagicMock) -> None:
        """Test retrieving non-existent summary."""
        meeting_id = MeetingId(uuid4())
        mock_uow.summaries.get_by_meeting = AsyncMock(return_value=None)

        service = MeetingService(mock_uow)
        result = await service.get_summary(meeting_id)

        assert result is None


class TestMeetingServiceSearch:
    """Tests for semantic search operations."""

    async def test_search_segments_delegates(self, mock_uow: MagicMock) -> None:
        """Test search_segments delegates to repository."""
        meeting_id = MeetingId(uuid4())
        segment = Segment(segment_id=0, text="A", start_time=0.0, end_time=1.0)
        mock_uow.segments.search_semantic = AsyncMock(return_value=[(segment, 0.9)])

        service = MeetingService(mock_uow)
        result = await service.search_segments(query_embedding=[0.1], meeting_id=meeting_id)

        assert len(result) == 1
        mock_uow.segments.search_semantic.assert_called_once_with(
            query_embedding=[0.1], limit=10, meeting_id=meeting_id
        )


class TestMeetingServiceAnnotations:
    """Tests for annotation operations."""

    async def test_add_annotation_success(self, mock_uow: MagicMock) -> None:
        """Test adding an annotation commits and returns saved entity."""
        meeting_id = MeetingId(uuid4())
        mock_uow.annotations.add = AsyncMock()

        service = MeetingService(mock_uow)
        await service.add_annotation(
            meeting_id=meeting_id,
            annotation_type=AnnotationType.NOTE,
            text="Note",
            start_time=0.0,
            end_time=1.0,
        )

        mock_uow.annotations.add.assert_called_once()
        mock_uow.commit.assert_called_once()

    async def test_get_annotations_in_range(self, mock_uow: MagicMock) -> None:
        """Test get_annotations_in_range delegates to repository."""
        meeting_id = MeetingId(uuid4())
        mock_uow.annotations.get_by_time_range = AsyncMock(return_value=[])

        service = MeetingService(mock_uow)
        await service.get_annotations_in_range(meeting_id, start_time=1.0, end_time=2.0)

        mock_uow.annotations.get_by_time_range.assert_called_once_with(meeting_id, 1.0, 2.0)

    async def test_update_annotation_not_found_raises(self, mock_uow: MagicMock) -> None:
        """Test update_annotation propagates repository errors."""
        meeting_id = MeetingId(uuid4())
        annotation = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=meeting_id,
            annotation_type=AnnotationType.NOTE,
            text="Note",
            start_time=0.0,
            end_time=1.0,
        )
        mock_uow.annotations.update = AsyncMock(side_effect=ValueError("Annotation not found"))

        service = MeetingService(mock_uow)
        with pytest.raises(ValueError, match="Annotation not found"):
            await service.update_annotation(annotation)

        mock_uow.commit.assert_not_called()

    async def test_delete_annotation_not_found(self, mock_uow: MagicMock) -> None:
        """Test delete_annotation returns False when missing."""
        annotation_id = AnnotationId(uuid4())
        mock_uow.annotations.delete = AsyncMock(return_value=False)

        service = MeetingService(mock_uow)
        result = await service.delete_annotation(annotation_id)

        assert result is False
        mock_uow.commit.assert_not_called()


class TestMeetingServiceAdditionalBranches:
    """Additional branch coverage for MeetingService."""

    async def test_stop_meeting_not_found(self, mock_uow: MagicMock) -> None:
        """stop_meeting should return None when meeting is missing."""
        mock_uow.meetings.get = AsyncMock(return_value=None)
        service = MeetingService(mock_uow)

        result = await service.stop_meeting(MeetingId(uuid4()))

        assert result is None
        mock_uow.commit.assert_not_called()

    async def test_complete_meeting_not_found(self, mock_uow: MagicMock) -> None:
        """complete_meeting should return None when meeting is missing."""
        mock_uow.meetings.get = AsyncMock(return_value=None)
        service = MeetingService(mock_uow)

        result = await service.complete_meeting(MeetingId(uuid4()))

        assert result is None
        mock_uow.commit.assert_not_called()

    async def test_get_annotation_delegates_repository(self, mock_uow: MagicMock) -> None:
        """get_annotation should delegate to repository."""
        annotation = Annotation(
            id=AnnotationId(uuid4()),
            meeting_id=MeetingId(uuid4()),
            annotation_type=AnnotationType.NOTE,
            text="note",
            start_time=0.0,
            end_time=1.0,
        )
        mock_uow.annotations.get = AsyncMock(return_value=annotation)
        service = MeetingService(mock_uow)

        result = await service.get_annotation(annotation.id)

        assert result is annotation
        mock_uow.annotations.get.assert_called_once_with(annotation.id)

    async def test_get_annotations_delegates_repository(self, mock_uow: MagicMock) -> None:
        """get_annotations should delegate to repository."""
        meeting_id = MeetingId(uuid4())
        mock_uow.annotations.get_by_meeting = AsyncMock(return_value=[])
        service = MeetingService(mock_uow)

        await service.get_annotations(meeting_id)

        mock_uow.annotations.get_by_meeting.assert_called_once_with(meeting_id)

    async def test_delete_annotation_success_commits(self, mock_uow: MagicMock) -> None:
        """delete_annotation should commit on success."""
        annotation_id = AnnotationId(uuid4())
        mock_uow.annotations.delete = AsyncMock(return_value=True)
        service = MeetingService(mock_uow)

        result = await service.delete_annotation(annotation_id)

        assert result is True
        mock_uow.commit.assert_called_once()
