"""Tests for DiarizationMixin RPC endpoints.

Covers RefineSpeakerDiarization, RenameSpeaker, GetDiarizationJobStatus,
and CancelDiarizationJob RPCs with comprehensive edge case coverage.
"""

from __future__ import annotations

from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Self, cast
from unittest.mock import AsyncMock
from uuid import uuid4

import grpc
import pytest
from _pytest.python_api import ApproxBase

from noteflow.domain.entities.segment import Segment
from noteflow.domain.utils import utc_now
from noteflow.domain.value_objects import MeetingId
from noteflow.grpc._config import ServicesConfig
from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer
from noteflow.infrastructure.diarization import DiarizationEngine
from noteflow.infrastructure.persistence.repositories import DiarizationJob

# Test constants for progress calculation
EXPECTED_RUNNING_JOB_PROGRESS_PERCENT = 50.0


def approx_float(expected: float, *, rel: float | None = None, abs: float | None = None) -> ApproxBase:
    """Type-safe wrapper for pytest.approx with float values.

    Provides explicit type annotations to avoid basedpyright reportUnknownMemberType
    errors caused by pytest.approx having partially unknown parameter types.

    Cast required: pytest.approx has untyped parameters (expected, rel, abs) in its
    signature. The return type ApproxBase is correctly typed, but pyright reports
    the function type as partially unknown due to the untyped parameters.
    """
    # Create a properly-typed callable alias to avoid reportUnknownMemberType
    _approx: ApproxCallable = cast(ApproxCallable, pytest.approx)
    return _approx(expected, rel=rel, abs=abs)


# Protocol for the typed approx callable
class ApproxCallable:
    """Protocol for pytest.approx with explicit parameter types."""

    def __call__(
        self,
        expected: float,
        *,
        rel: float | None = None,
        abs: float | None = None,
        nan_ok: bool = False,
    ) -> ApproxBase:
        """Call signature matching pytest.approx for float values."""
        ...

if TYPE_CHECKING:
    from noteflow.grpc.meeting_store import MeetingStore


class _MockGrpcContext:
    """Minimal async gRPC context for diarization mixin tests."""

    def __init__(self) -> None:
        """Initialize context with tracking for abort calls."""
        self.abort_called = False
        self.abort_code: grpc.StatusCode | None = None
        self.abort_details: str | None = None

    def set_code(self, code: grpc.StatusCode) -> None:
        """Set response status code."""
        self.abort_code = code

    def set_details(self, details: str) -> None:
        """Set response status details."""
        self.abort_details = details

    async def abort(self, code: grpc.StatusCode, details: str) -> None:
        """Record abort call and raise AssertionError to halt execution."""
        self.abort_called = True
        self.abort_code = code
        self.abort_details = details
        raise AssertionError(f"abort called: {code} - {details}")


class _FakeDiarizationEngine(DiarizationEngine):
    """Lightweight diarization engine stub for service config."""

    def __init__(self) -> None:
        super().__init__()


def _create_test_job(
    job_id: str,
    meeting_id: str,
    status: int,
    *,
    started_at: datetime | None = None,
    audio_duration_seconds: float | None = None,
) -> DiarizationJob:
    """Create a DiarizationJob for testing."""
    return DiarizationJob(
        job_id=job_id,
        meeting_id=meeting_id,
        status=status,
        started_at=started_at,
        audio_duration_seconds=audio_duration_seconds,
    )


@pytest.fixture
def diarization_servicer() -> NoteFlowServicer:
    """Create servicer with mock diarization engine for mixin tests."""
    return NoteFlowServicer(services=ServicesConfig(diarization_engine=_FakeDiarizationEngine()))


@pytest.fixture
def diarization_servicer_disabled() -> NoteFlowServicer:
    """Create servicer with diarization disabled."""
    return NoteFlowServicer(
        services=ServicesConfig(
            diarization_engine=_FakeDiarizationEngine(),
            diarization_refinement_enabled=False,
        )
    )


@pytest.fixture
def diarization_servicer_no_engine() -> NoteFlowServicer:
    """Create servicer without diarization engine."""
    return NoteFlowServicer()


def _get_store(servicer: NoteFlowServicer) -> MeetingStore:
    """Extract in-memory store from servicer."""
    return servicer.get_memory_store()


class _MockDiarizationJobsRepo:
    """Mock diarization jobs repository for testing."""

    def __init__(self) -> None:
        """Initialize with empty job storage."""
        self._jobs: dict[str, DiarizationJob] = {}

    async def create(self, job: DiarizationJob) -> DiarizationJob:
        """Store a job."""
        self._jobs[job.job_id] = job
        return job

    async def get(self, job_id: str) -> DiarizationJob | None:
        """Get job by ID."""
        return self._jobs.get(job_id)

    async def update_status(
        self,
        job_id: str,
        status: int,
        *,
        segments_updated: int | None = None,
        speaker_ids: list[str] | None = None,
        error_message: str | None = None,
        started_at: datetime | None = None,
    ) -> bool:
        """Update job status."""
        job = self._jobs.get(job_id)
        if job is None:
            return False
        job.status = status
        if segments_updated is not None:
            job.segments_updated = segments_updated
        if speaker_ids is not None:
            job.speaker_ids = speaker_ids
        if error_message is not None:
            job.error_message = error_message
        if started_at is not None:
            job.started_at = started_at
        job.updated_at = utc_now()
        return True

    async def get_active_for_meeting(self, meeting_id: str) -> DiarizationJob | None:
        """Get active job for meeting."""
        for job in self._jobs.values():
            if job.meeting_id == meeting_id and job.status in (
                noteflow_pb2.JOB_STATUS_QUEUED,
                noteflow_pb2.JOB_STATUS_RUNNING,
            ):
                return job
        return None

    async def get_all_active(self) -> list[DiarizationJob]:
        """Get all active jobs."""
        return [
            job
            for job in self._jobs.values()
            if job.status
            in (noteflow_pb2.JOB_STATUS_QUEUED, noteflow_pb2.JOB_STATUS_RUNNING)
        ]

    async def prune_completed(self, ttl_seconds: float) -> int:
        """Prune completed jobs (no-op for tests)."""
        return 0


class _MockRepositoryProvider:
    """Mock repository provider that supports diarization jobs."""

    def __init__(
        self,
        meetings_repo: AsyncMock,
        diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Initialize with mock repositories."""
        self.meetings = meetings_repo
        self.diarization_jobs = diarization_jobs_repo
        self.supports_diarization_jobs = True
        self.commit = AsyncMock()

    async def __aenter__(self) -> Self:
        """Enter async context."""
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: object,
    ) -> None:
        """Exit async context."""


@pytest.fixture
def mock_diarization_jobs_repo() -> _MockDiarizationJobsRepo:
    """Create mock diarization jobs repository."""
    return _MockDiarizationJobsRepo()


@pytest.fixture
def diarization_servicer_with_db(
    diarization_servicer: NoteFlowServicer,
    mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    monkeypatch: pytest.MonkeyPatch,
) -> NoteFlowServicer:
    """Create servicer with mock database support for diarization jobs.

    Patches create_repository_provider to return a mock that supports
    diarization jobs, while still using the in-memory meeting store.
    """
    original_provider = diarization_servicer.create_repository_provider

    def create_mock_provider() -> _MockRepositoryProvider:
        # Get the real memory store for meeting lookups
        real_provider = original_provider()
        mock_meetings = AsyncMock()
        mock_meetings.get = real_provider.meetings.get
        return _MockRepositoryProvider(
            mock_meetings,
            mock_diarization_jobs_repo,
        )

    # Use monkeypatch for proper typed patching
    monkeypatch.setattr(diarization_servicer, "create_repository_provider", create_mock_provider)
    return diarization_servicer


class TestRefineSpeakerDiarizationValidation:
    """Tests for RefineSpeakerDiarization input validation."""

    @pytest.mark.asyncio
    async def test_refine_rejects_invalid_meeting_id_format(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Invalid UUID format returns error response."""
        context = _MockGrpcContext()

        response = await diarization_servicer.RefineSpeakerDiarization(
            noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id="not-a-uuid"),
            context,
        )

        assert response.segments_updated == 0, "Invalid UUID should not update any segments"
        assert "invalid" in response.error_message.lower(), "Error message should mention 'invalid'"

    @pytest.mark.asyncio
    async def test_refine_rejects_nonexistent_meeting(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Meeting not found returns error response."""
        context = _MockGrpcContext()
        meeting_id = str(uuid4())

        response = await diarization_servicer.RefineSpeakerDiarization(
            noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=meeting_id),
            context,
        )

        assert response.segments_updated == 0, "Nonexistent meeting should not update any segments"
        assert "not found" in response.error_message.lower(), "Error message should mention 'not found'"


class TestRefineSpeakerDiarizationState:
    """Tests for RefineSpeakerDiarization meeting state validation."""

    @pytest.mark.asyncio
    async def test_refine_mixin_rejects_recording_meeting(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Refinement blocked while meeting is recording."""
        store = _get_store(diarization_servicer)
        meeting = store.create("Active recording meeting")
        meeting.start_recording()
        store.update(meeting)
        context = _MockGrpcContext()

        response = await diarization_servicer.RefineSpeakerDiarization(
            noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)),
            context,
        )

        assert response.segments_updated == 0, "Recording meeting should not update any segments"
        assert "stopped" in response.error_message.lower(), "Error message should mention 'stopped'"

    @pytest.mark.asyncio
    async def test_refine_rejects_stopping_meeting(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Refinement blocked while meeting is in stopping state."""
        store = _get_store(diarization_servicer)
        meeting = store.create("Stopping meeting")
        meeting.start_recording()
        meeting.begin_stopping()
        store.update(meeting)
        context = _MockGrpcContext()

        response = await diarization_servicer.RefineSpeakerDiarization(
            noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)),
            context,
        )

        assert response.segments_updated == 0, "Stopping meeting should not update any segments"
        assert "stopped" in response.error_message.lower(), "Error message should require meeting to be stopped"

    @pytest.mark.asyncio
    async def test_refine_accepts_stopped_meeting(
        self, diarization_servicer_with_db: NoteFlowServicer
    ) -> None:
        """Refinement allowed for stopped meetings."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Stopped meeting")
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        store.update(meeting)
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.RefineSpeakerDiarization(
            noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)),
            context,
        )

        assert response.job_id, "Should return job_id for accepted request"
        assert response.status == noteflow_pb2.JOB_STATUS_QUEUED, "Job status should be QUEUED"


class TestRefineSpeakerDiarizationServer:
    """Tests for server-side diarization configuration."""

    @pytest.mark.asyncio
    async def test_refine_returns_error_when_disabled(
        self, diarization_servicer_disabled: NoteFlowServicer
    ) -> None:
        """Disabled refinement returns error response."""
        store = _get_store(diarization_servicer_disabled)
        meeting = store.create("Test meeting")
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        store.update(meeting)
        context = _MockGrpcContext()

        response = await diarization_servicer_disabled.RefineSpeakerDiarization(
            noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)),
            context,
        )

        assert response.segments_updated == 0, "Disabled refinement should not update any segments"
        assert "disabled" in response.error_message.lower(), "Error message should mention 'disabled'"

    @pytest.mark.asyncio
    async def test_refine_returns_error_when_engine_unavailable(
        self, diarization_servicer_no_engine: NoteFlowServicer
    ) -> None:
        """Missing diarization engine returns error response."""
        store = _get_store(diarization_servicer_no_engine)
        meeting = store.create("Test meeting")
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        store.update(meeting)
        context = _MockGrpcContext()

        response = await diarization_servicer_no_engine.RefineSpeakerDiarization(
            noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)),
            context,
        )

        assert response.segments_updated == 0, "Missing engine should not update any segments"
        assert "not enabled" in response.error_message.lower(), "Error message should mention 'not enabled'"


class TestRenameSpeakerValidation:
    """Tests for RenameSpeaker input validation."""

    @pytest.mark.asyncio
    async def test_rename_aborts_on_missing_old_speaker_id(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Missing old_speaker_id aborts with INVALID_ARGUMENT."""
        context = _MockGrpcContext()

        with pytest.raises(AssertionError, match="abort called"):
            await diarization_servicer.RenameSpeaker(
                noteflow_pb2.RenameSpeakerRequest(
                    meeting_id=str(uuid4()),
                    old_speaker_id="",
                    new_speaker_name="Speaker 1",
                ),
                context,
            )

        assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, "Missing old_speaker_id should abort with INVALID_ARGUMENT"

    @pytest.mark.asyncio
    async def test_rename_aborts_on_missing_new_speaker_name(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Missing new_speaker_name aborts with INVALID_ARGUMENT."""
        context = _MockGrpcContext()

        with pytest.raises(AssertionError, match="abort called"):
            await diarization_servicer.RenameSpeaker(
                noteflow_pb2.RenameSpeakerRequest(
                    meeting_id=str(uuid4()),
                    old_speaker_id="SPEAKER_0",
                    new_speaker_name="",
                ),
                context,
            )

        assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, "Missing new_speaker_name should abort with INVALID_ARGUMENT"

    @pytest.mark.asyncio
    async def test_rename_aborts_on_invalid_meeting_id(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Invalid meeting_id format aborts with INVALID_ARGUMENT."""
        context = _MockGrpcContext()

        with pytest.raises(AssertionError, match="abort called"):
            await diarization_servicer.RenameSpeaker(
                noteflow_pb2.RenameSpeakerRequest(
                    meeting_id="invalid-uuid",
                    old_speaker_id="SPEAKER_0",
                    new_speaker_name="Alice",
                ),
                context,
            )

        assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, "Invalid meeting_id format should abort with INVALID_ARGUMENT"


class TestRenameSpeakerOperation:
    """Tests for RenameSpeaker segment updates."""

    @pytest.mark.asyncio
    async def test_rename_updates_matching_segments(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """Segments with matching speaker_id are updated."""
        store = _get_store(diarization_servicer)
        meeting = store.create("Meeting with speakers")
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()

        # Create segments with different speakers
        mid = MeetingId(meeting.id)
        meeting.segments = [
            Segment(meeting_id=mid, segment_id=0, start_time=0.0, end_time=5.0, text="Hello", speaker_id="SPEAKER_0"),
            Segment(meeting_id=mid, segment_id=1, start_time=5.0, end_time=10.0, text="World", speaker_id="SPEAKER_0"),
            Segment(meeting_id=mid, segment_id=2, start_time=10.0, end_time=15.0, text="Hi", speaker_id="SPEAKER_1"),
        ]
        store.update(meeting)

        response = await diarization_servicer.RenameSpeaker(
            noteflow_pb2.RenameSpeakerRequest(
                meeting_id=str(meeting.id), old_speaker_id="SPEAKER_0", new_speaker_name="Alice"
            ),
            _MockGrpcContext(),
        )

        assert response.segments_updated == 2, "Should update 2 SPEAKER_0 segments"
        assert response.success is True, "Rename should succeed"

    @pytest.mark.asyncio
    async def test_rename_returns_zero_for_no_matches(
        self, diarization_servicer: NoteFlowServicer
    ) -> None:
        """No matching segments returns segments_updated=0."""
        store = _get_store(diarization_servicer)
        meeting = store.create("Meeting without target speaker")
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()

        meeting_id = MeetingId(meeting.id)
        segment = Segment(
            meeting_id=meeting_id,
            segment_id=0,
            start_time=0.0,
            end_time=5.0,
            text="Hello",
            speaker_id="SPEAKER_1",
        )

        meeting.segments = [segment]
        store.update(meeting)
        context = _MockGrpcContext()

        response = await diarization_servicer.RenameSpeaker(
            noteflow_pb2.RenameSpeakerRequest(
                meeting_id=str(meeting.id),
                old_speaker_id="SPEAKER_0",
                new_speaker_name="Alice",
            ),
            context,
        )

        assert response.segments_updated == 0, "No matching speaker should result in zero segments updated"
        assert response.success is False, "Rename should fail when no segments match"


class TestGetDiarizationJobStatusProgress:
    """Tests for GetDiarizationJobStatus progress calculation."""

    @pytest.mark.asyncio
    async def test_status_progress_queued_is_zero(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
        mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Queued job progress is 0%."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Test meeting")
        store.update(meeting)

        job_id = str(uuid4())
        await mock_diarization_jobs_repo.create(
            _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_QUEUED)
        )
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.GetDiarizationJobStatus(
            noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id),
            context,
        )

        assert response.progress_percent == approx_float(0.0), "Queued job should have 0% progress"

    @pytest.mark.asyncio
    async def test_status_progress_running_is_time_based(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
        mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Running job progress is based on elapsed time vs estimated duration."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Test meeting")
        store.update(meeting)

        job_id = str(uuid4())
        await mock_diarization_jobs_repo.create(
            _create_test_job(
                job_id,
                str(meeting.id),
                noteflow_pb2.JOB_STATUS_RUNNING,
                started_at=utc_now() - timedelta(seconds=10),
                audio_duration_seconds=120.0,
            )
        )
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.GetDiarizationJobStatus(
            noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id),
            context,
        )

        # 120s audio at ~0.17 ratio = ~20s estimated; 10s elapsed = ~50%
        assert response.progress_percent == approx_float(EXPECTED_RUNNING_JOB_PROGRESS_PERCENT, rel=0.25), "Running job progress should be ~50% based on elapsed time"

    @pytest.mark.asyncio
    async def test_status_progress_completed_is_full(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
        mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Completed job progress is 100%."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Test meeting")
        store.update(meeting)

        job_id = str(uuid4())
        await mock_diarization_jobs_repo.create(
            _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_COMPLETED)
        )
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.GetDiarizationJobStatus(
            noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id),
            context,
        )

        assert response.progress_percent == approx_float(100.0), "Completed job should have 100% progress"

    @pytest.mark.asyncio
    async def test_status_progress_failed_is_zero(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
        mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Failed job progress is 0%."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Test meeting")
        store.update(meeting)

        job_id = str(uuid4())
        await mock_diarization_jobs_repo.create(
            _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_FAILED)
        )
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.GetDiarizationJobStatus(
            noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id),
            context,
        )

        assert response.progress_percent == approx_float(0.0), "Failed job should have 0% progress"


class TestCancelDiarizationJobStates:
    """Tests for CancelDiarizationJob state transitions."""

    @pytest.mark.asyncio
    async def test_cancel_mixin_queued_succeeds(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
        mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Queued job can be cancelled."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Test meeting")
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        store.update(meeting)

        job_id = str(uuid4())
        await mock_diarization_jobs_repo.create(
            _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_QUEUED)
        )
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.CancelDiarizationJob(
            noteflow_pb2.CancelDiarizationJobRequest(job_id=job_id),
            context,
        )

        assert response.success is True, "Cancelling queued job should succeed"
        assert response.status == noteflow_pb2.JOB_STATUS_CANCELLED, "Cancelled queued job status should be CANCELLED"

    @pytest.mark.asyncio
    async def test_cancel_mixin_running_succeeds(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
        mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Running job can be cancelled."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Test meeting")
        store.update(meeting)

        job_id = str(uuid4())
        await mock_diarization_jobs_repo.create(
            _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_RUNNING)
        )
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.CancelDiarizationJob(
            noteflow_pb2.CancelDiarizationJobRequest(job_id=job_id),
            context,
        )

        assert response.success is True, "Cancelling running job should succeed"
        assert response.status == noteflow_pb2.JOB_STATUS_CANCELLED, "Cancelled running job status should be CANCELLED"

    @pytest.mark.asyncio
    async def test_cancel_mixin_nonexistent_fails(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
    ) -> None:
        """Nonexistent job cannot be cancelled."""
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.CancelDiarizationJob(
            noteflow_pb2.CancelDiarizationJobRequest(job_id="nonexistent-job"),
            context,
        )

        assert response.success is False, "Cancelling nonexistent job should fail"
        assert "not found" in response.error_message.lower(), "Error message should mention 'not found'"

    @pytest.mark.asyncio
    async def test_cancel_completed_job_fails(
        self,
        diarization_servicer_with_db: NoteFlowServicer,
        mock_diarization_jobs_repo: _MockDiarizationJobsRepo,
    ) -> None:
        """Completed job cannot be cancelled."""
        store = _get_store(diarization_servicer_with_db)
        meeting = store.create("Test meeting")
        store.update(meeting)

        job_id = str(uuid4())
        await mock_diarization_jobs_repo.create(
            _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_COMPLETED)
        )
        context = _MockGrpcContext()

        response = await diarization_servicer_with_db.CancelDiarizationJob(
            noteflow_pb2.CancelDiarizationJobRequest(job_id=job_id),
            context,
        )

        assert response.success is False, "Cancelling completed job should fail"
        assert response.status == noteflow_pb2.JOB_STATUS_COMPLETED, "Completed job status should remain COMPLETED"
