# #region Test.OrchestratorExec [C:3] [TYPE Module] [SEMANTICS test, translate, execution, run, engine]
# @BRIEF Tests for orchestrator_exec.py — TranslationExecutionEngine.
# @RELATION BINDS_TO -> [TranslationExecutionEngine]
# @TEST_CONTRACT: execute_run -> TranslationRun | dispatches executor, handles outcomes
# @TEST_CONTRACT: _init_language_stats -> dict | initializes per-language stats
# @TEST_EDGE: missing_job
# @TEST_EDGE: wrong_status
# @TEST_EDGE: executor_failure
# @TEST_EDGE: cancelled_run

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))

import os
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")

from unittest.mock import MagicMock, AsyncMock, patch

import pytest

from src.plugins.translate.orchestrator_exec import TranslationExecutionEngine


class TestTranslationExecutionEngine:
    """TranslationExecutionEngine — execute runs, handle outcomes."""

    def _make_engine(self):
        """Create engine with mocked deps."""
        db = MagicMock()
        config = MagicMock()
        event_log = MagicMock()
        engine = TranslationExecutionEngine(db, config, event_log, "user")
        return engine, db, config, event_log

    def _make_run(self, **kwargs):
        run = MagicMock()
        run.id = "run-1"
        run.job_id = "job-1"
        run.status = "PENDING"
        for k, v in kwargs.items():
            setattr(run, k, v)
        return run

    def _make_job(self, **kwargs):
        job = MagicMock()
        job.id = "job-1"
        job.target_languages = ["ru", "de"]
        job.target_dialect = "en"
        for k, v in kwargs.items():
            setattr(job, k, v)
        return job

    @pytest.mark.asyncio
    async def test_execute_run_success(self):
        """Happy path: run executed, success completion handled."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        job = self._make_job()
        db.query.return_value.filter.return_value.first.return_value = job

        with patch(
            "src.plugins.translate.orchestrator_exec.TranslationExecutor"
        ) as MockExecutor:
            mock_executor = MockExecutor.return_value
            mock_executor.execute_run = AsyncMock(return_value=run)

            with patch(
                "src.plugins.translate.orchestrator_exec._complete_success"
            ) as mock_success:
                mock_success.return_value = run
                result = await engine.execute_run(run)

        event_log.log_event.assert_called_once_with(
            job_id="job-1", run_id="run-1", event_type="TRANSLATION_PHASE_STARTED",
            payload={}, created_by="user",
        )
        mock_success.assert_called_once()

    @pytest.mark.asyncio
    async def test_execute_run_missing_job_raises(self):
        """Missing job raises ValueError."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        db.query.return_value.filter.return_value.first.return_value = None

        with pytest.raises(ValueError, match="Job 'job-1' not found"):
            await engine.execute_run(run)

    @pytest.mark.asyncio
    async def test_execute_run_wrong_status_raises(self):
        """Non-PENDING run raises ValueError."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run(status="RUNNING")
        job = self._make_job()
        db.query.return_value.filter.return_value.first.return_value = job

        with pytest.raises(ValueError, match="Cannot execute run in status 'RUNNING'"):
            await engine.execute_run(run)

    @pytest.mark.asyncio
    async def test_executor_failure_handled(self):
        """Executor exception triggers failure handler."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        job = self._make_job()
        db.query.return_value.filter.return_value.first.return_value = job

        with patch(
            "src.plugins.translate.orchestrator_exec.TranslationExecutor"
        ) as MockExecutor:
            mock_executor = MockExecutor.return_value
            mock_executor.execute_run = AsyncMock(side_effect=ValueError("LLM failed"))

            with patch(
                "src.plugins.translate.orchestrator_exec._handle_executor_failure"
            ) as mock_failure:
                mock_failure.return_value = run
                result = await engine.execute_run(run)

        mock_failure.assert_called_once()
        assert result == run

    @pytest.mark.asyncio
    async def test_cancelled_run_handled(self):
        """Run with CANCELLED status after executor triggers complete_cancelled."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        job = self._make_job()
        db.query.return_value.filter.return_value.first.return_value = job

        with patch(
            "src.plugins.translate.orchestrator_exec.TranslationExecutor"
        ) as MockExecutor:
            cancelled_run = self._make_run(status="CANCELLED")
            mock_executor = MockExecutor.return_value
            mock_executor.execute_run = AsyncMock(return_value=cancelled_run)

            with patch(
                "src.plugins.translate.orchestrator_exec._complete_cancelled"
            ) as mock_cancelled:
                mock_cancelled.return_value = cancelled_run
                result = await engine.execute_run(run)

        mock_cancelled.assert_called_once()
        assert result == cancelled_run

    @pytest.mark.asyncio
    async def test_executor_passed_progress_callback(self):
        """Progress callback and skip_insert passed to executor."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        job = self._make_job()
        db.query.return_value.filter.return_value.first.return_value = job

        progress_cb = MagicMock()

        with patch(
            "src.plugins.translate.orchestrator_exec.TranslationExecutor"
        ) as MockExecutor:
            mock_executor = MockExecutor.return_value
            mock_executor.execute_run = AsyncMock(return_value=run)

            with patch(
                "src.plugins.translate.orchestrator_exec._complete_success"
            ) as mock_success:
                mock_success.return_value = run
                result = await engine.execute_run(run, on_batch_progress=progress_cb, skip_insert=True)

        # Verify executor was created with callback
        MockExecutor.assert_called_once()
        _, kwargs = MockExecutor.call_args
        assert kwargs["on_batch_progress"] == progress_cb

    def test_init_language_stats(self):
        """_init_language_stats creates stats for each target language."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        job = self._make_job(target_languages=["ru", "de"])

        result = engine._init_language_stats(run, job)

        assert len(result) == 2
        assert "ru" in result
        assert "de" in result
        assert result["ru"].language_code == "ru"
        assert result["ru"].run_id == "run-1"
        assert db.add.call_count == 2
        db.flush.assert_called_once()

    def test_init_language_stats_string_languages(self):
        """String target_languages is converted to list."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        job = self._make_job(target_languages=None, target_dialect="fr")

        result = engine._init_language_stats(run, job)

        assert len(result) == 1
        assert "fr" in result

    def test_init_language_stats_default_fallback(self):
        """No target_languages and no target_dialect defaults to 'en'."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        job = self._make_job(target_languages=None, target_dialect=None)

        result = engine._init_language_stats(run, job)

        assert len(result) == 1
        assert "en" in result

    def test_init_language_stats_string_language_not_list(self):
        """String target_languages (not a list) is wrapped in [str()] (line 108)."""
        engine, db, config, event_log = self._make_engine()
        run = self._make_run()
        # target_languages as a plain string (not a list) — line 107 isinstance check fails -> line 108
        job = self._make_job(target_languages="ru")

        result = engine._init_language_stats(run, job)

        assert len(result) == 1
        assert "ru" in result
# #endregion Test.OrchestratorExec
