# #region Test.Api.TranslateRunRoutes [C:3] [TYPE Module] [SEMANTICS test,translate,runs,execution,api]
# @BRIEF Tests for _run_routes.py — run execution, retry, cancel.
# @RELATION BINDS_TO -> [TranslateRunRoutesModule]
# @TEST_EDGE: run_value_error -> 400
# @TEST_EDGE: run_generic_error -> 502
# @TEST_EDGE: retry_value_error -> 400
# @TEST_EDGE: retry_generic_error -> 502

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

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from unittest.mock import AsyncMock

_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
    sys.path.insert(0, _src)


def _make_client(overrides: dict | None = None) -> TestClient:
    from src.api.routes.translate._run_routes import router
    from src.dependencies import (
        get_config_manager, get_current_user, get_db,
        has_permission, require_feature,
    )
    from src.schemas.auth import User, RoleSchema

    app = FastAPI()
    app.include_router(router)

    mock_user = User(
        id="user-1", username="admin", email="admin@x.com",
        auth_source="LOCAL",
        created_at=__import__("datetime").datetime.now(),
        roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])],
    )

    defaults = {
        get_current_user: lambda: mock_user,
        get_config_manager: lambda: MagicMock(),
        get_db: lambda: MagicMock(),
        has_permission: lambda *a, **kw: lambda: None,
        require_feature: lambda *a, **kw: lambda: None,
    }
    if overrides:
        defaults.update(overrides)
    for dep, mock_fn in defaults.items():
        app.dependency_overrides[dep] = mock_fn
    return TestClient(app)


def _mock_run(id="run-1", job_id="job-1", status="PENDING"):
    r = MagicMock()
    r.id = id
    r.job_id = job_id
    r.status = status
    r.trigger_type = "manual"
    r.started_at = None
    r.completed_at = None
    r.error_message = None
    r.total_records = 0
    r.successful_records = 0
    r.failed_records = 0
    r.skipped_records = 0
    r.cache_hits = 0
    r.insert_status = None
    r.superset_execution_id = None
    r.config_snapshot = None
    r.key_hash = None
    r.config_hash = None
    r.dict_snapshot_hash = None
    r.created_by = "admin"
    r.created_at = __import__("datetime").datetime(2026, 1, 1)
    return r


# =============================================================================
# Run Translation
# =============================================================================

class TestRunTranslation:
    """POST /api/translate/jobs/{job_id}/run"""

    def test_run_translation_success(self):
        mock_orch = MagicMock()
        mock_orch.start_run.return_value = _mock_run()

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.asyncio.create_task"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run")
        assert resp.status_code == 201
        data = resp.json()
        assert data["id"] == "run-1"
        assert data["status"] == "PENDING"

    def test_run_translation_full(self):
        mock_orch = MagicMock()
        mock_orch.start_run.return_value = _mock_run()

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.asyncio.create_task"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run?full_translation=true")
        assert resp.status_code == 201
        # Verify start_run was called with full_translation=True
        mock_orch.start_run.assert_called_once_with(
            job_id="job-1", is_scheduled=False, full_translation=True
        )

    def test_run_translation_value_error_400(self):
        mock_orch = MagicMock()
        mock_orch.start_run.side_effect = ValueError("Job not found")

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.asyncio.create_task"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-missing/run")
        assert resp.status_code == 400
        assert "Job not found" in resp.text

    def test_run_translation_generic_error_502(self):
        mock_orch = MagicMock()
        mock_orch.start_run.side_effect = RuntimeError("Unexpected error")

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.asyncio.create_task"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run")
        assert resp.status_code == 502
        assert "Run failed" in resp.text


# =============================================================================
# Retry Run
# =============================================================================

class TestRetryRun:
    """POST /api/translate/runs/{run_id}/retry"""

    def test_retry_success(self):
        mock_orch = MagicMock()
        mock_orch.retry_failed_batches = AsyncMock(return_value=_mock_run(status="RUNNING"))

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-1/retry")
        assert resp.status_code == 200
        assert resp.json()["status"] == "RUNNING"

    def test_retry_value_error_400(self):
        mock_orch = MagicMock()
        mock_orch.retry_failed_batches.side_effect = ValueError("Run not retriable")

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-1/retry")
        assert resp.status_code == 400

    def test_retry_generic_error_502(self):
        mock_orch = MagicMock()
        mock_orch.retry_failed_batches.side_effect = RuntimeError("Internal error")

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-1/retry")
        assert resp.status_code == 502
        assert "Retry failed" in resp.text


# =============================================================================
# Retry Insert
# =============================================================================

class TestRetryInsert:
    """POST /api/translate/runs/{run_id}/retry-insert"""

    def test_retry_insert_success(self):
        mock_orch = MagicMock()
        mock_orch.retry_insert = AsyncMock(return_value=_mock_run(status="COMPLETED"))

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-1/retry-insert")
        assert resp.status_code == 200
        assert resp.json()["status"] == "COMPLETED"

    def test_retry_insert_value_error_400(self):
        mock_orch = MagicMock()
        mock_orch.retry_insert.side_effect = ValueError("Insert not needed")

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-1/retry-insert")
        assert resp.status_code == 400

    def test_retry_insert_generic_error_502(self):
        mock_orch = MagicMock()
        mock_orch.retry_insert.side_effect = RuntimeError("Insert failed")

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-1/retry-insert")
        assert resp.status_code == 502
        assert "Retry insert failed" in resp.text


# =============================================================================
# Background Execute — covers _background_execute inner function (lines 57-120)
# =============================================================================

class TestBackgroundExecute:
    """Execute the _background_execute coroutine via real asyncio.create_task."""

    def test_bg_run_not_found(self):
        """Run not found in background: early return (lines 71-77)."""
        mock_orch = MagicMock()
        mock_orch.start_run.return_value = _mock_run()

        mock_bg_db = MagicMock()
        mock_bg_db.query.return_value.filter.return_value.first.return_value = None

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.SessionLocal",
                  return_value=mock_bg_db),
            patch("src.api.routes.translate._run_routes.seed_trace_id"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run")
        assert resp.status_code == 201
        mock_bg_db.close.assert_called_once()

    def test_bg_execute_success(self):
        """Background execute succeeds (lines 65-85)."""
        mock_run = _mock_run(status="COMPLETED")
        mock_run.completed_at = __import__("datetime").datetime(2026, 1, 1)
        mock_orch = MagicMock()
        mock_orch.start_run.return_value = _mock_run()
        mock_orch.execute_run = AsyncMock()

        mock_bg_db = MagicMock()
        # Two queries: first to find run, second to verify
        mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run, mock_run]

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.SessionLocal",
                  return_value=mock_bg_db),
            patch("src.api.routes.translate._run_routes.seed_trace_id"),
            patch("src.api.routes.translate._run_routes.get_trace_id",
                  return_value="trace-abc-123"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run")
        assert resp.status_code == 201
        mock_orch.execute_run.assert_awaited_once()
        mock_bg_db.close.assert_called_once()

    def test_bg_execute_not_terminal_marks_failed(self):
        """Run not in terminal state after commit: marked FAILED (lines 86-95)."""
        mock_run_pending = _mock_run(status="RUNNING")
        mock_orch = MagicMock()
        mock_orch.start_run.return_value = _mock_run()
        mock_orch.execute_run = AsyncMock()

        mock_bg_db = MagicMock()
        mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run_pending, mock_run_pending]

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.SessionLocal",
                  return_value=mock_bg_db),
            patch("src.api.routes.translate._run_routes.seed_trace_id"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run")
        assert resp.status_code == 201
        # Run should have been marked FAILED
        assert mock_run_pending.status == "FAILED"
        mock_bg_db.commit.assert_called()
        mock_bg_db.close.assert_called_once()

    def test_bg_execute_exception_marks_failed(self):
        """Exception in background marks run FAILED via fallback (lines 97-120)."""
        mock_run = _mock_run()
        mock_orch = MagicMock()
        mock_orch.start_run.return_value = mock_run
        mock_orch.execute_run = AsyncMock(side_effect=RuntimeError("LLM timeout"))

        mock_bg_db = MagicMock()
        mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run]

        # Second session for fallback
        mock_fb_db = MagicMock()
        mock_fb_db.query.return_value.filter.return_value.first.return_value = mock_run

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.SessionLocal",
                  side_effect=[mock_bg_db, mock_fb_db]),
            patch("src.api.routes.translate._run_routes.seed_trace_id"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run")
        assert resp.status_code == 201
        assert mock_run.status == "FAILED"
        assert mock_run.error_message is not None
        mock_fb_db.close.assert_called_once()
        mock_bg_db.close.assert_called_once()

    def test_bg_execute_exception_fallback_fails(self):
        """Exception in background AND fallback: handled gracefully (lines 115-117)."""
        mock_run = _mock_run()
        mock_orch = MagicMock()
        mock_orch.start_run.return_value = mock_run
        mock_orch.execute_run = AsyncMock(side_effect=RuntimeError("LLM timeout"))

        mock_bg_db = MagicMock()
        mock_bg_db.query.return_value.filter.return_value.first.return_value = mock_run

        # Fallback db also raises
        bad_fb_db = MagicMock()
        bad_fb_db.query.side_effect = RuntimeError("DB closed")

        with (
            patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                  return_value=mock_orch),
            patch("src.api.routes.translate._run_routes.SessionLocal",
                  side_effect=[mock_bg_db, bad_fb_db]),
            patch("src.api.routes.translate._run_routes.seed_trace_id"),
        ):
            client = _make_client()
            resp = client.post("/api/translate/jobs/job-1/run")
        assert resp.status_code == 201
        mock_bg_db.close.assert_called_once()


# =============================================================================
# Cancel Run
# =============================================================================

class TestCancelRun:
    """POST /api/translate/runs/{run_id}/cancel"""

    def test_cancel_success(self):
        mock_orch = MagicMock()
        mock_orch.cancel_run.return_value = _mock_run(status="CANCELLED")

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-1/cancel")
        assert resp.status_code == 200
        assert resp.json()["status"] == "CANCELLED"

    def test_cancel_value_error_400(self):
        mock_orch = MagicMock()
        mock_orch.cancel_run.side_effect = ValueError("Run not found")

        with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
                   return_value=mock_orch):
            client = _make_client()
            resp = client.post("/api/translate/runs/run-missing/cancel")
        assert resp.status_code == 400
# #endregion Test.Api.TranslateRunRoutes
