from uuid import uuid4
from datetime import UTC
from datetime import datetime
from datetime import timedelta

import pytest

from transcription.db.models import Document
from transcription.db.models import Job
from transcription.db.models import JobSource
from transcription.db.models import JobSourceStatus
from transcription.db.models import JobStatus
from transcription.db.models import Source
from transcription.services.documents import DocumentService
from transcription.services.jobs import JobDeleteBlockedError
from transcription.services.jobs import JobCancelBlockedError
from transcription.services.jobs import JobResubmitBlockedError
from transcription.services.jobs import JobService


class TestJobService:
    @pytest.mark.asyncio
    async def test_create_and_read_job(self, job_service: JobService, document_service: DocumentService):
        document = Document(id=uuid4(), name="test-bundle")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id)
        await job_service.create_job(job=job)

        fetched = await job_service.read_job(job_id=job.id)
        assert fetched.id == job.id
        assert fetched.document is not None
        assert fetched.document.id == document.id

    @pytest.mark.asyncio
    async def test_update_job_state_updates_status_and_retry(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="test-bundle")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id)
        await job_service.create_job(job=job)

        updated = await job_service.update_job_state(
            job_id=job.id,
            status=JobStatus.PROCESSING,
            retry_count_increment=1,
        )

        assert updated.status == JobStatus.PROCESSING
        assert updated.retry_count == 1

    @pytest.mark.asyncio
    async def test_query_jobs_by_status(self, job_service: JobService, document_service: DocumentService):
        document = Document(id=uuid4(), name="query-doc")
        await document_service.create_document(document=document)

        await job_service.create_job(job=Job(document_id=document.id, status=JobStatus.PROCESSING))
        await job_service.create_job(job=Job(document_id=document.id, status=JobStatus.QUEUED))

        result = await job_service.query_jobs(status=JobStatus.PROCESSING)
        assert len(result) == 1
        assert result[0].status == JobStatus.PROCESSING

    @pytest.mark.asyncio
    async def test_query_jobs_by_source_filename(self, job_service: JobService, document_service: DocumentService):
        document = Document(id=uuid4(), name="source-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id)
        await job_service.create_job(job=job)

        async with job_service._session_scope() as session:
            source = Source(
                document_id=document.id,
                page_number=1,
                upload_name="letter.jpg",
                filename="stored-letter.jpg",
                file_path="/uploads/stored-letter.jpg",
            )
            session.add(source)
            await session.flush()
            session.add(
                JobSource(
                    job_id=job.id,
                    source_id=source.id,
                    status=JobSourceStatus.PENDING,
                )
            )
            await session.commit()

        result = await job_service.query_jobs(filename="stored-letter.jpg")
        assert len(result) == 1
        assert result[0].id == job.id

    @pytest.mark.asyncio
    async def test_read_next_queued_job_orders_by_created_date(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="ordered-doc")
        await document_service.create_document(document=document)

        created_at = datetime.now(UTC)
        first = Job(document_id=document.id, status=JobStatus.QUEUED, date_created=created_at)
        second = Job(
            document_id=document.id,
            status=JobStatus.QUEUED,
            date_created=created_at + timedelta(microseconds=1),
        )
        await job_service.create_job(job=first)
        await job_service.create_job(job=second)

        next_job = await job_service.read_next_queued_job()
        assert next_job is not None
        assert next_job.id == first.id

    @pytest.mark.asyncio
    async def test_create_job_persists_provider_model_prompt(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="provider-doc")
        await document_service.create_document(document=document)

        job = Job(
            document_id=document.id,
            provider="openrouter",
            model="google/gemini-2.5-flash",
            prompt_name="transcribe_document.md",
        )
        await job_service.create_job(job=job)

        fetched = await job_service.read_job(job_id=job.id)
        assert fetched.provider == "openrouter"
        assert fetched.model == "google/gemini-2.5-flash"
        assert fetched.prompt_name == "transcribe_document.md"

    @pytest.mark.asyncio
    async def test_read_job_resolves_filename_from_linked_source(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="filename-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id)
        await job_service.create_job(job=job)

        async with job_service._session_scope() as session:
            source = Source(
                document_id=document.id,
                page_number=1,
                upload_name="page_001.png",
                filename="stored_page_001.png",
                file_path="/uploads/stored_page_001.png",
            )
            session.add(source)
            await session.flush()

            session.add(
                JobSource(
                    job_id=job.id,
                    source_id=source.id,
                    status=JobSourceStatus.PENDING,
                )
            )
            await session.commit()

        fetched = await job_service.read_job(job_id=job.id)
        assert fetched.filename == "stored_page_001.png"

    @pytest.mark.asyncio
    async def test_delete_job_with_guardrails_blocks_processing_jobs(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="processing-delete-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id, status=JobStatus.PROCESSING)
        await job_service.create_job(job=job)

        with pytest.raises(JobDeleteBlockedError):
            await job_service.delete_job_with_guardrails(job_id=job.id)

    @pytest.mark.asyncio
    async def test_delete_job_with_guardrails_removes_jobsource_links(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="delete-job-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id, status=JobStatus.QUEUED)
        await job_service.create_job(job=job)

        async with job_service._session_scope() as session:
            source = Source(
                document_id=document.id,
                page_number=1,
                upload_name="delete-job-source.jpg",
                filename="stored-delete-job-source.jpg",
                file_path="/uploads/stored-delete-job-source.jpg",
            )
            session.add(source)
            await session.flush()
            session.add(
                JobSource(
                    job_id=job.id,
                    source_id=source.id,
                    status=JobSourceStatus.PENDING,
                )
            )
            await session.commit()

        await job_service.delete_job_with_guardrails(job_id=job.id)

        with pytest.raises(ValueError):
            await job_service.read_job(job_id=job.id)

    @pytest.mark.asyncio
    async def test_cancel_job_marks_non_transcribed_sources_failed(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="cancel-job-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id, status=JobStatus.QUEUED)
        await job_service.create_job(job=job)

        async with job_service._session_scope() as session:
            source_one = Source(
                document_id=document.id,
                page_number=1,
                upload_name="cancel-1.jpg",
                filename="stored-cancel-1.jpg",
                file_path="/uploads/stored-cancel-1.jpg",
            )
            source_two = Source(
                document_id=document.id,
                page_number=2,
                upload_name="cancel-2.jpg",
                filename="stored-cancel-2.jpg",
                file_path="/uploads/stored-cancel-2.jpg",
            )
            session.add(source_one)
            session.add(source_two)
            await session.flush()

            session.add(
                JobSource(
                    job_id=job.id,
                    source_id=source_one.id,
                    status=JobSourceStatus.TRANSCRIBED,
                    raw_transcription="done",
                )
            )
            session.add(
                JobSource(
                    job_id=job.id,
                    source_id=source_two.id,
                    status=JobSourceStatus.PENDING,
                )
            )
            await session.commit()

        cancelled = await job_service.cancel_job(job_id=job.id)
        assert cancelled.status == JobStatus.FAILED

        refreshed = await job_service.read_job(job_id=job.id)
        statuses = {item.status for item in refreshed.job_sources}
        assert JobSourceStatus.TRANSCRIBED in statuses
        assert JobSourceStatus.FAILED in statuses
        pending_entry = next(item for item in refreshed.job_sources if item.status == JobSourceStatus.FAILED)
        assert pending_entry.error_detail == "Cancelled by user"

    @pytest.mark.asyncio
    async def test_resubmit_non_transcribed_sources_resets_only_non_transcribed(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="resubmit-job-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id, status=JobStatus.FAILED)
        await job_service.create_job(job=job)

        async with job_service._session_scope() as session:
            source_one = Source(
                document_id=document.id,
                page_number=1,
                upload_name="resubmit-1.jpg",
                filename="stored-resubmit-1.jpg",
                file_path="/uploads/stored-resubmit-1.jpg",
                raw_transcription="existing text",
            )
            source_two = Source(
                document_id=document.id,
                page_number=2,
                upload_name="resubmit-2.jpg",
                filename="stored-resubmit-2.jpg",
                file_path="/uploads/stored-resubmit-2.jpg",
                raw_transcription="done text",
            )
            session.add(source_one)
            session.add(source_two)
            await session.flush()

            session.add(
                JobSource(
                    job_id=job.id,
                    source_id=source_one.id,
                    status=JobSourceStatus.FAILED,
                    raw_transcription=None,
                    error_detail="prior error",
                )
            )
            session.add(
                JobSource(
                    job_id=job.id,
                    source_id=source_two.id,
                    status=JobSourceStatus.TRANSCRIBED,
                    raw_transcription="done text",
                )
            )
            await session.commit()

        count = await job_service.resubmit_non_transcribed_sources(job_id=job.id)
        assert count == 1

        refreshed = await job_service.read_job(job_id=job.id)
        assert refreshed.status == JobStatus.QUEUED

        failed_entry = next(item for item in refreshed.job_sources if item.source is not None and item.source.page_number == 1)
        transcribed_entry = next(item for item in refreshed.job_sources if item.source is not None and item.source.page_number == 2)
        assert failed_entry.status == JobSourceStatus.PENDING
        assert failed_entry.error_detail is None
        assert failed_entry.source is not None
        assert failed_entry.source.raw_transcription is None
        assert transcribed_entry.status == JobSourceStatus.TRANSCRIBED

    @pytest.mark.asyncio
    async def test_resubmit_non_transcribed_sources_blocks_when_processing(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="resubmit-blocked-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id, status=JobStatus.PROCESSING)
        await job_service.create_job(job=job)

        with pytest.raises(JobResubmitBlockedError):
            await job_service.resubmit_non_transcribed_sources(job_id=job.id)

    @pytest.mark.asyncio
    async def test_cancel_job_blocks_transcribed_terminal_jobs(
        self,
        job_service: JobService,
        document_service: DocumentService,
    ):
        document = Document(id=uuid4(), name="cancel-blocked-doc")
        await document_service.create_document(document=document)

        job = Job(document_id=document.id, status=JobStatus.TRANSCRIBED)
        await job_service.create_job(job=job)

        with pytest.raises(JobCancelBlockedError):
            await job_service.cancel_job(job_id=job.id)
