# #region Test.OrchestratorCancel [C:3] [TYPE Module] [SEMANTICS test, translate, cancel, retry]
# @BRIEF Verify orchestrator_cancel contracts — cancel_run and retry_insert.
# @RELATION BINDS_TO -> [orchestrator_cancel]
# @TEST_EDGE: run_not_found -> cancel_run raises ValueError
# @TEST_EDGE: invalid_status -> cancel_run raises ValueError for COMPLETED/FAILED runs
# @TEST_EDGE: lock_timeout -> cancel_run falls back to _fallback_cancel_request
# @TEST_EDGE: retry_insert_run_not_found -> retry_insert raises ValueError
# @TEST_EDGE: retry_insert_job_not_found -> retry_insert raises ValueError
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import UTC, datetime
import uuid

from sqlalchemy.orm import Session

from src.models.translate import (
    TranslationJob, TranslationRun, TranslationEvent,
)
from src.plugins.translate.events import TranslationEventLog

from .conftest import JOB_ID, RUN_ID


class _MockExecute:
    """Helper: patch session.execute so SET LOCAL succeeds for SQLite compat."""

    @staticmethod
    def _make_passthrough(session):
        """Return a side_effect that passes thru SET LOCAL, delegates real SQL."""
        real_execute = session.execute

        def passthrough(statement, *args, **kwargs):
            sql_str = str(statement)
            if 'SET LOCAL' in sql_str or 'lock_timeout' in sql_str:
                return  # silently pass PostgreSQL-specific SET LOCAL
            return real_execute(statement, *args, **kwargs)

        return passthrough


class TestCancelRun:
    """Verify cancel_run function."""

    def _patch_set_local(self, session):
        """Patch session.execute so SET LOCAL succeeds."""
        return patch.object(session, 'execute',
                            side_effect=_MockExecute._make_passthrough(session))

    def test_cancel_run_success(self, db_with_run):
        """Happy path: cancel a RUNNING run."""
        session, run_id = db_with_run
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.status = "RUNNING"
        session.commit()
        event_log = TranslationEventLog(session)

        from src.plugins.translate.orchestrator_cancel import cancel_run
        with self._patch_set_local(session):
            result = cancel_run(session, event_log, "test_user", run_id)

        assert result.status == "CANCELLED"
        assert result.completed_at is not None

        events = session.query(TranslationEvent).filter(
            TranslationEvent.run_id == run_id
        ).all()
        assert any(e.event_type == "RUN_CANCELLED" for e in events)

    def test_cancel_run_not_found(self, db_session):
        """Negative: run_id does not exist."""
        event_log = TranslationEventLog(db_session)
        from src.plugins.translate.orchestrator_cancel import cancel_run

        with pytest.raises(ValueError, match="not found"):
            with self._patch_set_local(db_session):
                cancel_run(db_session, event_log, "test_user", "nonexistent-id")

    def test_cancel_run_invalid_status(self, db_with_run):
        """Negative: cannot cancel COMPLETED run."""
        session, run_id = db_with_run
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.status = "COMPLETED"
        session.commit()
        event_log = TranslationEventLog(session)

        from src.plugins.translate.orchestrator_cancel import cancel_run
        with pytest.raises(ValueError, match="Cannot cancel"):
            with self._patch_set_local(session):
                cancel_run(session, event_log, "test_user", run_id)

    def test_cancel_run_fail_status(self, db_with_run):
        """Negative: cannot cancel FAILED run."""
        session, run_id = db_with_run
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.status = "FAILED"
        session.commit()
        event_log = TranslationEventLog(session)

        from src.plugins.translate.orchestrator_cancel import cancel_run
        with pytest.raises(ValueError, match="Cannot cancel"):
            with self._patch_set_local(session):
                cancel_run(session, event_log, "test_user", run_id)

    def test_cancel_run_lock_timeout_fallback(self, db_with_run):
        """Edge: lock timeout on execute -> _fallback_cancel_request."""
        session, run_id = db_with_run
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.status = "RUNNING"
        session.commit()
        event_log = TranslationEventLog(session)

        from src.plugins.translate.orchestrator_cancel import cancel_run

        _exec_count = [0]
        real_exec = session.execute

        def _fail_first_then_real(statement, *args, **kwargs):
            _exec_count[0] += 1
            if _exec_count[0] == 1:
                raise Exception("lock timeout")
            return real_exec(statement, *args, **kwargs)

        with patch.object(session, 'execute', side_effect=_fail_first_then_real):
            result = cancel_run(session, event_log, "test_user", run_id)
            assert result is not None

    def test_cancel_run_flush_fallback(self, db_with_run):
        """Edge: flush failure triggers _fallback_cancel_request."""
        session, run_id = db_with_run
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.status = "RUNNING"
        session.commit()
        event_log = TranslationEventLog(session)

        from src.plugins.translate.orchestrator_cancel import cancel_run

        _flush_count = [0]
        def _fail_once():
            _flush_count[0] += 1
            if _flush_count[0] == 1:
                raise Exception("flush failed")

        with self._patch_set_local(session), patch.object(session, 'flush', side_effect=_fail_once):
            result = cancel_run(session, event_log, "test_user", run_id)
            assert result is not None

    def test_cancel_run_fallback_no_run_found(self):
        """_fallback_cancel_request with nonexistent run raises ValueError."""
        session, engine = None, None
        from sqlalchemy import create_engine, event
        from sqlalchemy.orm import sessionmaker
        from src.plugins.translate.orchestrator_cancel import _fallback_cancel_request

        from src.models.translate import Base
        engine = create_engine("sqlite:///:memory:")
        Base.metadata.create_all(bind=engine)
        session = sessionmaker(bind=engine)()
        try:
            with pytest.raises(ValueError, match="not found"):
                _fallback_cancel_request(session, "nonexistent-run-id")
        finally:
            session.close()


class TestRetryInsert:
    """Verify retry_insert function."""

    @pytest.mark.asyncio
    async def test_retry_insert_run_not_found(self, db_session):
        """Negative: run_id does not exist."""
        event_log = TranslationEventLog(db_session)
        config_manager = MagicMock()
        from src.plugins.translate.orchestrator_cancel import retry_insert

        with pytest.raises(ValueError, match="not found"):
            await retry_insert(db_session, config_manager, event_log, "test_user", "nonexistent-id")

    @pytest.mark.asyncio
    async def test_retry_insert_job_not_found(self, db_with_run):
        """Negative: run's job does not exist (deleted)."""
        session, run_id = db_with_run
        session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).delete()
        session.commit()

        event_log = TranslationEventLog(session)
        config_manager = MagicMock()
        from src.plugins.translate.orchestrator_cancel import retry_insert

        with pytest.raises(ValueError, match="not found"):
            await retry_insert(session, config_manager, event_log, "test_user", run_id)

    @pytest.mark.asyncio
    async def test_retry_insert_success(self, db_with_run):
        """Happy path: retry_insert completes successfully."""
        session, run_id = db_with_run
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.status = "COMPLETED"
        session.commit()

        event_log = TranslationEventLog(session)
        config_manager = MagicMock()

        from src.plugins.translate.orchestrator_cancel import retry_insert

        with patch('src.plugins.translate.orchestrator_cancel.SQLInsertService') as MockSvc:
            mock_svc = MockSvc.return_value
            mock_svc.generate_and_insert_sql = AsyncMock(return_value={
                "status": "success", "query_id": "q123"
            })

            result = await retry_insert(session, config_manager, event_log, "test_user", run_id)

            assert result.insert_status == "success"
            assert result.superset_execution_id == "q123"
            events = session.query(TranslationEvent).filter(
                TranslationEvent.run_id == run_id
            ).all()
            assert any(e.event_type == "RUN_RETRY_INSERT" for e in events)

    @pytest.mark.asyncio
    async def test_retry_insert_with_error(self, db_with_run):
        """Edge: insert_result contains error_message."""
        session, run_id = db_with_run
        run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first()
        run.status = "COMPLETED"
        session.commit()

        event_log = TranslationEventLog(session)
        config_manager = MagicMock()

        from src.plugins.translate.orchestrator_cancel import retry_insert

        with patch('src.plugins.translate.orchestrator_cancel.SQLInsertService') as MockSvc:
            mock_svc = MockSvc.return_value
            mock_svc.generate_and_insert_sql = AsyncMock(return_value={
                "status": "error", "query_id": None,
                "error_message": "DB connection failed"
            })

            result = await retry_insert(session, config_manager, event_log, "test_user", run_id)

            assert result.error_message == "DB connection failed"
            assert result.superset_execution_id == ""
# #endregion Test.OrchestratorCancel
