"""Crash scenario recovery tests.

Tests verify recovery from various crash scenarios including
meeting recovery, diarization job recovery, and concurrent recovery.

Note on SIGKILL behavior:
    SIGKILL (kill -9) cannot be caught or handled by the process. When the
    server is killed with SIGKILL, no cleanup handlers run and state is left
    dirty (RECORDING meetings, RUNNING jobs). The RecoveryService handles
    this on next startup by marking such resources as ERROR/FAILED.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING
from uuid import uuid4

import pytest

from noteflow.domain.entities import Meeting
from noteflow.domain.value_objects import MeetingId, MeetingState
from noteflow.grpc.proto import noteflow_pb2

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

# Test constants
CONCURRENT_RECOVERY_COUNT = 5
RECORDING_MEETINGS_COUNT = 3
RUNNING_JOBS_COUNT = 2


class TestMeetingCrashRecovery:
    """Test meeting recovery after crashes."""

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_recording_meeting_recovery(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify RECORDING meeting marked ERROR after crash."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        # Create meeting in RECORDING state
        meeting = Meeting.create(title="Crash Test")
        meeting.start_recording()  # State = RECORDING
        meeting_id = meeting.id

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        # Simulate crash recovery with new UoW
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        # Verify recovered
        assert result.meetings_recovered >= 1, "Should recover recording meeting"

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered = await uow.meetings.get(meeting_id)
            assert recovered is not None, "Recovered meeting should exist"
            assert recovered.state == MeetingState.ERROR, "Should be marked ERROR"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_stopping_meeting_recovery(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify STOPPING meeting marked ERROR after crash."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Stopping Crash")
        meeting.start_recording()
        meeting_id = meeting.id

        # Manually set state to STOPPING (simulating mid-stop crash)
        meeting.state = MeetingState.STOPPING

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        assert result.meetings_recovered >= 1, "Should recover stopping meeting"

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered = await uow.meetings.get(meeting_id)
            assert recovered is not None, "Recovered meeting should exist"
            assert recovered.state == MeetingState.ERROR, "Should be marked ERROR"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_created_meeting_not_recovered(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify CREATED meetings are not affected by recovery."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Created Meeting")
        meeting_id = meeting.id
        # State is CREATED by default

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            await recovery_service.recover_all()

        # CREATED meeting should not be recovered
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered = await uow.meetings.get(meeting_id)
            assert recovered is not None, "Created meeting should exist"
            assert recovered.state == MeetingState.CREATED, "Should stay CREATED"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_completed_meeting_not_recovered(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify COMPLETED meetings are not affected by recovery."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Completed Meeting")
        meeting.start_recording()
        meeting.begin_stopping()
        meeting.stop_recording()
        meeting.complete()  # State = COMPLETED
        meeting_id = meeting.id

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            await recovery_service.recover_all()

        # COMPLETED meeting should not be recovered
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered = await uow.meetings.get(meeting_id)
            assert recovered is not None, "Completed meeting should exist"
            assert recovered.state == MeetingState.COMPLETED, "Should stay COMPLETED"


class TestDiarizationJobRecovery:
    """Test diarization job recovery after crashes."""

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_running_job_marked_failed(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify RUNNING diarization jobs marked FAILED."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.repositories import DiarizationJob
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Job Recovery")
        meeting_id = meeting.id

        job = DiarizationJob(
            job_id=str(uuid4()),
            meeting_id=str(meeting_id),
            status=noteflow_pb2.JOB_STATUS_RUNNING,
        )

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.diarization_jobs.create(job)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        assert result.diarization_jobs_failed >= 1, "Should fail running job"

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered_job = await uow.diarization_jobs.get(job.job_id)
            assert recovered_job is not None, "Job should exist after recovery"
            assert recovered_job.status == noteflow_pb2.JOB_STATUS_FAILED, "Should be FAILED"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_queued_job_marked_failed(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify QUEUED diarization jobs marked FAILED."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.repositories import DiarizationJob
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Queued Job")
        meeting_id = meeting.id

        job = DiarizationJob(
            job_id=str(uuid4()),
            meeting_id=str(meeting_id),
            status=noteflow_pb2.JOB_STATUS_QUEUED,
        )

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.diarization_jobs.create(job)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        assert result.diarization_jobs_failed >= 1, "Should fail queued job"

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered_job = await uow.diarization_jobs.get(job.job_id)
            assert recovered_job is not None, "Job should exist after recovery"
            assert recovered_job.status == noteflow_pb2.JOB_STATUS_FAILED, "Should be FAILED"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_completed_job_not_recovered(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify COMPLETED diarization jobs are not affected."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.repositories import DiarizationJob
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Completed Job")
        meeting_id = meeting.id

        job = DiarizationJob(
            job_id=str(uuid4()),
            meeting_id=str(meeting_id),
            status=noteflow_pb2.JOB_STATUS_COMPLETED,
        )

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.diarization_jobs.create(job)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            await recovery_service.recover_all()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered_job = await uow.diarization_jobs.get(job.job_id)
            assert recovered_job is not None, "Completed job should exist"
            assert recovered_job.status == noteflow_pb2.JOB_STATUS_COMPLETED, "Should stay COMPLETED"


class TestRecoveryIdempotence:
    """Test recovery idempotence and concurrent safety."""

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_recovery_is_idempotent(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify running recovery twice doesn't corrupt state."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Idempotent Test")
        meeting.start_recording()
        meeting_id = meeting.id

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        # Run recovery first time
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result1 = await recovery_service.recover_all()

        # Run recovery second time
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result2 = await recovery_service.recover_all()

        # Second run should find nothing to recover
        assert result1.meetings_recovered >= 1, "First recovery should find meeting"
        assert result2.meetings_recovered == 0, "Second recovery should find nothing"

        # Final state should be ERROR
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            final = await uow.meetings.get(meeting_id)
            assert final is not None, "Meeting should exist after recovery"
            assert final.state == MeetingState.ERROR, "Recovered meeting should be ERROR"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_concurrent_recovery_calls(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify concurrent recovery calls don't corrupt data."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        # Create multiple crashed meetings
        meeting_ids: list[MeetingId] = []
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            for i in range(CONCURRENT_RECOVERY_COUNT):
                meeting = Meeting.create(title=f"Concurrent-{i}")
                meeting.start_recording()
                meeting_ids.append(meeting.id)
                await uow.meetings.create(meeting)
            await uow.commit()

        # Run recovery sequentially (concurrent would need separate UoW instances)
        async def run_recovery() -> int:
            async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
                recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
                result = await recovery_service.recover_all()
                return result.meetings_recovered

        # Run recovery - first call should recover all, subsequent ones find nothing
        result1 = await run_recovery()
        result2 = await run_recovery()
        result3 = await run_recovery()

        # Combined should be exactly the count (first wins, others find nothing)
        total_recovered = result1 + result2 + result3
        assert total_recovered == CONCURRENT_RECOVERY_COUNT, (
            f"expected {CONCURRENT_RECOVERY_COUNT} total recovered, got {total_recovered}"
        )

        # Verify all in ERROR state
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            meetings = [await uow.meetings.get(mid) for mid in meeting_ids]
            missing = [m for m in meetings if m is None]
            wrong_state = [m for m in meetings if m is not None and m.state != MeetingState.ERROR]
            assert not missing, f"All recovered meetings should exist, but {len(missing)} are missing"
            assert not wrong_state, f"All meetings should be in ERROR state, but {len(wrong_state)} are not"


class TestMixedCrashRecovery:
    """Test recovery with mixed meeting and job states."""

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_mixed_recovery_counts(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify recovery correctly counts recovered entities."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.repositories import DiarizationJob
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            # Recording meeting with running job (should be recovered)
            meeting = Meeting.create(title="Recording + Running Job")
            meeting.start_recording()
            await uow.meetings.create(meeting)

            job = DiarizationJob(
                job_id=str(uuid4()),
                meeting_id=str(meeting.id),
                status=noteflow_pb2.JOB_STATUS_RUNNING,
            )
            await uow.diarization_jobs.create(job)

            # Created meeting (should not be recovered)
            created = Meeting.create(title="Created Meeting")
            await uow.meetings.create(created)

            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        assert result.meetings_recovered == 1, "Should recover 1 recording meeting"
        assert result.diarization_jobs_failed == 1, "Should fail 1 running job"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_mixed_recovery_preserves_completed(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify recovery doesn't affect completed meetings and jobs."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.repositories import DiarizationJob
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            # Completed meeting with completed job
            meeting = Meeting.create(title="Completed")
            meeting.start_recording()
            meeting.begin_stopping()
            meeting.stop_recording()
            meeting.complete()
            await uow.meetings.create(meeting)

            job = DiarizationJob(
                job_id=str(uuid4()),
                meeting_id=str(meeting.id),
                status=noteflow_pb2.JOB_STATUS_COMPLETED,
            )
            await uow.diarization_jobs.create(job)
            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            await recovery_service.recover_all()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            m = await uow.meetings.get(meeting.id)
            assert m is not None, "Meeting should exist"
            assert m.state == MeetingState.COMPLETED, "Completed meeting unchanged"

            j = await uow.diarization_jobs.get(job.job_id)
            assert j is not None, "Job should exist"
            assert j.status == noteflow_pb2.JOB_STATUS_COMPLETED, "Completed job unchanged"


class TestRecoveryResult:
    """Test RecoveryResult data structure."""

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_recovery_result_counts(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify RecoveryResult correctly reports counts."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.repositories import DiarizationJob
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            # Create recording meetings
            for i in range(RECORDING_MEETINGS_COUNT):
                meeting = Meeting.create(title=f"Recording-{i}")
                meeting.start_recording()
                await uow.meetings.create(meeting)

            # Create running jobs
            for i in range(RUNNING_JOBS_COUNT):
                meeting = Meeting.create(title=f"Job-{i}")
                await uow.meetings.create(meeting)
                job = DiarizationJob(
                    job_id=str(uuid4()),
                    meeting_id=str(meeting.id),
                    status=noteflow_pb2.JOB_STATUS_RUNNING,
                )
                await uow.diarization_jobs.create(job)

            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        assert result.meetings_recovered == RECORDING_MEETINGS_COUNT, (
            f"expected {RECORDING_MEETINGS_COUNT} meetings recovered, "
            f"got {result.meetings_recovered}"
        )
        assert result.diarization_jobs_failed == RUNNING_JOBS_COUNT, (
            f"expected {RUNNING_JOBS_COUNT} jobs failed, "
            f"got {result.diarization_jobs_failed}"
        )

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_empty_recovery_result(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify RecoveryResult is zero when nothing to recover."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        # Create only CREATED/COMPLETED meetings
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            meeting1 = Meeting.create(title="Created")
            await uow.meetings.create(meeting1)

            meeting2 = Meeting.create(title="Completed")
            meeting2.start_recording()
            meeting2.begin_stopping()
            meeting2.stop_recording()
            meeting2.complete()
            await uow.meetings.create(meeting2)

            await uow.commit()

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        assert result.meetings_recovered == 0, (
            f"expected 0 meetings recovered, got {result.meetings_recovered}"
        )
        assert result.diarization_jobs_failed == 0, (
            f"expected 0 jobs failed, got {result.diarization_jobs_failed}"
        )


class TestPartialTransactionRecovery:
    """Test recovery from partial/interrupted transactions."""

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_uncommitted_changes_not_visible(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify uncommitted state changes are rolled back."""
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        meeting = Meeting.create(title="Partial Transaction")
        meeting_id = meeting.id

        # Start transaction, make changes, don't commit (simulates crash)
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            # No commit - simulates crash before commit

        # New session should not see uncommitted meeting
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            found = await uow.meetings.get(meeting_id)
            assert found is None, "Uncommitted meeting should not be visible"

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_partial_state_transition_recovery(
        self,
        session_factory: async_sessionmaker[AsyncSession],
        tmp_path: Path,
    ) -> None:
        """Verify recovery handles interrupted state transitions."""
        from noteflow.application.services.recovery_service import RecoveryService
        from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

        # Create meeting in valid state
        meeting = Meeting.create(title="State Transition")
        meeting.start_recording()
        meeting_id = meeting.id

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            await uow.meetings.create(meeting)
            await uow.commit()

        # Simulate interrupted stop: begin_stopping but crash before stop_recording
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            m = await uow.meetings.get(meeting_id)
            assert m is not None, f"meeting {meeting_id} should exist before state transition"
            m.begin_stopping()  # State = STOPPING
            await uow.meetings.update(m)
            await uow.commit()

        # Recovery should handle STOPPING state
        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
            result = await recovery_service.recover_all()

        assert result.meetings_recovered >= 1, "STOPPING meeting should be recovered"

        async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
            recovered = await uow.meetings.get(meeting_id)
            assert recovered is not None, f"meeting {meeting_id} should exist after recovery"
            assert recovered.state == MeetingState.ERROR, (
                f"recovered meeting should be ERROR, got {recovered.state}"
            )
