"""API-тесты для PUT /api/v1/admin/jobs/settings/{job_type}.

Регрессия по issue #1223: PUT для отсутствующей строки молча терял изменения
и отвечал 200 с fallback-дефолтами. После фикса update() выполняет INSERT,
если UPDATE затронул 0 строк, и возвращает новую строку с сохранёнными полями.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock

from fastapi.testclient import TestClient

from app.main import app


def _make_db_recorder(update_rowcounts: list[int]) -> tuple[MagicMock, list[tuple[str, Any]]]:
    """Db-mock, который:
    - возвращает заданные rowcount для UPDATE/INSERT (по списку),
    - имитирует поведение get_one(): SELECT возвращает строку с теми же
      значениями, что были последним INSERT (или None, если не вставлялась).
    """
    db = MagicMock()
    calls: list[tuple[str, Any]] = []
    state: dict[str, Any] = {"inserted": None}
    plan = list(update_rowcounts)

    def execute(stmt: Any, params: Any = None) -> Any:
        sql = str(stmt)
        calls.append((sql, params))
        result = MagicMock()
        if "UPDATE job_settings" in sql:
            result.rowcount = plan.pop(0) if plan else 0
        elif "INSERT INTO job_settings" in sql:
            result.rowcount = plan.pop(0) if plan else 0
            if result.rowcount > 0 and isinstance(params, dict):
                state["inserted"] = params
        elif "SELECT" in sql and "job_settings" in sql:
            # Имитация get_one после успешного INSERT
            row = state["inserted"]
            if row is None:
                result.mappings.return_value.first.return_value = None
            else:
                import datetime

                result.mappings.return_value.first.return_value = {
                    "job_type": row.get("jt"),
                    "enabled": row.get("enabled", True),
                    "queue_name": row.get("queue_name", "celery"),
                    "cron_schedule": row.get("cron_schedule"),
                    "rate_ms": row.get("rate_ms"),
                    "max_retries": row.get("max_retries", 2),
                    "max_concurrency": row.get("max_concurrency", 1),
                    "extra_config": {},  # JSON-строка сериализована — для теста плоский dict ok
                    "updated_at": datetime.datetime(2026, 6, 13, tzinfo=datetime.UTC),
                    "updated_by": row.get("updated_by"),
                    "description": None,
                }
        return result

    db.execute = execute
    db.commit = MagicMock()
    return db, calls


def _override_db(db: MagicMock) -> Any:
    def _factory():
        yield db

    return _factory


def test_put_job_settings_existing_row_updates_only() -> None:
    """Существующая строка — UPDATE, без INSERT."""
    from app.core.db import get_db

    db, calls = _make_db_recorder(update_rowcounts=[1])
    app.dependency_overrides[get_db] = _override_db(db)

    try:
        client = TestClient(app)
        r = client.put(
            "/api/v1/admin/jobs/settings/scrape_kn",
            json={"rate_ms": 750},
        )
        assert r.status_code == 200, r.text
        assert any("UPDATE job_settings" in sql for sql, _ in calls)
        assert not any("INSERT INTO job_settings" in sql for sql, _ in calls)
    finally:
        app.dependency_overrides.clear()


def test_put_job_settings_missing_row_inserts_and_returns_persisted_values() -> None:
    """Issue #1223: PUT для отсутствующей строки должен сохранить изменения.

    После фикса: UPDATE → 0 строк, затем INSERT с переданными полями.
    Ответ должен содержать **переданные** значения, а не fallback-дефолты.
    """
    from app.core.db import get_db

    # UPDATE 0, INSERT 1 — типовой happy-path для несуществующей строки.
    db, calls = _make_db_recorder(update_rowcounts=[0, 1])
    app.dependency_overrides[get_db] = _override_db(db)

    try:
        client = TestClient(app)
        r = client.put(
            "/api/v1/admin/jobs/settings/brand_new_job",
            json={
                "enabled": False,
                "rate_ms": 1234,
                "max_retries": 5,
            },
        )
        assert r.status_code == 200, r.text
        body = r.json()
        # Ключевая проверка: ответ — НЕ fallback (_fallback ставит enabled=True).
        # Значит INSERT действительно произошёл, get_one вернул вставленную строку.
        assert body["job_type"] == "brand_new_job"
        assert body["enabled"] is False
        assert body["rate_ms"] == 1234
        assert body["max_retries"] == 5

        # Подтверждаем, что INSERT реально был.
        assert any("INSERT INTO job_settings" in sql for sql, _ in calls)
        insert_call = next(
            (sql, params) for sql, params in calls if "INSERT INTO job_settings" in sql
        )
        assert "ON CONFLICT (job_type) DO NOTHING" in insert_call[0]
        assert insert_call[1]["jt"] == "brand_new_job"
        assert insert_call[1]["enabled"] is False
        assert insert_call[1]["rate_ms"] == 1234
        assert insert_call[1]["updated_by"] == "admin-ui"
    finally:
        app.dependency_overrides.clear()
