# #region Test.OrchestratorRunCompletion [C:3] [TYPE Module] [SEMANTICS test, translate, run, completion, failure]
# @BRIEF Tests for orchestrator_run_completion.py — handle_executor_failure, complete_cancelled, complete_success.
# @RELATION BINDS_TO -> [orchestrator_run_completion]
# @TEST_CONTRACT: handle_executor_failure -> TranslationRun | marks run as FAILED
# @TEST_CONTRACT: complete_cancelled -> TranslationRun | finalizes cancelled run
# @TEST_CONTRACT: complete_success -> TranslationRun | finalizes successful run
# @TEST_EDGE: rollback_fails
# @TEST_EDGE: skip_insert
# @TEST_EDGE: insert_failed

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_run_completion import (
    handle_executor_failure,
    complete_cancelled,
    complete_success,
)


class TestHandleExecutorFailure:
    """handle_executor_failure — mark run as FAILED with error info."""

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

    def _make_job(self):
        job = MagicMock()
        job.id = "job-1"
        return job

    def test_marks_run_failed(self):
        """Run status set to FAILED."""
        db = MagicMock()
        db.merge.return_value = db.merge.return_value  # default: returns new mock
        event_log = MagicMock()
        run = self._make_run()
        job = self._make_job()
        error = ValueError("LLM timeout")

        # Make db.merge return the original run so .id is consistent
        db.merge.return_value = run
        result = handle_executor_failure(db, event_log, run, job, error, "test-user")

        assert result.status == "FAILED"
        assert "LLM timeout" in result.error_message
        assert result.completed_at is not None
        db.rollback.assert_called_once()
        db.flush.assert_called_once()
        db.commit.assert_called_once()

    def test_logs_event(self):
        """RUN_FAILED event logged."""
        db = MagicMock()
        db.merge.return_value = self._make_run(id="run-1")
        event_log = MagicMock()
        run = self._make_run()
        job = self._make_job()
        error = RuntimeError("fail")

        handle_executor_failure(db, event_log, run, job, error, "test-user")

        event_log.log_event.assert_called_once()
        call_kwargs = event_log.log_event.call_args[1]
        assert call_kwargs["job_id"] == "job-1"
        assert call_kwargs["run_id"] == "run-1"
        assert call_kwargs["event_type"] == "RUN_FAILED"
        assert call_kwargs["payload"] == {"error": "fail", "phase": "translation"}

    def test_rollback_error_handled_gracefully(self):
        """If db.rollback() raises, it's silently caught."""
        db = MagicMock()
        db.rollback.side_effect = Exception("rollback error")
        db.merge.return_value = self._make_run()
        event_log = MagicMock()
        run = self._make_run()
        job = self._make_job()
        error = Exception("exec error")

        result = handle_executor_failure(db, event_log, run, job, error, "test-user")
        assert result.status == "FAILED"
        db.rollback.assert_called_once()
        assert db.merge.called  # still continues

    def test_run_merged_after_rollback(self):
        """Run is merged after rollback attempt."""
        db = MagicMock()
        event_log = MagicMock()
        run = self._make_run()
        job = self._make_job()
        error = Exception()

        handle_executor_failure(db, event_log, run, job, error)
        db.merge.assert_called_once_with(run)

    def test_error_message_includes_phase(self):
        """Error message includes 'Translation execution failed:' prefix."""
        db = MagicMock()
        db.merge.return_value = self._make_run()
        event_log = MagicMock()
        run = self._make_run()
        job = self._make_job()

        result = handle_executor_failure(db, event_log, run, job, ValueError("bad"), "user")
        assert result.error_message.startswith("Translation execution failed:")


class TestCompleteCancelled:
    """complete_cancelled — finalize cancelled run."""

    def test_updates_stats_and_logs(self):
        """Aggregator stats updated and event logged."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        run = MagicMock()
        run.id = "run-1"
        run.job_id = "job-1"
        stats_map = {"ru": MagicMock()}

        result = complete_cancelled(db, event_log, aggregator, run, stats_map, "test-user")

        aggregator.update_language_stats.assert_called_once_with("run-1", stats_map)
        event_log.log_event.assert_called_once_with(
            job_id="job-1", run_id="run-1", event_type="RUN_CANCELLED",
            payload={"reason": "cancellation_flag"}, created_by="test-user",
        )
        db.commit.assert_called_once()
        assert result == run

    def test_no_current_user(self):
        """Works without current_user."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        run = MagicMock()
        run.id = "run-1"
        run.job_id = "job-1"

        result = complete_cancelled(db, event_log, aggregator, run, {}, None)
        event_log.log_event.assert_called_once()
        assert result == run


class TestCompleteSuccess:
    """complete_success — finalize successful run."""

    def _make_run(self, **kwargs):
        run = MagicMock()
        run.id = "run-1"
        run.job_id = "job-1"
        run.status = "RUNNING"
        run.total_records = 100
        run.successful_records = 95
        run.failed_records = 3
        run.skipped_records = 2
        for k, v in kwargs.items():
            setattr(run, k, v)
        return run

    def _make_job(self):
        job = MagicMock()
        job.id = "job-1"
        return job

    @pytest.mark.asyncio
    async def test_skip_insert_completes_run(self):
        """skip_insert=True -> COMPLETED without SQL."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        run = self._make_run()
        job = self._make_job()

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=True,
            language_stats_map={}, current_user="u",
        )

        assert result.status == "COMPLETED"
        assert result.completed_at is not None
        aggregator.update_language_stats.assert_called_once()
        sql_service.generate_and_insert_sql.assert_not_called()
        assert db.commit.call_count >= 1

    @pytest.mark.asyncio
    async def test_insert_success(self):
        """SQL insert succeeds -> COMPLETED."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        sql_service.generate_and_insert_sql.return_value = {
            "status": "success", "query_id": "q-1",
        }
        run = self._make_run()
        job = self._make_job()
        # After commit, run is re-queried
        final_run = MagicMock(status="COMPLETED")
        final_run.insert_status = "success"
        final_run.superset_execution_id = "q-1"
        final_run.error_message = None
        db.query.return_value.filter.return_value.first.return_value = final_run

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=False,
            language_stats_map={}, current_user="u",
        )

        assert result.status == "COMPLETED"
        sql_service.generate_and_insert_sql.assert_called_once_with(job, run)
        assert result.insert_status == "success"
        assert result.superset_execution_id == "q-1"

    @pytest.mark.asyncio
    async def test_insert_failed_sets_status_failed(self):
        """Insert status 'failed' -> run status FAILED."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        sql_service.generate_and_insert_sql.return_value = {
            "status": "failed", "error_message": "DB timeout",
        }
        run = self._make_run()
        job = self._make_job()
        final_run = MagicMock(status="FAILED")
        final_run.error_message = "DB timeout"
        db.query.return_value.filter.return_value.first.return_value = final_run

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=False,
            language_stats_map={}, current_user="u",
        )

        assert result.status == "FAILED"
        assert "DB timeout" in result.error_message

    @pytest.mark.asyncio
    async def test_insert_timeout_sets_status_failed(self):
        """Insert status 'timeout' -> run status FAILED."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        sql_service.generate_and_insert_sql.return_value = {
            "status": "timeout", "error_message": None,
        }
        run = self._make_run()
        job = self._make_job()
        final_run = MagicMock(status="FAILED")
        final_run.error_message = "SQL insert phase timeout"
        db.query.return_value.filter.return_value.first.return_value = final_run

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=False,
            language_stats_map={}, current_user="u",
        )

        assert result.status == "FAILED"
        assert "SQL insert phase timeout" in result.error_message

    @pytest.mark.asyncio
    async def test_insert_skipped_non_failure(self):
        """Insert status 'skipped' -> not a failure, COMPLETED."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        sql_service.generate_and_insert_sql.return_value = {
            "status": "skipped", "query_id": None,
        }
        run = self._make_run()
        job = self._make_job()
        final_run = MagicMock(status="COMPLETED")
        db.query.return_value.filter.return_value.first.return_value = final_run

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=False,
            language_stats_map={}, current_user="u",
        )

        assert result.status == "COMPLETED"

    @pytest.mark.asyncio
    async def test_insert_with_error_message_appended(self):
        """error_message from insert appended to run."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        sql_service.generate_and_insert_sql.return_value = {
            "status": "success", "error_message": "warning: slow query",
        }
        run = self._make_run()
        run.error_message = None
        job = self._make_job()
        final_run = MagicMock(status="COMPLETED")
        final_run.error_message = "warning: slow query"
        db.query.return_value.filter.return_value.first.return_value = final_run

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=False,
            language_stats_map={}, current_user="u",
        )

        assert result.error_message == "warning: slow query"

    @pytest.mark.asyncio
    async def test_insert_failed_without_error_message_uses_default(self):
        """Insert failed without error_message -> default message."""
        db = MagicMock()
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        sql_service.generate_and_insert_sql.return_value = {
            "status": "failed", "error_message": None,
        }
        run = self._make_run()
        run.error_message = None
        job = self._make_job()
        final_run = MagicMock(status="FAILED")
        final_run.error_message = "SQL insert phase failed"
        db.query.return_value.filter.return_value.first.return_value = final_run

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=False,
            language_stats_map={}, current_user="u",
        )

        assert "SQL insert phase failed" in result.error_message

    @pytest.mark.asyncio
    async def test_refreshes_run_after_commit(self):
        """Run is re-queried after commit."""
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = MagicMock(status="COMPLETED")
        event_log = MagicMock()
        aggregator = MagicMock()
        sql_service = AsyncMock()
        sql_service.generate_and_insert_sql.return_value = {
            "status": "success", "query_id": "q-1",
        }
        run = self._make_run()
        job = self._make_job()

        result = await complete_success(
            db, event_log, aggregator, sql_service,
            run, job, skip_insert=False,
            language_stats_map={}, current_user="u",
        )

        # Should re-query run after commit
        db.query.assert_called()
# #endregion Test.OrchestratorRunCompletion
