"""Tests for Sprint 15.1 critical bug fixes.

Verification tests for:
1. Race condition in stream initialization (lock protection)
2. Double StopMeeting idempotency
3. Datetime timezone awareness in diarization jobs

These tests validate the fixes for the three critical bugs identified
in Phase 4.5 Sprint 15.1.
"""

from __future__ import annotations

import asyncio
import re
from datetime import timedelta
from pathlib import Path
from uuid import uuid4

import pytest

from noteflow.domain.entities.meeting import Meeting, MeetingState
from noteflow.domain.utils import utc_now
from noteflow.domain.value_objects import MeetingId
from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer
from noteflow.infrastructure.persistence.repositories import DiarizationJob


class TestStreamInitRaceCondition:
    """Tests for Bug 1: Race condition in stream initialization."""

    @pytest.mark.asyncio
    async def teststream_init_lock_exists(
        self, memory_servicer: NoteFlowServicer
    ) -> None:
        """Verify stream_init_lock attribute exists on servicer."""
        assert hasattr(memory_servicer, "stream_init_lock"), (
            "stream_init_lock attribute missing"
        )
        assert isinstance(memory_servicer.stream_init_lock, asyncio.Lock), (
            "stream_init_lock should be an asyncio.Lock"
        )

    @pytest.mark.asyncio
    async def teststream_init_lock_is_functional(
        self, memory_servicer: NoteFlowServicer
    ) -> None:
        """Verify lock can be acquired and released."""
        lock = memory_servicer.stream_init_lock

        # Should be able to acquire
        async with lock:
            # Lock is held, should be locked
            assert lock.locked(), "Lock should be held inside context"

        # Lock released outside context
        assert not lock.locked(), "Lock should be released after context"

    @pytest.mark.asyncio
    async def testactive_streams_check_and_add_is_atomic_with_lock(
        self, memory_servicer: NoteFlowServicer
    ) -> None:
        """Verify the lock prevents race conditions in check-and-add."""
        meeting_id = str(uuid4())
        results: list[str] = []

        async def check_and_add(task_name: str) -> None:
            """Simulate the check-and-add pattern under lock."""
            async with memory_servicer.stream_init_lock:
                if meeting_id in memory_servicer.active_streams:
                    results.append(f"{task_name}_rejected")
                else:
                    memory_servicer.active_streams.add(meeting_id)
                    results.append(f"{task_name}_success")

        # Concurrent attempts should result in exactly one success
        await asyncio.gather(
            check_and_add("task1"),
            check_and_add("task2"),
        )

        success_count = sum(1 for r in results if r.endswith("_success"))
        reject_count = sum(1 for r in results if r.endswith("_rejected"))

        assert success_count == 1, f"Exactly one should succeed: {results}"
        assert reject_count == 1, f"Exactly one should be rejected: {results}"

    def teststream_init_lock_in_protocol(self) -> None:
        """Verify stream_init_lock is declared in ServicerHost protocol."""
        protocol_path = Path("src/noteflow/grpc/_mixins/protocols.py")
        content = protocol_path.read_text()

        assert "stream_init_lock: asyncio.Lock" in content, (
            "stream_init_lock not declared in ServicerHost protocol"
        )

    def test_stream_init_uses_lock(self) -> None:
        """Verify _init_stream_for_meeting uses the lock."""
        # Check in the session manager module (streaming package)
        session_path = Path("src/noteflow/grpc/_mixins/streaming/_session.py")
        content = session_path.read_text()

        assert "async with host.stream_init_lock:" in content, (
            "_init_stream_for_meeting should use stream_init_lock"
        )


class TestStopMeetingIdempotency:
    """Tests for Bug 2: Double StopMeeting causes state inconsistency."""

    def test_idempotency_guard_in_code(self) -> None:
        """Verify the idempotency guard code exists in StopMeeting."""
        meeting_path = Path("src/noteflow/grpc/_mixins/meeting.py")
        content = meeting_path.read_text()

        # Verify the idempotency guard pattern exists
        # The pattern can be either inline tuple or via variable
        assert "meeting.state in " in content, (
            "Idempotency guard pattern not found in meeting.py"
        )
        assert "MeetingState.STOPPED" in content, (
            "STOPPED state check not found in meeting.py"
        )
        assert "MeetingState.STOPPING" in content, (
            "STOPPING state check not found in meeting.py"
        )
        assert "MeetingState.COMPLETED" in content, (
            "COMPLETED state check not found in meeting.py"
        )

    def test_idempotency_guard_returns_early(self) -> None:
        """Verify idempotency guard returns meeting proto without state transition."""
        meeting_path = Path("src/noteflow/grpc/_mixins/meeting.py")
        content = meeting_path.read_text()

        # Find the idempotency guard pattern and verify it returns
        # Pattern: if meeting.state in terminal_states: ... return meeting_to_proto(meeting)
        pattern = r"if meeting\.state in terminal_states:\s+.*?return meeting_to_proto\(meeting\)"
        match = re.search(pattern, content, re.DOTALL)
        assert match is not None, (
            "Idempotency guard should return meeting_to_proto(meeting) for terminal states"
        )

    @pytest.mark.parametrize(
        "state",
        [
            pytest.param(MeetingState.STOPPED, id="stopped"),
            pytest.param(MeetingState.STOPPING, id="stopping"),
            pytest.param(MeetingState.COMPLETED, id="completed"),
        ],
    )
    def test_terminal_state_is_valid_meeting_state(self, state: MeetingState) -> None:
        """Verify terminal state is a valid MeetingState value."""
        assert state in MeetingState.__members__.values(), (
            f"{state} should be a valid MeetingState"
        )

    @pytest.mark.parametrize(
        "state",
        [
            pytest.param(MeetingState.STOPPED, id="stopped"),
            pytest.param(MeetingState.STOPPING, id="stopping"),
            pytest.param(MeetingState.COMPLETED, id="completed"),
        ],
    )
    def test_idempotency_guard_triggers_for_terminal_state(
        self, state: MeetingState
    ) -> None:
        """Verify idempotency guard activates for terminal state."""
        meeting_id = MeetingId(uuid4())
        meeting = Meeting(id=meeting_id, title="Test Meeting", state=state)

        # Check the condition that would trigger early return
        should_skip = meeting.state in (
            MeetingState.STOPPED,
            MeetingState.STOPPING,
            MeetingState.COMPLETED,
        )
        assert should_skip, f"State {state.name} should trigger idempotency guard"


class TestDiarizationDatetimeAwareness:
    """Tests for Bug 3: Datetime mismatch causes memory leak."""

    def test_diarization_job_updated_at_allows_utc_comparison(self) -> None:
        """DiarizationJob updated_at can be compared with utc_now() without error."""
        job = DiarizationJob(
            job_id=str(uuid4()),
            meeting_id=str(uuid4()),
            status=noteflow_pb2.JOB_STATUS_COMPLETED,
            started_at=utc_now(),
        )
        # Simulate what happens after job completion
        job.updated_at = utc_now()

        # This comparison should not raise TypeError
        cutoff = utc_now()
        assert job.updated_at is not None, "updated_at should be set"
        # Should be able to compare without error (would fail with naive datetime)
        assert job.updated_at <= cutoff, "updated_at should be before or equal to cutoff"

    def test_utc_now_returns_timezone_aware_datetime(self) -> None:
        """utc_now() returns timezone-aware datetime."""
        now = utc_now()
        assert now.tzinfo is not None, "utc_now() must return timezone-aware datetime"

    def test_diarization_job_pruning_comparison_works(self) -> None:
        """Simulated pruning logic works with timezone-aware datetimes."""
        job = DiarizationJob(
            job_id=str(uuid4()),
            meeting_id=str(uuid4()),
            status=noteflow_pb2.JOB_STATUS_COMPLETED,
            started_at=utc_now(),
        )
        job.updated_at = utc_now()

        # Simulate pruning check
        ttl_seconds = 3600  # 1 hour
        cutoff = utc_now() - timedelta(seconds=ttl_seconds)

        # This should work without TypeError
        is_expired = job.updated_at < cutoff
        assert not is_expired, "Recently updated job should not be expired"

        # Simulate old job
        job.updated_at = utc_now() - timedelta(seconds=ttl_seconds + 100)
        is_expired = job.updated_at < cutoff
        assert is_expired, "Old job should be expired"

    def test_no_datetime_now_in_diarization_mixin(self) -> None:
        """Verify datetime.now() is not used in diarization mixin package."""
        diarization_dir = Path("src/noteflow/grpc/_mixins/diarization")

        # Check all Python files in the diarization package
        files_with_datetime_now = [
            p.name for p in diarization_dir.glob("*.py")
            if "datetime.now()" in p.read_text()
        ]
        assert not files_with_datetime_now, (
            f"datetime.now() found in {files_with_datetime_now} - "
            "should use utc_now() for timezone-aware datetimes"
        )
