"""Tests for Meeting entity."""

from __future__ import annotations

from datetime import datetime, timedelta
from uuid import UUID

import pytest

from noteflow.domain.entities.meeting import Meeting, MeetingLoadParams
from noteflow.domain.entities.segment import Segment
from noteflow.domain.entities.summary import Summary
from noteflow.domain.utils.time import utc_now
from noteflow.domain.value_objects import MeetingState

# Test constants
HALF_HOUR_SECONDS = 1800.0


class TestMeetingCreation:
    """Tests for Meeting creation methods."""

    @pytest.mark.parametrize(
        "attr,expected",
        [
            ("state", MeetingState.CREATED),
            ("started_at", None),
            ("ended_at", None),
            ("segments", []),
            ("summary", None),
        ],
    )
    def test_create_default_attributes(self, attr: str, expected: object) -> None:
        """Test factory method sets default attribute values."""
        meeting = Meeting.create()
        actual = getattr(meeting, attr)
        assert actual == expected, f"expected {attr}={expected!r}, got {actual!r}"

    def test_create_generates_default_title(self) -> None:
        """Test factory method generates default title prefix."""
        meeting = Meeting.create()
        assert meeting.title.startswith("Meeting "), f"expected title starting with 'Meeting ', got {meeting.title!r}"

    def test_create_with_custom_title(self) -> None:
        """Test factory method accepts custom title."""
        meeting = Meeting.create(title="Team Standup")
        assert meeting.title == "Team Standup", f"expected title 'Team Standup', got {meeting.title!r}"

    def test_create_with_metadata(self) -> None:
        """Test factory method accepts metadata."""
        metadata = {"project": "NoteFlow", "team": "Engineering"}
        meeting = Meeting.create(title="Sprint Planning", metadata=metadata)
        assert meeting.metadata == metadata, f"expected metadata {metadata!r}, got {meeting.metadata!r}"

    def test_from_uuid_str(self) -> None:
        """Test creation from existing UUID string."""
        uuid_str = "12345678-1234-5678-1234-567812345678"
        params = MeetingLoadParams(state=MeetingState.STOPPED)
        meeting = Meeting.from_uuid_str(
            uuid_str=uuid_str,
            title="Restored Meeting",
            params=params,
        )
        assert meeting.id == UUID(uuid_str), "id should match provided UUID"
        assert meeting.title == "Restored Meeting", "title should match provided value"
        assert meeting.state == MeetingState.STOPPED, "state should match provided value"


class TestMeetingStateTransitions:
    """Tests for Meeting state machine transitions."""

    def test_start_recording_from_created(self) -> None:
        """Test starting recording from CREATED state."""
        meeting = Meeting.create()
        meeting.start_recording()
        assert meeting.state == MeetingState.RECORDING, "state should transition to RECORDING"
        assert meeting.started_at is not None, "started_at should be set on start"

    def test_start_recording_invalid_state_raises(self) -> None:
        """Test starting recording from invalid state raises."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        with pytest.raises(ValueError, match="Cannot start recording"):
            meeting.start_recording()

    def test_begin_stopping_from_recording(self) -> None:
        """Test transitioning to STOPPING from RECORDING state."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        assert meeting.state == MeetingState.STOPPING, f"expected state STOPPING, got {meeting.state}"

    def test_begin_stopping_invalid_state_raises(self) -> None:
        """Test begin_stopping from invalid state raises."""
        meeting = Meeting.create()
        with pytest.raises(ValueError, match="Cannot begin stopping"):
            meeting.begin_stopping()

    def test_stop_recording_from_stopping(self) -> None:
        """Test stopping recording from STOPPING state."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        assert meeting.state == MeetingState.STOPPED, "state should transition to STOPPED"
        assert meeting.ended_at is not None, "ended_at should be set on stop"

    def test_stop_recording_from_recording_raises(self) -> None:
        """Test stopping recording directly from RECORDING raises.

        Must go through STOPPING state for graceful shutdown.
        """
        meeting = Meeting.create()
        meeting.start_recording()
        with pytest.raises(ValueError, match="Cannot stop recording"):
            meeting.stop_recording()

    def test_stop_recording_from_created_raises(self) -> None:
        """Test stopping recording from CREATED state raises."""
        meeting = Meeting.create()
        with pytest.raises(ValueError, match="Cannot stop recording"):
            meeting.stop_recording()

    def test_complete_from_stopped(self) -> None:
        """Test completing meeting from STOPPED state."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        meeting.complete()
        assert meeting.state == MeetingState.COMPLETED, f"expected state COMPLETED, got {meeting.state}"

    def test_complete_invalid_state_raises(self) -> None:
        """Test completing from invalid state raises."""
        meeting = Meeting.create()
        with pytest.raises(ValueError, match="Cannot complete"):
            meeting.complete()

    def test_mark_error(self) -> None:
        """Test marking meeting as error state."""
        meeting = Meeting.create()
        meeting.mark_error()
        assert meeting.state == MeetingState.ERROR, f"expected state ERROR, got {meeting.state}"

    def test_stopping_to_recording_invalid(self) -> None:
        """Test cannot transition from STOPPING back to RECORDING."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        with pytest.raises(ValueError, match="Cannot start recording"):
            meeting.start_recording()


class TestMeetingSegments:
    """Tests for Meeting segment management."""

    def test_add_segment(self) -> None:
        """Test adding a segment to meeting."""
        meeting = Meeting.create()
        segment = Segment(segment_id=0, text="Hello world", start_time=0.0, end_time=1.0)
        meeting.add_segment(segment)
        assert meeting.segment_count == 1, "segment_count should increment"
        assert meeting.segments[0] == segment, "segment should be retrievable"

    def testnext_segment_id_empty(self) -> None:
        """Test next segment ID when no segments exist."""
        meeting = Meeting.create()
        assert meeting.next_segment_id == 0, f"expected next_segment_id 0 for empty meeting, got {meeting.next_segment_id}"

    def testnext_segment_id_with_segments(self) -> None:
        """Test next segment ID increments correctly."""
        meeting = Meeting.create()
        meeting.add_segment(Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0))
        meeting.add_segment(Segment(segment_id=1, text="Second", start_time=1.0, end_time=2.0))
        assert meeting.next_segment_id == 2, f"expected next_segment_id 2, got {meeting.next_segment_id}"

    def testnext_segment_id_non_contiguous(self) -> None:
        """Test next segment ID uses max + 1 for non-contiguous IDs."""
        meeting = Meeting.create()
        meeting.add_segment(Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0))
        meeting.add_segment(Segment(segment_id=5, text="Sixth", start_time=1.0, end_time=2.0))
        assert meeting.next_segment_id == 6, f"expected next_segment_id 6 for max id 5, got {meeting.next_segment_id}"

    def test_full_transcript(self) -> None:
        """Test concatenating all segment text."""
        meeting = Meeting.create()
        meeting.add_segment(Segment(segment_id=0, text="Hello", start_time=0.0, end_time=1.0))
        meeting.add_segment(Segment(segment_id=1, text="world", start_time=1.0, end_time=2.0))
        assert meeting.full_transcript == "Hello world", f"expected 'Hello world', got {meeting.full_transcript!r}"

    def test_full_transcript_empty(self) -> None:
        """Test full_transcript is empty when there are no segments."""
        meeting = Meeting.create()
        assert meeting.full_transcript == "", f"expected empty transcript, got {meeting.full_transcript!r}"


class TestMeetingProperties:
    """Tests for Meeting computed properties."""

    def test_duration_seconds_not_started(self) -> None:
        """Test duration is 0 when not started."""
        meeting = Meeting.create()
        assert meeting.duration_seconds == 0.0, f"expected 0.0 duration for unstarted meeting, got {meeting.duration_seconds}"

    def test_duration_seconds_with_times(self) -> None:
        """Test duration calculation with start and end times."""
        meeting = Meeting.create()
        meeting.started_at = datetime(2024, 1, 1, 10, 0, 0)
        meeting.ended_at = datetime(2024, 1, 1, 10, 30, 0)
        assert meeting.duration_seconds == HALF_HOUR_SECONDS, f"expected {HALF_HOUR_SECONDS} seconds (30 min), got {meeting.duration_seconds}"

    def test_duration_seconds_in_progress(self) -> None:
        """Test duration is > 0 when started but not ended."""
        meeting = Meeting.create()
        meeting.started_at = utc_now() - timedelta(seconds=5)
        assert meeting.duration_seconds >= 5.0, f"expected duration >= 5.0 seconds, got {meeting.duration_seconds}"

    def test_is_in_active_state_created(self) -> None:
        """Test is_in_active_state returns True for CREATED state."""
        meeting = Meeting.create()
        assert meeting.is_in_active_state() is True, "CREATED state should be considered active"

    def test_is_in_active_state_recording(self) -> None:
        """Test is_in_active_state returns True for RECORDING state."""
        meeting = Meeting.create()
        meeting.start_recording()
        assert meeting.is_in_active_state() is True, "RECORDING state should be considered active"

    def test_is_in_active_state_stopping(self) -> None:
        """Test is_in_active_state returns False for STOPPING state."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        assert meeting.is_in_active_state() is False, "STOPPING state should not be considered active"

    def test_is_in_active_state_stopped(self) -> None:
        """Test is_in_active_state returns False for STOPPED state."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        assert meeting.is_in_active_state() is False, "STOPPED state should not be considered active"

    def test_has_summary_false(self) -> None:
        """Test has_summary returns False when no summary."""
        meeting = Meeting.create()
        assert meeting.has_summary() is False, "meeting without summary should return has_summary=False"

    def test_has_summary_true(self) -> None:
        """Test has_summary returns True when summary set."""
        meeting = Meeting.create()
        summary = Summary(meeting_id=meeting.id)
        meeting.set_summary(summary)
        assert meeting.has_summary() is True, "meeting with summary should return has_summary=True"


class TestMeetingEdgeCases:
    """Edge case tests for Meeting entity."""

    def test_full_transcript_segments_with_empty_text(self) -> None:
        """Test full_transcript handles segments with empty text."""
        meeting = Meeting.create()
        meeting.add_segment(Segment(segment_id=0, text="Hello", start_time=0.0, end_time=1.0))
        meeting.add_segment(Segment(segment_id=1, text="", start_time=1.0, end_time=1.5))
        meeting.add_segment(Segment(segment_id=2, text="world", start_time=1.5, end_time=2.0))
        assert meeting.full_transcript == "Hello  world", f"expected 'Hello  world', got {meeting.full_transcript!r}"

    def test_full_transcript_segments_whitespace_only(self) -> None:
        """Test full_transcript handles segments with whitespace-only text."""
        meeting = Meeting.create()
        meeting.add_segment(Segment(segment_id=0, text="Hello", start_time=0.0, end_time=1.0))
        meeting.add_segment(Segment(segment_id=1, text="   ", start_time=1.0, end_time=1.5))
        meeting.add_segment(Segment(segment_id=2, text="world", start_time=1.5, end_time=2.0))
        transcript = meeting.full_transcript
        assert "Hello" in transcript, "transcript should contain first segment"
        assert "world" in transcript, "transcript should contain last segment"

    def test_full_transcript_unicode_and_emoji(self) -> None:
        """Test full_transcript handles unicode and emoji."""
        meeting = Meeting.create()
        meeting.add_segment(Segment(segment_id=0, text="你好", start_time=0.0, end_time=1.0))
        meeting.add_segment(Segment(segment_id=1, text="🚀", start_time=1.0, end_time=2.0))
        meeting.add_segment(Segment(segment_id=2, text="café", start_time=2.0, end_time=3.0))
        expected = "你好 🚀 café"
        assert meeting.full_transcript == expected, f"expected {expected!r}, got {meeting.full_transcript!r}"

    def test_start_recording_sets_recent_timestamp(self) -> None:
        """Test started_at is set to a recent timestamp."""
        before = utc_now()
        meeting = Meeting.create()
        meeting.start_recording()
        after = utc_now()

        assert meeting.started_at is not None, "started_at should be set"
        assert before <= meeting.started_at <= after, "started_at should be within test window"

    def test_stop_recording_sets_recent_timestamp(self) -> None:
        """Test ended_at is set to a recent timestamp."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()

        before = utc_now()
        meeting.stop_recording()
        after = utc_now()

        assert meeting.ended_at is not None, "ended_at should be set"
        assert before <= meeting.ended_at <= after, "ended_at should be within test window"

    def test_state_transition_does_not_modify_segments(self) -> None:
        """Test state transitions don't accidentally modify segments."""
        meeting = Meeting.create()
        meeting.add_segment(Segment(segment_id=0, text="Pre-recording", start_time=0.0, end_time=1.0))

        meeting.start_recording()
        assert meeting.segment_count == 1, "segments preserved after start_recording"
        assert meeting.segments[0].text == "Pre-recording", "segment text unchanged"

        meeting.begin_stopping()
        assert meeting.segment_count == 1, "segments preserved after begin_stopping"

        meeting.stop_recording()
        assert meeting.segment_count == 1, "segments preserved after stop_recording"

    def test_many_segments_accumulation(self, meeting_with_100_segments: Meeting) -> None:
        """Test meeting can accumulate many segments."""
        expected_segment_count = 100
        assert meeting_with_100_segments.segment_count == expected_segment_count, "all segments should be stored"
        assert meeting_with_100_segments.next_segment_id == expected_segment_count, "next_id should equal count"

    def test_metadata_accepts_string_values(self) -> None:
        """Test meeting metadata accepts string values."""
        complex_metadata = {
            "participants": "Alice,Bob,Charlie",
            "settings": '{"auto_transcribe": true, "language": "en"}',
            "tags": "[]",
        }
        meeting = Meeting.create(title="Complex Metadata", metadata=complex_metadata)
        assert meeting.metadata["participants"] == "Alice,Bob,Charlie", "metadata preserved"
        assert meeting.metadata["settings"] == '{"auto_transcribe": true, "language": "en"}'

    def test_create_with_empty_title_generates_default(self) -> None:
        """Test empty title triggers default title generation."""
        meeting = Meeting.create(title="")
        # Factory generates default title when empty string passed
        assert meeting.title.startswith("Meeting "), f"expected title starting with 'Meeting ', got {meeting.title!r}"

    def test_create_with_very_long_title(self) -> None:
        """Test meeting accepts very long titles."""
        long_title = "A" * 1000
        meeting = Meeting.create(title=long_title)
        assert len(meeting.title) == 1000, f"expected title length 1000, got {len(meeting.title)}"

    def test_immediate_stop_after_start_zero_duration(self) -> None:
        """Test immediate stop after start yields near-zero duration (ED-01).

        This edge case ensures the system handles recordings with
        essentially zero audio content gracefully.
        """
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()

        # Duration should be very small (sub-second)
        assert meeting.duration_seconds < 1.0, "immediate stop should yield near-zero duration"
        assert meeting.state == MeetingState.STOPPED, "state should be STOPPED"
        assert meeting.started_at is not None, "started_at should be set"
        assert meeting.ended_at is not None, "ended_at should be set"
        assert meeting.ended_at >= meeting.started_at, "ended_at should not precede started_at"

    def test_future_started_at_allowed(self) -> None:
        """Test meeting can be created with future started_at (ED-03).

        This is allowed for scheduling use cases where meetings are
        pre-created before they actually occur.
        """
        from datetime import timedelta

        future_time = utc_now() + timedelta(hours=1)
        params = MeetingLoadParams(state=MeetingState.CREATED, started_at=future_time)
        meeting = Meeting.from_uuid_str(
            uuid_str="12345678-1234-5678-1234-567812345678",
            title="Scheduled Meeting",
            params=params,
        )

        assert meeting.started_at is not None, "started_at should be set"
        assert meeting.started_at == future_time, "started_at should match provided future time"
        assert meeting.started_at > utc_now(), "started_at can be in the future"

    def test_future_ended_at_with_past_started_at(self) -> None:
        """Test meeting can have future ended_at with past started_at (ED-03).

        This edge case may occur when importing external meeting data
        or when clock synchronization issues exist.
        """
        from datetime import timedelta

        past_time = utc_now() - timedelta(hours=1)
        future_time = utc_now() + timedelta(hours=1)

        params = MeetingLoadParams(
            state=MeetingState.STOPPED,
            started_at=past_time,
            ended_at=future_time,
        )
        meeting = Meeting.from_uuid_str(
            uuid_str="12345678-1234-5678-1234-567812345678",
            title="Time-Anomaly Meeting",
            params=params,
        )

        # Duration calculation still works
        assert meeting.duration_seconds > 0, "duration should be positive"
        # Anomalous condition: ended_at > now > started_at
        assert meeting.ended_at is not None, "ended_at should be set"
        assert meeting.ended_at > utc_now(), "ended_at can be in the future"


class TestMeetingStateInvariants:
    """Test state machine invariants are maintained."""

    def test_completed_meeting_has_ended_at(self) -> None:
        """Test completed meeting always has ended_at set."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        meeting.complete()

        assert meeting.state == MeetingState.COMPLETED, "state should be COMPLETED"
        assert meeting.ended_at is not None, "completed meeting must have ended_at"

    def test_recording_meeting_has_started_at(self) -> None:
        """Test recording meeting always has started_at set."""
        meeting = Meeting.create()
        meeting.start_recording()

        assert meeting.state == MeetingState.RECORDING, "state should be RECORDING"
        assert meeting.started_at is not None, "recording meeting must have started_at"

    def test_stopped_meeting_has_both_timestamps(self) -> None:
        """Test stopped meeting has both started_at and ended_at."""
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()

        assert meeting.started_at is not None, "stopped meeting must have started_at"
        assert meeting.ended_at is not None, "stopped meeting must have ended_at"
        assert meeting.started_at <= meeting.ended_at, "started_at must precede ended_at"


class TestMeetingSegmentMutability:
    """Tests for segment modification behavior across states (BH-01).

    Documents current behavior: segments CAN be added to meetings in any state.
    This is intentional for late transcription and correction workflows.
    """

    def test_add_segment_to_completed_meeting_allowed(self) -> None:
        """Test segments can be added to completed meetings.

        This behavior supports late transcription scenarios where
        audio is processed after the meeting has ended.
        """
        meeting = Meeting.create()
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        meeting.complete()

        assert meeting.state == MeetingState.COMPLETED, f"expected state COMPLETED, got {meeting.state}"

        # Adding segment to completed meeting is allowed
        segment = Segment(segment_id=0, text="Late transcription", start_time=0.0, end_time=1.0)
        meeting.add_segment(segment)

        assert meeting.segment_count == 1, "segment should be added"
        assert meeting.segments[0].text == "Late transcription", "segment text should match"

    def test_add_segment_to_error_meeting_allowed(self) -> None:
        """Test segments can be added to error-state meetings.

        This supports recovery workflows where partial transcription
        data may be salvaged from failed recordings.
        """
        meeting = Meeting.create()
        meeting.mark_error()

        assert meeting.state == MeetingState.ERROR, f"expected state ERROR, got {meeting.state}"

        segment = Segment(segment_id=0, text="Recovered text", start_time=0.0, end_time=1.0)
        meeting.add_segment(segment)

        assert meeting.segment_count == 1, "segment should be recoverable in error state"

    def test_segment_list_is_mutable_reference(self) -> None:
        """Test segments list is directly mutable (current behavior).

        Note: This is a documentation of current behavior, not a
        recommendation. Consider making segments immutable in the future.
        """
        meeting = Meeting.create()
        segment = Segment(segment_id=0, text="Original", start_time=0.0, end_time=1.0)
        meeting.add_segment(segment)

        # Direct mutation is possible (documents current behavior)
        meeting.segments[0] = Segment(
            segment_id=0, text="Modified", start_time=0.0, end_time=1.0
        )

        assert meeting.segments[0].text == "Modified", f"expected segment text 'Modified', got {meeting.segments[0].text!r}"
