# #region Test.Api.TranslateRunListRoutes [C:3] [TYPE Module] [SEMANTICS test,translate,runs,listing,detail,csv,api]
# @BRIEF Tests for _run_list_routes.py — run listing, detail, CSV downloads.
# @RELATION BINDS_TO -> [TranslateRunListRoutesModule]
# @TEST_EDGE: list_runs_exception -> 400
# @TEST_EDGE: run_detail_value_error -> 404
# @TEST_EDGE: skipped_csv_exception -> 400
# @TEST_EDGE: failed_csv_run_not_found -> 404
# @TEST_EDGE: failed_csv_no_records -> 404
# @TEST_EDGE: failed_csv_exception -> 400

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

_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_list_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="COMPLETED",
    trigger_type="manual", started_at=None, completed_at=None,
    language_stats=None, config_snapshot=None,
):
    from datetime import datetime, UTC

    r = MagicMock()
    r.id = id
    r.job_id = job_id
    r.status = status
    r.trigger_type = trigger_type
    r.started_at = started_at or datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
    r.completed_at = completed_at or datetime(2026, 1, 1, 12, 30, 0, tzinfo=UTC)
    r.error_message = None
    r.total_records = 100
    r.successful_records = 95
    r.failed_records = 3
    r.skipped_records = 2
    r.cache_hits = 10
    r.insert_status = "completed"
    r.superset_execution_id = None
    r.config_hash = "abc123"
    r.dict_snapshot_hash = None
    r.created_by = "admin"
    r.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
    r.language_stats = language_stats or []
    r.config_snapshot = config_snapshot
    return r


def _mock_lang_stat(language_code="es", **kw):
    ls = MagicMock()
    ls.language_code = language_code
    ls.total_rows = kw.get("total_rows", 100)
    ls.translated_rows = kw.get("translated_rows", 90)
    ls.failed_rows = kw.get("failed_rows", 5)
    ls.skipped_rows = kw.get("skipped_rows", 5)
    ls.token_count = kw.get("token_count", 1000)
    ls.estimated_cost = kw.get("estimated_cost", 0.02)
    return ls


def _mock_record(id="rec-1", run_id="run-1", status="SKIPPED"):
    rec = MagicMock()
    rec.id = id
    rec.run_id = run_id
    rec.status = status
    rec.source_sql = "SELECT * FROM table"
    rec.target_sql = "INSERT INTO table"
    rec.source_object_type = "table"
    rec.source_object_id = "42"
    rec.source_object_name = "my_table"
    rec.error_message = None
    rec.created_at = __import__("datetime").datetime(2026, 1, 1)
    rec.source_data = {"key": "value"}
    rec.languages = []
    return rec


# =============================================================================
# List Runs
# =============================================================================

class TestListRuns:
    """GET /api/translate/runs"""

    def test_list_runs_success(self):
        mock_db = MagicMock()
        run = _mock_run()
        # Build a query chain
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.offset.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.count.return_value = 1
        mock_query.all.return_value = [run]

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 1
        assert len(data["items"]) == 1
        assert data["items"][0]["id"] == "run-1"

    def test_list_runs_with_filters(self):
        mock_db = MagicMock()
        run = _mock_run()
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.offset.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.count.return_value = 1
        mock_query.all.return_value = [run]

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get(
            "/api/translate/runs"
            "?job_id=job-1&run_status=COMPLETED&trigger_type=manual"
            "&created_by=admin&date_from=2026-01-01T00:00:00"
            "&date_to=2026-01-31T00:00:00&page=1&page_size=10"
        )
        assert resp.status_code == 200
        assert resp.json()["total"] == 1

    def test_list_runs_with_language_stats(self):
        stats = [_mock_lang_stat("es"), _mock_lang_stat("fr")]
        run = _mock_run(language_stats=stats)
        mock_db = MagicMock()
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.offset.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.count.return_value = 1
        mock_query.all.return_value = [run]

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs")
        assert resp.status_code == 200
        data = resp.json()
        item = data["items"][0]
        assert "es" in item["language_stats"]
        assert "fr" in item["language_stats"]
        assert item["target_languages"] == ["es", "fr"]
        assert item["total_translated"] == 180

    def test_list_runs_empty(self):
        mock_db = MagicMock()
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.offset.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.count.return_value = 0
        mock_query.all.return_value = []

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 0
        assert data["items"] == []

    def test_list_runs_with_invalid_date_from(self):
        """Invalid date_from is logged and ignored (debug path)."""
        mock_db = MagicMock()
        run = _mock_run()
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.offset.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.count.return_value = 1
        mock_query.all.return_value = [run]

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs?date_from=not-a-date")
        assert resp.status_code == 200
        assert resp.json()["total"] == 1

    def test_list_runs_with_invalid_date_to(self):
        """Invalid date_to is logged and ignored (debug path)."""
        mock_db = MagicMock()
        run = _mock_run()
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.offset.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.count.return_value = 1
        mock_query.all.return_value = [run]

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs?date_to=not-a-date")
        assert resp.status_code == 200
        assert resp.json()["total"] == 1

    def test_list_runs_generic_error_400(self):
        mock_db = MagicMock()
        mock_db.query.side_effect = Exception("DB connection error")

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs")
        assert resp.status_code == 400


# =============================================================================
# Run Detail
# =============================================================================

class TestGetRunDetail:
    """GET /api/translate/runs/{run_id}/detail"""

    def test_detail_success(self):
        mock_orch = MagicMock()
        mock_orch.get_run_status.return_value = {"id": "run-1", "status": "COMPLETED"}
        mock_orch.get_run_records.return_value = {
            "items": [{"id": "rec-1"}], "total": 1, "page": 1, "page_size": 50,
        }

        mock_event_log = MagicMock()
        mock_event_log.query_events.return_value = [{"event": "started"}]
        mock_event_log.get_run_event_summary.return_value = {"total_events": 1}

        mock_run = _mock_run(config_snapshot={"key": "val"})

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

        from src.dependencies import get_db

        with (
            patch(
                "src.api.routes.translate._run_list_routes.TranslationOrchestrator",
                return_value=mock_orch,
            ),
            patch(
                "src.api.routes.translate._run_list_routes.TranslationEventLog",
                return_value=mock_event_log,
            ),
        ):
            client = _make_client({get_db: lambda: mock_db})
            resp = client.get("/api/translate/runs/run-1/detail")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == "run-1"
        assert data["config_snapshot"] == {"key": "val"}
        assert data["events"] == [{"event": "started"}]
        assert data["event_invariants"] == {"total_events": 1}

    def test_detail_not_found_404(self):
        mock_orch = MagicMock()
        mock_orch.get_run_status.side_effect = ValueError("Run not found")

        with patch(
            "src.api.routes.translate._run_list_routes.TranslationOrchestrator",
            return_value=mock_orch,
        ):
            client = _make_client()
            resp = client.get("/api/translate/runs/run-missing/detail")
        assert resp.status_code == 404


# =============================================================================
# Download Skipped CSV
# =============================================================================

class TestDownloadSkippedCsv:
    """GET /api/translate/runs/{run_id}/skipped.csv"""

    def test_skipped_csv_success(self):
        mock_db = MagicMock()
        mock_rec = _mock_record()
        mock_db.query.return_value.filter.return_value.all.return_value = [mock_rec]

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/skipped.csv")
        assert resp.status_code == 200
        assert "text/csv" in resp.headers["content-type"]
        assert "skipped_run-1.csv" in resp.headers["content-disposition"]
        assert "rec-1" in resp.text

    def test_skipped_csv_no_records(self):
        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.all.return_value = []

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/skipped.csv")
        assert resp.status_code == 200
        assert "source_object_name" in resp.text  # headers still present

    def test_skipped_csv_error_400(self):
        mock_db = MagicMock()
        mock_db.query.side_effect = Exception("DB error")

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/skipped.csv")
        assert resp.status_code == 400


# =============================================================================
# Download Failed CSV
# =============================================================================

class TestDownloadFailedCsv:
    """GET /api/translate/runs/{run_id}/failed.csv"""

    def _setup_mock_db_for_failed_csv(self, mock_run, records, run_first=True):
        """Helper: create a mock DB that returns run on first query and records on second."""
        mock_db = MagicMock()
        call_count = [0]

        def query_side_effect(model):
            call_count[0] += 1
            if call_count[0] == 1:
                # TranslationRun query
                q = MagicMock()
                q.filter.return_value.first.return_value = mock_run
                return q
            else:
                # TranslationRecord query
                q = MagicMock()
                q.options.return_value.filter.return_value.order_by.return_value.all.return_value = records
                return q

        mock_db.query.side_effect = query_side_effect
        return mock_db

    def test_failed_csv_success(self):
        mock_run = _mock_run()
        mock_rec = _mock_record(id="rec-1", status="SUCCESS")
        mock_rec.languages = []

        mock_db = self._setup_mock_db_for_failed_csv(mock_run, [mock_rec])

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/failed.csv")
        assert resp.status_code == 200
        assert "text/csv" in resp.headers["content-type"]
        assert "failed_run-1.csv" in resp.headers["content-disposition"]

    def test_failed_csv_run_not_found_404(self):
        mock_db = MagicMock()
        run_query = MagicMock()
        run_query.filter.return_value.first.return_value = None
        mock_db.query.return_value = run_query

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-missing/failed.csv")
        assert resp.status_code == 404
        assert "Run not found" in resp.text

    def test_failed_csv_no_records_404(self):
        mock_run = _mock_run()
        mock_db = self._setup_mock_db_for_failed_csv(mock_run, [])

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/failed.csv")
        assert resp.status_code == 404
        assert "No successful records found" in resp.text

    def test_failed_csv_with_languages(self):
        mock_run = _mock_run()

        mock_lang = MagicMock()
        mock_lang.language_code = "es"
        mock_lang.final_value = "hola"
        mock_lang.translated_value = "hola"

        mock_rec = _mock_record(id="rec-1", status="SUCCESS")
        mock_rec.languages = [mock_lang]
        mock_rec.source_data = {"col": "val"}

        mock_db = self._setup_mock_db_for_failed_csv(mock_run, [mock_rec])

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/failed.csv")
        assert resp.status_code == 200
        assert "hola" in resp.text
        assert "es" in resp.text

    def test_failed_csv_unserializable_source_data(self):
        """Covers json.dumps TypeError fallback in failed.csv (lines 322-323)."""
        mock_run = _mock_run()

        mock_rec = _mock_record(id="rec-1", status="SUCCESS")
        mock_rec.languages = []
        mock_rec.source_data = object()  # Not JSON-serializable → TypeError fallback

        mock_db = self._setup_mock_db_for_failed_csv(mock_run, [mock_rec])

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/failed.csv")
        assert resp.status_code == 200
        # Fallback: str(rec.source_data) should contain "object"
        assert "object" in resp.text

    def test_failed_csv_generic_error_400(self):
        mock_db = MagicMock()
        mock_db.query.side_effect = Exception("Unexpected error")

        from src.dependencies import get_db
        client = _make_client({get_db: lambda: mock_db})
        resp = client.get("/api/translate/runs/run-1/failed.csv")
        assert resp.status_code == 400
# #endregion Test.Api.TranslateRunListRoutes
