# #region Test.OrchestratorRetry [C:3] [TYPE Module] [SEMANTICS test, translate, retry, cancellation]
# @BRIEF Tests for orchestrator_retry.py — TranslationRunRetryManager.
# @RELATION BINDS_TO -> [TranslationRunRetryManager]
# @TEST_CONTRACT: retry_failed_batches -> TranslationRun | retries failed batches
# @TEST_CONTRACT: retry_insert -> TranslationRun | delegates to orchestrator_cancel.retry_insert
# @TEST_CONTRACT: cancel_run -> TranslationRun | delegates to orchestrator_cancel.cancel_run
# @TEST_EDGE: no_failed_batches
# @TEST_EDGE: empty_failed_records
# @TEST_EDGE: all_retry_successful

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_retry import TranslationRunRetryManager


class TestTranslationRunRetryManager:
    """TranslationRunRetryManager — retry and cancellation."""

    def _make_retry_mgr(self):
        """Create retry manager with mock deps."""
        db = MagicMock()
        config = MagicMock()
        event_log = MagicMock()
        mgr = TranslationRunRetryManager(db, config, event_log, "user")
        return mgr, db, config, event_log

    def _make_run(self, **kwargs):
        run = MagicMock()
        run.id = "run-1"
        run.job_id = "job-1"
        run.status = "COMPLETED"
        run.successful_records = 90
        run.failed_records = 5
        run.skipped_records = 5
        run.completed_at = None
        for k, v in kwargs.items():
            setattr(run, k, v)
        return run

    def _make_batch(self, batch_index: int, status: str = "FAILED"):
        batch = MagicMock()
        batch.id = f"batch-{batch_index}"
        batch.batch_index = batch_index
        batch.status = status
        return batch

    def _make_record(self, record_id: str, **kwargs):
        rec = MagicMock()
        rec.id = record_id
        rec.source_object_id = "row_1"
        rec.source_sql = "SELECT *"
        rec.source_object_name = "table"
        rec.status = "FAILED"
        for k, v in kwargs.items():
            setattr(rec, k, v)
        return rec

    @pytest.mark.asyncio
    async def test_retry_failed_batches_success(self):
        """Retry failed batches updates run stats."""
        mgr, db, config, event_log = self._make_retry_mgr()
        mgr.event_log = event_log
        run = self._make_run()
        job = MagicMock()
        job.id = "job-1"

        db.query.return_value.filter.return_value.first.side_effect = [run, job]

        batch1 = self._make_batch(1)
        failed_rec = self._make_record("rec-1")
        # Use side_effect on .all() to return different values per call
        db.query.return_value.filter.return_value.all.side_effect = [
            [batch1],      # 1st .all(): failed batches
            [failed_rec],  # 2nd .all(): failed records
        ]

        with patch(
            "src.plugins.translate.orchestrator_retry.TranslationExecutor"
        ) as MockExecutor:
            mock_executor = MockExecutor.return_value
            mock_executor._process_batch = AsyncMock(return_value={
                "successful": 3, "failed": 1, "skipped": 0,
            })

            result = await mgr.retry_failed_batches("run-1")

        # failed=5+1=6, successful=90+3=93 (both non-zero -> COMPLETED)
        assert result.status == "COMPLETED"
        assert result.successful_records >= 90
        assert result.failed_records >= 5
        assert result.completed_at is not None
        event_log.log_event.assert_any_call(
            job_id="job-1", run_id="run-1", event_type="RUN_RETRYING", payload={"batch_count": 1},
            created_by="user",
        )
        event_log.log_event.assert_any_call(
            job_id="job-1", run_id="run-1", event_type="RUN_COMPLETED",
            payload={"retry": True}, created_by="user",
        )

    @pytest.mark.asyncio
    async def test_retry_missing_run_raises(self):
        """Missing run raises ValueError."""
        mgr, db, config, event_log = self._make_retry_mgr()
        db.query.return_value.filter.return_value.first.return_value = None

        with pytest.raises(ValueError, match="Run 'bad-id' not found"):
            await mgr.retry_failed_batches("bad-id")

    @pytest.mark.asyncio
    async def test_retry_missing_job_raises(self):
        """Missing job raises ValueError."""
        mgr, db, config, event_log = self._make_retry_mgr()
        run = self._make_run()
        db.query.return_value.filter.return_value.first.side_effect = [run, None]

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

    @pytest.mark.asyncio
    async def test_no_failed_batches_raises(self):
        """No failed batches raises ValueError."""
        mgr, db, config, event_log = self._make_retry_mgr()
        run = self._make_run()
        job = MagicMock()
        job.id = "job-1"
        db.query.return_value.filter.return_value.first.side_effect = [run, job]
        db.query.return_value.filter.return_value.all.return_value = []

        with pytest.raises(ValueError, match="No failed batches found for run 'run-1'"):
            await mgr.retry_failed_batches("run-1")

    @pytest.mark.asyncio
    async def test_retry_skips_batches_with_no_failed_records(self):
        """Batch with no failed records is skipped."""
        mgr, db, config, event_log = self._make_retry_mgr()
        run = self._make_run()
        job = MagicMock()
        job.id = "job-1"
        db.query.return_value.filter.return_value.first.side_effect = [run, job]

        batch1 = self._make_batch(1)
        # Use .all.side_effect — don't overwrite .filter.return_value
        db.query.return_value.filter.return_value.all.side_effect = [
            [batch1],  # first .all(): failed batches
            [],        # second .all(): failed records
        ]

        with patch(
            "src.plugins.translate.orchestrator_retry.TranslationExecutor"
        ) as MockExecutor:
            mock_executor = MockExecutor.return_value
            mock_executor._process_batch = AsyncMock()

            result = await mgr.retry_failed_batches("run-1")

        # Since no records, _process_batch should NOT be called
        mock_executor._process_batch.assert_not_called()
        assert result.completed_at is not None

    @pytest.mark.asyncio
    async def test_retry_completed_without_errors(self):
        """All retries successful -> status COMPLETED."""
        mgr, db, config, event_log = self._make_retry_mgr()
        run = self._make_run(successful_records=90, failed_records=0, skipped_records=0)
        job = MagicMock()
        job.id = "job-1"
        db.query.return_value.filter.return_value.first.side_effect = [run, job]

        batch1 = self._make_batch(1)
        failed_rec = self._make_record("rec-1")
        db.query.return_value.filter.return_value.all.side_effect = [
            [batch1],    # failed batches
            [failed_rec],  # failed records
        ]

        with patch(
            "src.plugins.translate.orchestrator_retry.TranslationExecutor"
        ) as MockExecutor:
            mock_executor = MockExecutor.return_value
            mock_executor._process_batch = AsyncMock(return_value={
                "successful": 5, "failed": 0, "skipped": 0,
            })
            result = await mgr.retry_failed_batches("run-1")

        # failed=0+0=0 -> COMPLETED
        assert result.status == "COMPLETED"

    @pytest.mark.asyncio
    async def test_retry_all_failed(self):
        """All retries fail -> status FAILED."""
        mgr, db, config, event_log = self._make_retry_mgr()
        run = self._make_run(successful_records=0, failed_records=10, skipped_records=0)
        job = MagicMock()
        job.id = "job-1"
        db.query.return_value.filter.return_value.first.side_effect = [run, job]

        batch1 = self._make_batch(1)
        failed_rec = self._make_record("rec-1")
        # Use .all.side_effect — don't overwrite .filter.return_value
        db.query.return_value.filter.return_value.all.side_effect = [
            [batch1],      # failed batches
            [failed_rec],  # failed records
        ]

        with patch(
            "src.plugins.translate.orchestrator_retry.TranslationExecutor"
        ) as MockExecutor:
            mock_executor = MockExecutor.return_value
            mock_executor._process_batch = AsyncMock(return_value={
                "successful": 0, "failed": 5, "skipped": 0,
            })
            result = await mgr.retry_failed_batches("run-1")

        # failed=10+5=15 != 0, successful=0+0=0 == 0 -> FAILED
        assert result.status == "FAILED"

    @pytest.mark.asyncio
    async def test_retry_insert_delegates(self):
        """retry_insert delegates to orchestrator_cancel.retry_insert."""
        mgr, db, config, event_log = self._make_retry_mgr()

        with patch(
            "src.plugins.translate.orchestrator_retry._retry_insert"
        ) as mock_retry_insert:
            mock_retry_insert.return_value = MagicMock(id="run-1")
            result = await mgr.retry_insert("run-1")

        mock_retry_insert.assert_called_once_with(db, config, event_log, "user", "run-1")

    def test_cancel_run_delegates(self):
        """cancel_run delegates to orchestrator_cancel.cancel_run."""
        mgr, db, config, event_log = self._make_retry_mgr()

        with patch(
            "src.plugins.translate.orchestrator_retry._cancel_run"
        ) as mock_cancel:
            mock_cancel.return_value = MagicMock(id="run-1")
            result = mgr.cancel_run("run-1")

        mock_cancel.assert_called_once_with(db, event_log, "user", "run-1")
# #endregion Test.OrchestratorRetry
