"""Tests for transcription.worker."""

from pathlib import Path
from threading import Event

import pytest
from sqlmodel import select

from transcription.config import Settings
from transcription.errors import AppError, ErrorCategory
from transcription.models import Document, Job, JobStatus, Transcript
from transcription.providers.base import TranscriptionResult
from transcription.worker import process_next_queued_job, run_worker_loop


def _create_queued_job(session, *, filename: str = "doc.jpg", file_path: str = "uploads/doc.jpg") -> Job:
    document = Document(filename=filename, file_path=file_path)
    session.add(document)
    session.commit()
    session.refresh(document)

    job = Job(document_id=document.id, status=JobStatus.QUEUED)
    session.add(job)
    session.commit()
    session.refresh(job)
    return job


@pytest.mark.integration
class TestWorkerQueueBehavior:
    """Verify worker behavior when selecting queued jobs."""

    def test_returns_false_when_queue_empty(self, session):
        """process_next_queued_job returns False when there are no queued jobs."""
        processed = process_next_queued_job(session=session)
        assert processed is False


@pytest.mark.integration
class TestWorkerSuccessPath:
    """Verify worker success-path lifecycle transitions and transcript persistence."""

    def test_transitions_processing_to_transcribed(self, session, monkeypatch, tmp_path: Path):
        """process_next_queued_job transitions queued jobs to transcribed on success."""
        job = _create_queued_job(session)

        def _fake_transcribe(_path):
            return TranscriptionResult(text="ok", provider="openrouter", model="test-model")

        monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
        monkeypatch.setattr(
            "transcription.worker.get_settings",
            lambda: Settings(openrouter_api_key="test-key", prompt_dir=tmp_path),
        )

        processed = process_next_queued_job(session=session)
        session.refresh(job)

        assert processed is True
        assert job.status == JobStatus.TRANSCRIBED

    def test_persists_transcript_text_on_success(self, session, monkeypatch, tmp_path: Path):
        """process_next_queued_job stores transcript text for successful jobs."""
        job = _create_queued_job(session)

        def _fake_transcribe(_path):
            return TranscriptionResult(text="Transcript body", provider="openrouter", model="test-model")

        monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
        monkeypatch.setattr(
            "transcription.worker.get_settings",
            lambda: Settings(openrouter_api_key="test-key", prompt_dir=tmp_path),
        )

        process_next_queued_job(session=session)

        transcript = session.exec(
            select(Transcript).where(Transcript.job_id == job.id)
        ).first()
        assert transcript is not None
        assert transcript.text == "Transcript body"
        assert transcript.error_detail is None


@pytest.mark.integration
class TestWorkerFailurePath:
    """Verify worker failure-path lifecycle transitions and error persistence."""

    def test_sets_failed_and_error_detail_on_failure(self, session, monkeypatch, tmp_path: Path):
        """process_next_queued_job marks failed and stores error detail on exception."""
        job = _create_queued_job(session)

        def _fake_transcribe(_path):
            raise RuntimeError("provider failure")

        monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
        monkeypatch.setattr(
            "transcription.worker.get_settings",
            lambda: Settings(openrouter_api_key="test-key", prompt_dir=tmp_path),
        )

        processed = process_next_queued_job(session=session)
        session.refresh(job)
        transcript = session.exec(
            select(Transcript).where(Transcript.job_id == job.id)
        ).first()

        assert processed is True
        assert job.status == JobStatus.FAILED
        assert transcript is not None
        assert transcript.text is None
        assert "provider failure" in transcript.error_detail
        assert "[internal_unexpected_error]" in transcript.error_detail
        assert "error_id=" in transcript.error_detail
        assert "suggestion=" in transcript.error_detail

    def test_updates_existing_transcript_if_present(self, session, monkeypatch, tmp_path: Path):
        """process_next_queued_job updates existing transcript instead of duplicating."""
        job = _create_queued_job(session)
        existing = Transcript(job_id=job.id, text="old", error_detail=None)
        session.add(existing)
        session.commit()
        session.refresh(existing)

        def _fake_transcribe(_path):
            raise RuntimeError("provider failure")

        monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
        monkeypatch.setattr(
            "transcription.worker.get_settings",
            lambda: Settings(openrouter_api_key="test-key", prompt_dir=tmp_path),
        )

        process_next_queued_job(session=session)

        transcripts = session.exec(
            select(Transcript).where(Transcript.job_id == job.id)
        ).all()
        assert len(transcripts) == 1
        assert transcripts[0].id == existing.id
        assert transcripts[0].text is None
        assert "provider failure" in transcripts[0].error_detail
        assert "[internal_unexpected_error]" in transcripts[0].error_detail
        assert "error_id=" in transcripts[0].error_detail


@pytest.mark.integration
class TestWorkerRetryBehavior:
    """Verify worker retry and terminal failure policies."""

    def test_retriable_failure_requeues_until_limit(self, session, monkeypatch, tmp_path: Path):
        """Retriable failures requeue jobs while retry budget remains."""
        job = _create_queued_job(session)

        def _fake_transcribe(_path):
            raise AppError(
                "temporary upstream outage",
                category=ErrorCategory.EXTERNAL_PROVIDER,
                suggestion="Retry from jobs page.",
                retriable=True,
            )

        monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
        monkeypatch.setattr(
            "transcription.worker.get_settings",
            lambda: Settings(
                openrouter_api_key="test-key",
                prompt_dir=tmp_path,
                worker_max_retries=1,
                worker_retry_backoff_seconds=0.0,
            ),
        )

        processed = process_next_queued_job(session=session)
        session.refresh(job)

        assert processed is True
        assert job.status == JobStatus.QUEUED
        assert job.retry_count == 1

    def test_retriable_failure_exhaustion_sets_failed(self, session, monkeypatch, tmp_path: Path):
        """Retriable failures transition to failed when retry budget is exhausted."""
        job = _create_queued_job(session)
        job.retry_count = 1
        session.add(job)
        session.commit()

        def _fake_transcribe(_path):
            raise AppError(
                "temporary upstream outage",
                category=ErrorCategory.EXTERNAL_PROVIDER,
                suggestion="Retry from jobs page.",
                retriable=True,
            )

        monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
        monkeypatch.setattr(
            "transcription.worker.get_settings",
            lambda: Settings(
                openrouter_api_key="test-key",
                prompt_dir=tmp_path,
                worker_max_retries=1,
                worker_retry_backoff_seconds=0.0,
            ),
        )

        process_next_queued_job(session=session)
        session.refresh(job)

        assert job.status == JobStatus.FAILED
        assert job.retry_count == 1


@pytest.mark.unit
class TestWorkerLoopControl:
    """Verify worker loop start/stop behavior."""

    def test_stops_when_stop_event_is_set(self, monkeypatch):
        """run_worker_loop exits when a stop event is set."""
        stop_event = Event()
        stop_event.set()

        called = {"value": False}

        def _fake_process_next_queued_job(**_kwargs):
            called["value"] = True
            return False

        monkeypatch.setattr("transcription.worker.process_next_queued_job", _fake_process_next_queued_job)

        run_worker_loop(stop_event=stop_event, poll_interval_seconds=0.01)
        assert called["value"] is False
