# #region Test.TranslationExecutor [C:3] [TYPE Module] [SEMANTICS test, translate, executor, orchestration]
# @BRIEF Verify TranslationExecutor contracts — execute_run, _prepare_run, _process_batches, _finalize_run, delegation wrappers.
# @RELATION BINDS_TO -> [TranslationExecutor]
# @TEST_EDGE: missing_job -> Raises ValueError
# @TEST_EDGE: no_source_rows -> Run completes immediately
# @TEST_EDGE: cancellation_during_batch -> Run marked CANCELLED
# @TEST_EDGE: batch_insert_failure -> Non-fatal, continues
# @TEST_EDGE: all_failed -> Run marked FAILED
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import UTC, datetime

from src.models.translate import TranslationJob, TranslationRun, TranslationBatch
from src.plugins.translate.executor import TranslationExecutor
from src.plugins.translate._run_service import RunExecutionService

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="Executor 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]
    session.commit()
    return job


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

    def test_init(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config, current_user="test-user")
        assert executor.db is db_session
        assert executor.config_manager is config
        assert executor.current_user == "test-user"
        assert executor.on_batch_progress is None
        assert executor._run_service is not None

    def test_init_with_callback(self, db_session):
        config = MagicMock()
        callback = MagicMock()
        executor = TranslationExecutor(db_session, config, current_user="test", on_batch_progress=callback)
        assert executor.on_batch_progress is callback


class TestExecuteRun:
    """Verify execute_run full orchestration."""

    @pytest.mark.asyncio
    async def test_missing_job_raises_error(self, db_session):
        """Negative: run references nonexistent job."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        # Create a run with a valid FK-compatible job_id, then change it to nonexistent
        run = TranslationRun(id="bad-run", job_id=JOB_ID, status="PENDING")
        db_session.add(run)
        db_session.commit()
        # Now change the job_id to a nonexistent one to trigger the ValueError
        run.job_id = "nonexistent"

        with db_session.no_autoflush:
            with pytest.raises(ValueError, match="not found"):
                await executor.execute_run(run)

    @pytest.mark.asyncio
    async def test_no_source_rows_returns_early(self, db_with_run):
        """Edge: no source rows to translate."""
        session, run_id = db_with_run
        config = MagicMock()
        executor = TranslationExecutor(session, config)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()

        with patch.object(executor._run_service, '_fetch_source_rows',
                          new=AsyncMock(return_value=[])):
            result = await executor.execute_run(run)
            assert result.status == "COMPLETED"

    @pytest.mark.asyncio
    async def test_happy_path_full_translation(self, db_with_run):
        """Happy: full translation flow with batches."""
        session, run_id = db_with_run
        config = MagicMock()
        executor = TranslationExecutor(session, config)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.config_snapshot = {"full_translation": True}

        source_rows = [
            {"row_index": "0", "source_text": "Hello", "source_data": {"table": "t"}},
            {"row_index": "1", "source_text": "World", "source_data": {"table": "t"}},
        ]

        with patch.object(executor._run_service, '_fetch_source_rows',
                          new=AsyncMock(return_value=source_rows)):
            with patch.object(executor, '_process_batch',
                              new=AsyncMock(return_value={
                                  "successful": 2, "failed": 0, "skipped": 0,
                                  "retries": 0, "cache_hits": 0, "batch_id": "batch-1",
                              })):
                with patch.object(executor, '_insert_batch_to_target',
                                  new=AsyncMock(return_value=None)):
                    result = await executor.execute_run(run)
                    assert result.status == "COMPLETED"
                    assert result.successful_records == 2

    @pytest.mark.asyncio
    async def test_new_key_only_filtering(self, db_with_run):
        """Edge: new_key_only trigger filters existing keys."""
        session, run_id = db_with_run
        config = MagicMock()
        executor = TranslationExecutor(session, config)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.trigger_type = "new_key_only"

        with patch.object(executor._run_service, '_fetch_source_rows',
                          new=AsyncMock(return_value=[{"row_index": "0", "source_text": "Hello"}])):
            with patch.object(executor._run_service, '_filter_new_keys',
                              return_value=[]):
                result = await executor.execute_run(run)
                assert result.status == "COMPLETED"
                assert result.insert_status == "skipped"

    @pytest.mark.asyncio
    async def test_cancellation_during_batches(self, db_with_run):
        """Edge: cancel requested during batch processing."""
        session, run_id = db_with_run
        config = MagicMock()
        executor = TranslationExecutor(session, config)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.config_snapshot = {"full_translation": True}

        source_rows = [
            {"row_index": "0", "source_text": "Hello", "source_data": {"table": "t"}},
        ]

        with patch.object(executor._run_service, '_fetch_source_rows',
                          new=AsyncMock(return_value=source_rows)):
            with patch.object(executor, '_process_batch',
                              new=AsyncMock(return_value={
                                  "successful": 1, "failed": 0, "skipped": 0,
                                  "retries": 0, "cache_hits": 0, "batch_id": "batch-1",
                              })):
                with patch.object(executor, '_insert_batch_to_target',
                                  new=AsyncMock(return_value=None)):
                    run.error_message = "CANCEL_REQUESTED"
                    result = await executor.execute_run(run)
                    assert result.status == "CANCELLED"


class TestPrepareRun:
    """Verify _prepare_run."""

    @pytest.mark.asyncio
    async def test_prepare_run_sets_running(self, db_with_run):
        session, run_id = db_with_run
        config = MagicMock()
        executor = TranslationExecutor(session, config)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()

        with patch.object(executor._run_service, '_fetch_source_rows',
                          new=AsyncMock(return_value=[{"row_index": "0", "source_text": "Hello"}])):
            result_run, rows, result_job = await executor._prepare_run(run, job)
            assert result_run.status == "RUNNING"
            assert len(rows) == 1


class TestProcessBatches:
    """Verify _process_batches."""

    @pytest.mark.asyncio
    async def test_batch_insert_failure_non_fatal(self, db_with_run):
        """Edge: batch insert fails but processing continues."""
        session, run_id = db_with_run
        config = MagicMock()
        executor = TranslationExecutor(session, config)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()

        batches = [
            [{"row_index": "0", "source_text": "Hello", "source_data": {"table": "t"}}],
        ]

        with patch.object(executor, '_process_batch',
                          new=AsyncMock(return_value={
                              "successful": 1, "failed": 0, "skipped": 0,
                              "retries": 0, "cache_hits": 0, "batch_id": "batch-1",
                          })):
            with patch.object(executor, '_insert_batch_to_target',
                              new=AsyncMock(side_effect=ValueError("DB timeout"))):
                # Should not raise — insert failure is non-fatal
                result = await executor._process_batches(run, job, batches, ["fr"])
                assert result.status is not None

    @pytest.mark.asyncio
    async def test_progress_callback_called(self, db_with_run):
        """Verify on_batch_progress callback."""
        session, run_id = db_with_run
        config = MagicMock()
        callback = MagicMock()
        executor = TranslationExecutor(session, config, on_batch_progress=callback)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()

        batches = [
            [{"row_index": "0", "source_text": "Hello", "source_data": {"table": "t"}}],
        ]

        with patch.object(executor, '_process_batch',
                          new=AsyncMock(return_value={
                              "successful": 1, "failed": 0, "skipped": 0,
                              "retries": 0, "cache_hits": 0, "batch_id": "batch-1",
                          })):
            with patch.object(executor, '_insert_batch_to_target',
                              new=AsyncMock(return_value=None)):
                await executor._process_batches(run, job, batches, ["fr"])
                callback.assert_called_once()


class TestFinalizeRun:
    """Verify _finalize_run."""

    def test_all_success(self, db_session):
        run = TranslationRun(id="r1", job_id=JOB_ID, status="RUNNING")
        result = TranslationExecutor._finalize_run(run, 10, 0, 0)
        assert result.status == "COMPLETED"

    def test_all_failed(self, db_session):
        run = TranslationRun(id="r1", job_id=JOB_ID, status="RUNNING")
        result = TranslationExecutor._finalize_run(run, 0, 5, 0)
        assert result.status == "FAILED"
        assert "All 5 record(s) failed" in result.error_message

    def test_mixed_failed(self, db_session):
        run = TranslationRun(id="r1", job_id=JOB_ID, status="RUNNING")
        result = TranslationExecutor._finalize_run(run, 8, 2, 0)
        assert result.status == "COMPLETED"


class TestDelegationWrappers:
    """Verify delegation wrapper methods (lazy imports patched at source)."""

    @pytest.mark.asyncio
    async def test_fetch_source_rows_delegates(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        with patch.object(executor._run_service, '_fetch_source_rows',
                          new=AsyncMock(return_value=[{"row_index": "0"}])):
            result = await executor._fetch_source_rows("job-1", "run-1")
            assert result == [{"row_index": "0"}]

    def test_filter_new_keys_delegates(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        rows = [{"row_index": "0"}]
        with patch.object(executor._run_service, '_filter_new_keys',
                          return_value=[]):
            result = executor._filter_new_keys(MagicMock(), "run-1", rows)
            assert result == []

    def test_extract_chart_data_rows_static(self):
        rows = TranslationExecutor._extract_chart_data_rows({"data": [{"id": 1}]})
        assert rows == [{"id": 1}]

    def test_load_preview_edits(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        with patch.object(executor._run_service, '_load_preview_edits') as mock_load:
            executor._load_preview_edits("job-1")
            mock_load.assert_called_once_with("job-1")

    def test_compute_key_hash_static(self):
        h = TranslationExecutor._compute_key_hash({"table": "test"})
        assert isinstance(h, str)
        assert len(h) > 0

    def test_resolve_provider_model_with_provider(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = "test-provider"
        with patch('src.plugins.translate.executor.LLMProviderService') as MockSvc:
            mock_instance = MockSvc.return_value
            mock_instance.get_provider.return_value = MagicMock(default_model="gpt-4")
            result = executor._resolve_provider_model(job)
            assert result == "gpt-4"

    def test_resolve_provider_model_no_provider(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = None
        result = executor._resolve_provider_model(job)
        assert result is None

    def test_resolve_provider_model_exception(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = "test-provider"
        with patch('src.plugins.translate.executor.LLMProviderService') as MockSvc:
            mock_instance = MockSvc.return_value
            mock_instance.get_provider.side_effect = ValueError("DB error")
            result = executor._resolve_provider_model(job)
            assert result is None

    def test_resolve_provider_config(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = "test-provider"
        with patch('src.plugins.translate.executor.LLMProviderService') as MockSvc:
            mock_instance = MockSvc.return_value
            mock_instance.get_provider_token_config.return_value = {
                "model": "gpt-4", "context_window": 8192, "max_output_tokens": 4096,
            }
            result = executor._resolve_provider_config(job)
            assert result["model"] == "gpt-4"

    def test_resolve_provider_config_no_provider(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = None
        result = executor._resolve_provider_config(job)
        assert result == {"model": None, "context_window": None, "max_output_tokens": None}

    def test_resolve_provider_model_alias(self, db_session):
        """resolve_provider_model backward compat alias."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = "test-provider"
        with patch.object(executor, '_resolve_provider_model', return_value="gpt-4"):
            result = executor.resolve_provider_model(job)
            assert result == "gpt-4"

    def test_auto_size_batches(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = "test-provider"
        with patch('src.plugins.translate._batch_sizer.AdaptiveBatchSizer') as MockSizer:
            mock_instance = MockSizer.return_value
            mock_instance.auto_size_batches.return_value = [{"rows": ["a"]}]
            result = executor._auto_size_batches(job, ["a"], ["fr"], "gpt-4")
            assert len(result) == 1

    @pytest.mark.asyncio
    async def test_process_batch_delegates(self, db_session):
        """Patch at source since executor uses lazy import inside method."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        with patch('src.plugins.translate._batch_proc.BatchProcessingService') as MockBPS:
            mock_instance = MockBPS.return_value
            mock_instance.process_batch = AsyncMock(return_value={"successful": 1})
            result = await executor._process_batch(
                MagicMock(), "run-1", 0, [{"row_index": "0"}], "hash1", "hash2",
            )
            assert result["successful"] == 1

    @pytest.mark.asyncio
    async def test_insert_batch_to_target_delegates(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        with patch('src.plugins.translate._batch_proc.BatchProcessingService') as MockBPS:
            mock_instance = MockBPS.return_value
            mock_instance.insert_batch_to_target = AsyncMock(return_value=None)
            await executor._insert_batch_to_target(MagicMock(), "batch-1", "run-1")

    @pytest.mark.asyncio
    async def test_call_llm_for_batch_delegates(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        with patch('src.plugins.translate._llm_call.LLMTranslationService') as MockLLM:
            mock_instance = MockLLM.return_value
            mock_instance.call_llm_for_batch = AsyncMock(return_value={"successful": 1})
            result = await executor._call_llm_for_batch(
                MagicMock(), "run-1", [], [], "batch-1",
            )
            assert result["successful"] == 1

    @pytest.mark.asyncio
    async def test_call_llm_delegates(self, db_session):
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        with patch('src.plugins.translate._llm_call.LLMTranslationService') as MockLLM:
            mock_instance = MockLLM.return_value
            mock_instance.call_llm = AsyncMock(return_value=("response", "stop"))
            result = await executor._call_llm(MagicMock(), "prompt")
            assert result == ("response", "stop")

    @pytest.mark.asyncio
    async def test_call_openai_compatible_static(self):
        with patch('src.plugins.translate._llm_call.LLMTranslationService') as MockLLM:
            MockLLM.call_openai_compatible = AsyncMock(return_value=("response", "stop"))
            result = await TranslationExecutor._call_openai_compatible(
                "url", "key", "model", "prompt",
            )
            assert result == ("response", "stop")

    def test_parse_llm_response_static(self):
        with patch('src.plugins.translate._llm_call.LLMTranslationService') as MockLLM:
            MockLLM._parse_llm_response.return_value = {"0": {"fr": "test"}}
            result = TranslationExecutor._parse_llm_response('{"fr": "test"}', 1, ["fr"])
            assert result == {"0": {"fr": "test"}}


class TestPrepareBatches:
    """Verify _prepare_batches edge cases."""

    def test_target_languages_not_list(self, db_session):
        """Edge: target_languages is a single string, not list -> wrapped into list."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock(spec=TranslationJob)
        job.target_languages = "fr"  # single string, not list
        job.target_dialect = "fr"

        with patch.object(executor, '_auto_size_batches', return_value=[]):
            tls, batches = executor._prepare_batches(job, [], "run-1")

        assert tls == ["fr"]
        assert batches == []

    def test_target_languages_none_uses_dialect(self, db_session):
        """Edge: target_languages is None, falls back to target_dialect."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock(spec=TranslationJob)
        job.target_languages = None
        job.target_dialect = "de"

        with patch.object(executor, '_auto_size_batches', return_value=[]):
            tls, batches = executor._prepare_batches(job, [], "run-1")

        assert tls == ["de"]


class TestProcessBatchesEdgeCases:
    """Verify _process_batches edge cases."""

    @pytest.mark.asyncio
    async def test_language_stats_update_error_non_fatal(self, db_with_run):
        """Edge: language stats update fails, processing continues."""
        session, run_id = db_with_run
        config = MagicMock()
        executor = TranslationExecutor(session, config)
        job = _make_job(session)
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()

        batches = [
            [{"row_index": "0", "source_text": "Hello", "source_data": {"table": "t"}}],
        ]
        language_stats_map = {"fr": MagicMock()}

        with patch.object(executor, '_process_batch',
                          new=AsyncMock(return_value={
                              "successful": 1, "failed": 0, "skipped": 0,
                              "retries": 0, "cache_hits": 0, "batch_id": "batch-1",
                          })):
            with patch.object(executor, '_insert_batch_to_target',
                              new=AsyncMock(return_value=None)):
                with patch.object(executor, '_update_language_stats_incremental',
                                  side_effect=ValueError("Stats update failed")):
                    # Should not raise — stats failure is non-fatal
                    result = await executor._process_batches(
                        run, job, batches, ["fr"], language_stats_map,
                    )
                    assert result.status is not None


class TestDelegationWrappersExtra:
    """Additional delegation wrapper tests for uncovered lines."""

    def test_update_language_stats_incremental_delegates(self, db_session):
        """Verify _update_language_stats_incremental delegates to _run_service."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        stats_map = {"fr": MagicMock()}

        with patch.object(executor._run_service, '_update_language_stats_incremental') as mock_update:
            executor._update_language_stats_incremental("run-1", stats_map)
            mock_update.assert_called_once_with("run-1", stats_map)

    def test_resolve_provider_config_exception(self, db_session):
        """Edge: LLMProviderService raises, returns safe defaults."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = "test-provider"

        with patch('src.plugins.translate.executor.LLMProviderService') as MockSvc:
            mock_instance = MockSvc.return_value
            mock_instance.get_provider_token_config.side_effect = ValueError("DB error")
            result = executor._resolve_provider_config(job)
            assert result == {"model": None, "context_window": None, "max_output_tokens": None}

    def test_resolve_provider_config_exception_no_provider(self, db_session):
        """Edge: provider_id is None, returns safe defaults directly."""
        config = MagicMock()
        executor = TranslationExecutor(db_session, config)
        job = MagicMock()
        job.provider_id = None
        result = executor._resolve_provider_config(job)
        assert result == {"model": None, "context_window": None, "max_output_tokens": None}
# #endregion Test.TranslationExecutor
