# #region Test.BatchProcessingService [C:3] [TYPE Module] [SEMANTICS test, batch, process, classify, llm]
# @BRIEF Verify BatchProcessingService contracts — process_batch, _classify, _detect_languages, _check_cache, _persist_pre.
# @RELATION BINDS_TO -> [BatchProcessingService]
# @TEST_EDGE: empty_batch -> Returns zeros
# @TEST_EDGE: all_same_language -> No LLM call, all in pre_rows
# @TEST_EDGE: all_cached -> No LLM call
# @TEST_EDGE: preview_edits_cache -> Uses approved translation
# @TEST_EDGE: llm_rows -> Calls LLM service
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from src.models.translate import (
    TranslationBatch, TranslationJob, TranslationLanguage,
    TranslationRecord,
)
from src.plugins.translate._batch_proc import BatchProcessingService

from .conftest import JOB_ID, RUN_ID


def _make_job(session, target_langs=None):
    job = session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).first()
    if job is None:
        job = TranslationJob(
            id=JOB_ID, name="Batch Proc Test", source_dialect="en",
            target_dialect=target_langs[0] if target_langs else "fr",
            target_languages=target_langs or ["fr"],
            status="ACTIVE", provider_id="test-provider",
        )
        session.add(job)
    else:
        if target_langs:
            job.target_languages = target_langs
            job.target_dialect = target_langs[0]
        # Ensure provider_id is always set so _process_llm enters the try/except block
        job.provider_id = "test-provider"
    session.commit()
    return job


def _batch_rows(count=2, detected_lang="en"):
    return [
        {
            "row_index": str(i),
            "source_text": f"Hello world {i}",
            "source_data": {"table": "test", "row_id": i},
            "_detected_lang": detected_lang,
        }
        for i in range(count)
    ]


class TestInit:
    """Verify initialization."""

    def test_init(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        assert svc.db is db_session
        assert svc.config_manager is config
        assert svc._llm_service is not None


class TestCreateBatch:
    """Verify _create_batch."""

    def test_creates_batch_record(self, db_with_run):
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        rows = _batch_rows(3)
        batch = svc._create_batch(run_id, 0, rows)
        assert batch.run_id == run_id
        assert batch.batch_index == 0
        assert batch.total_records == 3
        assert batch.status == "RUNNING"
        fetched = session.query(TranslationBatch).filter(
            TranslationBatch.id == batch.id
        ).first()
        assert fetched is not None


class TestDetectLanguages:
    """Verify _detect_languages."""

    def test_detects_languages(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = _batch_rows(2)
        with patch('src.plugins.translate._batch_proc.batch_detect',
                   return_value=["en", "fr"]):
            svc._detect_languages(rows, ["fr", "de"])
            assert rows[0]["_detected_lang"] == "en"
            assert rows[1]["_detected_lang"] == "fr"


class TestCheckCache:
    """Verify _check_cache."""

    def test_cache_miss(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        job = _make_job(db_session)
        rows = _batch_rows(1)
        with patch('src.plugins.translate._batch_proc._check_translation_cache',
                   return_value=None):
            svc._check_cache(job, rows, "hash1", "hash2")
            assert "_cached_lang_values" not in rows[0]
            assert "_source_hash" in rows[0]

    def test_cache_hit(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        job = _make_job(db_session)
        rows = _batch_rows(1)
        with patch('src.plugins.translate._batch_proc._check_translation_cache',
                   return_value={"fr": "Bonjour"}):
            svc._check_cache(job, rows, "hash1", "hash2")
            assert rows[0]["_cached_lang_values"] == {"fr": "Bonjour"}

    def test_approved_translation_skips_cache(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        job = _make_job(db_session)
        rows = [{"row_index": "0", "source_text": "hello", "approved_translation": "Bonjour"}]
        svc._check_cache(job, rows, "hash1", "hash2")
        assert "_cached_lang_values" not in rows[0]


class TestClassify:
    """Verify _classify rows into llm_rows and pre_rows."""

    def test_same_language_shortcut(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = _batch_rows(1, detected_lang="fr")
        llm_rows, pre_rows = svc._classify(rows, None, ["fr"])
        assert len(pre_rows) == 1
        assert len(llm_rows) == 0
        assert rows[0].get("_same_language") is True

    def test_partial_language_match_still_llm(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = _batch_rows(1, detected_lang="fr")
        llm_rows, pre_rows = svc._classify(rows, None, ["fr", "de"])
        assert len(pre_rows) == 0
        assert len(llm_rows) == 1

    def test_approved_translation_pre_row(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = [{"row_index": "0", "source_text": "hello",
                 "_detected_lang": "en", "approved_translation": "Bonjour"}]
        llm_rows, pre_rows = svc._classify(rows, None, ["fr"])
        assert len(pre_rows) == 1
        assert len(llm_rows) == 0

    def test_cached_values_pre_row(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = [{"row_index": "0", "source_text": "hello",
                 "_detected_lang": "en",
                 "_cached_lang_values": {"fr": "Bonjour"}}]
        llm_rows, pre_rows = svc._classify(rows, None, ["fr"])
        assert len(pre_rows) == 1
        assert len(llm_rows) == 0

    def test_preview_edits_cache(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = [{"row_index": "0", "source_text": "hello", "_detected_lang": "en",
                 "source_data": {"table": "test", "key": "val"}}]
        preview_cache = {"hash_of_val": {"fr": "Bonjour"}}
        with patch('src.plugins.translate._batch_proc._compute_key_hash',
                   return_value="hash_of_val"):
            llm_rows, pre_rows = svc._classify(rows, preview_cache, ["fr"])
            assert len(pre_rows) == 1
            assert len(llm_rows) == 0
            assert rows[0].get("approved_translation") is not None

    def test_llm_row_default(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = _batch_rows(1, detected_lang="en")
        llm_rows, pre_rows = svc._classify(rows, None, ["fr"])
        assert len(pre_rows) == 0
        assert len(llm_rows) == 1

    def test_und_language_goes_to_llm(self, db_session):
        config = MagicMock()
        svc = BatchProcessingService(db_session, config)
        rows = [{"row_index": "0", "source_text": "hello", "_detected_lang": "und"}]
        llm_rows, pre_rows = svc._classify(rows, None, ["fr"])
        assert len(llm_rows) == 1


class TestPersistPre:
    """Verify _persist_pre."""

    def test_persist_same_language(self, db_with_run):
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        rows = [{"row_index": "0", "source_text": "Bonjour le monde",
                 "_detected_lang": "fr", "_same_language": True}]
        count = svc._persist_pre(rows, "batch-1", run_id, ["fr", "de"])
        assert count == 1
        records = session.query(TranslationRecord).all()
        assert len(records) == 1
        assert records[0].target_sql == "Bonjour le monde"
        langs = session.query(TranslationLanguage).all()
        lang_codes = [l.language_code for l in langs]
        assert "fr" in lang_codes
        assert "de" not in lang_codes

    def test_persist_cached_values(self, db_with_run):
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        rows = [{"row_index": "0", "source_text": "hello",
                 "_detected_lang": "en",
                 "_cached_lang_values": {"fr": "Bonjour", "de": "Hallo"}}]
        count = svc._persist_pre(rows, "batch-1", run_id, ["fr", "de"])
        assert count == 1
        langs = session.query(TranslationLanguage).all()
        lang_dict = {l.language_code: l.final_value for l in langs}
        assert lang_dict["fr"] == "Bonjour"
        assert lang_dict["de"] == "Hallo"

    def test_persist_approved_translation(self, db_with_run):
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        rows = [{"row_index": "0", "source_text": "hello",
                 "_detected_lang": "en",
                 "approved_translation": "Bonjour le monde"}]
        count = svc._persist_pre(rows, "batch-1", run_id, ["fr"])
        assert count == 1
        records = session.query(TranslationRecord).all()
        assert records[0].target_sql == "Bonjour le monde"


class TestProcessLlm:
    """Verify _process_llm."""

    @pytest.mark.asyncio
    async def test_process_llm_calls_service(self, db_with_run):
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session)
        rows = _batch_rows(1)

        with patch('src.plugins.translate._batch_proc.LLMProviderService') as MockProv:
            mock_prov = MockProv.return_value
            mock_prov.get_provider_token_config.return_value = {
                "model": "gpt-4", "context_window": 8192, "max_output_tokens": 4096,
            }
            with patch('src.plugins.translate._batch_proc.estimate_token_budget',
                       return_value={
                           "max_output_needed": 1000, "max_rows_by_output": 10,
                           "estimated_input_tokens": 500, "warning": None,
                       }):
                with patch.object(svc._llm_service, 'call_llm_for_batch',
                                  new=AsyncMock(return_value={"successful": 1, "failed": 0,
                                                                "skipped": 0, "retries": 0})):
                    result = await svc._process_llm(
                        job, run_id, rows, [], "batch-1", ["fr"],
                    )
                    assert result["successful"] == 1

    @pytest.mark.asyncio
    async def test_process_llm_token_warning(self, db_with_run):
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session)
        rows = _batch_rows(1)

        with patch('src.plugins.translate._batch_proc.LLMProviderService') as MockProv:
            mock_prov = MockProv.return_value
            mock_prov.get_provider_token_config.return_value = {
                "model": "gpt-4", "context_window": 100, "max_output_tokens": 50,
            }
            with patch('src.plugins.translate._batch_proc.estimate_token_budget',
                       return_value={
                           "max_output_needed": 2000, "max_rows_by_output": 1,
                           "estimated_input_tokens": 5000, "warning": "High token usage",
                       }):
                with patch.object(svc._llm_service, 'call_llm_for_batch',
                                  new=AsyncMock(return_value={"successful": 1, "failed": 0,
                                                                "skipped": 0, "retries": 0})):
                    result = await svc._process_llm(
                        job, run_id, rows, [], "batch-1", ["fr"],
                    )
                    assert result["successful"] == 1


class TestProcessBatch:
    """Verify process_batch orchestration."""

    @pytest.mark.asyncio
    async def test_full_batch_flow(self, db_with_run):
        """Happy: full process_batch flow with LLM calls."""
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session, target_langs=["fr"])
        rows = _batch_rows(2, detected_lang="en")

        with patch.object(svc, '_process_llm',
                          new=AsyncMock(return_value={"successful": 2, "failed": 0,
                                                       "skipped": 0, "retries": 0})):
            with patch('src.plugins.translate._batch_proc.DictionaryManager.filter_for_batch',
                       return_value=[]):
                result = await svc.process_batch(
                    job=job, run_id=run_id, batch_index=0, batch_rows=rows,
                )
                assert result["successful"] == 2
                assert "batch_id" in result
                batch = session.query(TranslationBatch).filter(
                    TranslationBatch.id == result["batch_id"]
                ).first()
                assert batch is not None
                assert batch.status == "COMPLETED"
                assert batch.successful_records == 2

    @pytest.mark.asyncio
    async def test_batch_all_same_language(self, db_with_run):
        """All rows are same-language — no LLM calls."""
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session, target_langs=["fr"])
        rows = _batch_rows(2, detected_lang="fr")

        # Mock _detect_languages to preserve pre-set _detected_lang values
        with patch.object(svc, '_detect_languages', side_effect=lambda rows, tls: None):
            result = await svc.process_batch(
                job=job, run_id=run_id, batch_index=0, batch_rows=rows,
            )
        assert result["successful"] == 2
        assert result.get("cache_hits", 0) == 0
        records = session.query(TranslationRecord).all()
        assert len(records) == 2

    @pytest.mark.asyncio
    async def test_batch_with_errors(self, db_with_run):
        """Some LLM rows fail."""
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session, target_langs=["fr"])
        rows = _batch_rows(2, detected_lang="en")

        with patch.object(svc, '_process_llm',
                          new=AsyncMock(return_value={"successful": 0, "failed": 2,
                                                       "skipped": 0, "retries": 3})):
            with patch('src.plugins.translate._batch_proc.DictionaryManager.filter_for_batch',
                       return_value=[]):
                result = await svc.process_batch(
                    job=job, run_id=run_id, batch_index=0, batch_rows=rows,
                )
                assert result["failed"] == 2
                batch = session.query(TranslationBatch).filter(
                    TranslationBatch.id == result["batch_id"]
                ).first()
                assert batch.status == "COMPLETED_WITH_ERRORS"

    @pytest.mark.asyncio
    async def test_batch_with_cache_hits(self, db_with_run):
        """Cache hits counted."""
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session, target_langs=["fr"])
        rows = _batch_rows(2, detected_lang="en")
        for r in rows:
            r["_cached_lang_values"] = {"fr": "Bonjour"}

        result = await svc.process_batch(
            job=job, run_id=run_id, batch_index=0, batch_rows=rows,
        )
        assert result["successful"] == 2
        assert result.get("cache_hits", 0) == 2


class TestProcessLlmProviderEdgeCases:
    """Cover provider_id edge cases in _process_llm."""

    @pytest.mark.asyncio
    async def test_process_llm_no_provider_id(self, db_with_run):
        """Edge: job has no provider_id, uses default token config."""
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session)
        job.provider_id = None  # Override to test the no-provider path
        rows = _batch_rows(1)

        with patch('src.plugins.translate._batch_proc.estimate_token_budget',
                   return_value={
                       "max_output_needed": 1000, "max_rows_by_output": 10,
                       "estimated_input_tokens": 500, "warning": None,
                   }):
            with patch.object(svc._llm_service, 'call_llm_for_batch',
                              new=AsyncMock(return_value={"successful": 1, "failed": 0,
                                                           "skipped": 0, "retries": 0})):
                result = await svc._process_llm(
                    job, run_id, rows, [], "batch-1", ["fr"],
                )
                assert result["successful"] == 1

    @pytest.mark.asyncio
    async def test_process_llm_provider_exception(self, db_with_run):
        """Edge: LLMProviderService raises in provider resolution."""
        session, run_id = db_with_run
        config = MagicMock()
        svc = BatchProcessingService(session, config)
        job = _make_job(session)
        rows = _batch_rows(1)

        with patch('src.plugins.translate._batch_proc.LLMProviderService') as MockProv:
            mock_instance = MockProv.return_value
            mock_instance.get_provider_token_config.side_effect = RuntimeError("API error")
            with patch('src.plugins.translate._batch_proc.estimate_token_budget',
                       return_value={
                           "max_output_needed": 1000, "max_rows_by_output": 10,
                           "estimated_input_tokens": 500, "warning": None,
                       }):
                with patch.object(svc._llm_service, 'call_llm_for_batch',
                                  new=AsyncMock(return_value={"successful": 1, "failed": 0,
                                                               "skipped": 0, "retries": 0})):
                    result = await svc._process_llm(
                        job, run_id, rows, [], "batch-1", ["fr"],
                    )
                    assert result["successful"] == 1
# #endregion Test.BatchProcessingService
