"""Tests для admin weight profiles endpoints.

Покрывает:
- GET /api/v1/admin/site-finder/weight-profiles → 200 + list
- POST → 201 + created profile
- GET /{id} → 200 / 404
- PUT /{id} → 200 / 404
- DELETE /{id} → 204 / 404
- 401 при отсутствии X-Admin-Token
- 422 при невалидных weights (неизвестная категория, вес вне диапазона)

Mock-based: get_db переопределяется через dependency override.
"""

from __future__ import annotations

from datetime import UTC, datetime
from unittest.mock import MagicMock

import pytest
from fastapi.testclient import TestClient

from app.core.db import get_db
from app.main import app
from app.services.site_finder.weight_profiles import WeightProfile

_ADMIN_TOKEN = "test-admin-token"
_HEADERS = {"X-Admin-Token": _ADMIN_TOKEN}

_NOW = datetime.now(UTC)


def _make_profile(
    profile_id: int = 1,
    user_id: str = "user-1",
    profile_name: str = "Test",
    weights: dict | None = None,
    is_default: bool = False,
) -> WeightProfile:
    return WeightProfile(
        id=profile_id,
        user_id=user_id,
        profile_name=profile_name,
        weights=weights or {"school": 1.5, "park": 1.8},
        is_default=is_default,
        description=None,
        created_at=_NOW,
        updated_at=_NOW,
    )


@pytest.fixture()
def client_with_token(monkeypatch: pytest.MonkeyPatch) -> TestClient:
    """TestClient с переопределённым SCRAPE_ADMIN_TOKEN."""
    monkeypatch.setattr("app.core.config.settings.scrape_admin_token", _ADMIN_TOKEN)
    return TestClient(app)


@pytest.fixture()
def mock_db() -> MagicMock:
    return MagicMock()


def _override_db(mock: MagicMock):
    def _get_db_override():
        yield mock

    app.dependency_overrides[get_db] = _get_db_override
    return mock


def _clear_overrides():
    app.dependency_overrides.pop(get_db, None)


# ── GET list ───────────────────────────────────────────────────────────────────


def test_list_empty(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
    """GET ?user_id= → 200 + пустой список."""
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.list_profiles",
            lambda db, user_id: [],
        )
        r = client_with_token.get(
            "/api/v1/admin/site-finder/weight-profiles",
            params={"user_id": "user-x"},
            headers=_HEADERS,
        )
        assert r.status_code == 200
        assert r.json() == []
    finally:
        _clear_overrides()


def test_list_returns_profiles(
    client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """GET ?user_id= → 200 + список профилей."""
    profiles = [_make_profile(1, is_default=True), _make_profile(2, profile_name="B")]
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.list_profiles",
            lambda db, user_id: profiles,
        )
        r = client_with_token.get(
            "/api/v1/admin/site-finder/weight-profiles",
            params={"user_id": "user-1"},
            headers=_HEADERS,
        )
        assert r.status_code == 200
        body = r.json()
        assert len(body) == 2
        assert body[0]["id"] == 1
    finally:
        _clear_overrides()


# ── POST create ────────────────────────────────────────────────────────────────


def test_create_then_get(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
    """POST создаёт профиль, возвращает его со статусом 201."""
    created = _make_profile(42, profile_name="Семейный", weights={"school": 2.0, "park": 1.5})
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.create_profile",
            lambda db, payload: created,
        )
        r = client_with_token.post(
            "/api/v1/admin/site-finder/weight-profiles",
            json={
                "user_id": "user-1",
                "profile_name": "Семейный",
                "weights": {"school": 2.0, "park": 1.5},
                "is_default": False,
            },
            headers=_HEADERS,
        )
        assert r.status_code == 201
        body = r.json()
        assert body["id"] == 42
        assert body["profile_name"] == "Семейный"
        assert body["weights"]["school"] == 2.0
    finally:
        _clear_overrides()


def test_create_validation_unknown_category(client_with_token: TestClient) -> None:
    """POST с неизвестной POI-категорией → 422 (Pydantic validation)."""
    r = client_with_token.post(
        "/api/v1/admin/site-finder/weight-profiles",
        json={
            "user_id": "user-1",
            "profile_name": "Bad",
            "weights": {"supermarket": 1.0},  # не в ALLOWED_CATEGORIES
        },
        headers=_HEADERS,
    )
    assert r.status_code == 422


def test_create_validation_weight_out_of_bounds(client_with_token: TestClient) -> None:
    """POST с весом вне [-2, 3] → 422."""
    r = client_with_token.post(
        "/api/v1/admin/site-finder/weight-profiles",
        json={
            "user_id": "user-1",
            "profile_name": "Bad",
            "weights": {"school": 99.0},
        },
        headers=_HEADERS,
    )
    assert r.status_code == 422


# ── GET one ────────────────────────────────────────────────────────────────────


def test_get_profile_found(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
    """GET /{id}?user_id= → 200."""
    profile = _make_profile(7)
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.get_profile",
            lambda db, user_id, profile_id: profile,
        )
        r = client_with_token.get(
            "/api/v1/admin/site-finder/weight-profiles/7",
            params={"user_id": "user-1"},
            headers=_HEADERS,
        )
        assert r.status_code == 200
        assert r.json()["id"] == 7
    finally:
        _clear_overrides()


def test_get_profile_not_found(
    client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """GET /{id} несуществующего профиля → 404."""
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.get_profile",
            lambda db, user_id, profile_id: None,
        )
        r = client_with_token.get(
            "/api/v1/admin/site-finder/weight-profiles/999",
            params={"user_id": "user-1"},
            headers=_HEADERS,
        )
        assert r.status_code == 404
    finally:
        _clear_overrides()


# ── PUT update ─────────────────────────────────────────────────────────────────


def test_update_profile(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
    """PUT /{id} → 200 + обновлённый профиль."""
    updated = _make_profile(3, profile_name="Обновлённый")
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.update_profile",
            lambda db, user_id, profile_id, payload: updated,
        )
        r = client_with_token.put(
            "/api/v1/admin/site-finder/weight-profiles/3",
            params={"user_id": "user-1"},
            json={"profile_name": "Обновлённый"},
            headers=_HEADERS,
        )
        assert r.status_code == 200
        assert r.json()["profile_name"] == "Обновлённый"
    finally:
        _clear_overrides()


def test_update_profile_not_found(
    client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """PUT /{id} несуществующего → 404."""
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.update_profile",
            lambda db, user_id, profile_id, payload: None,
        )
        r = client_with_token.put(
            "/api/v1/admin/site-finder/weight-profiles/999",
            params={"user_id": "user-1"},
            json={"profile_name": "X"},
            headers=_HEADERS,
        )
        assert r.status_code == 404
    finally:
        _clear_overrides()


# ── DELETE ─────────────────────────────────────────────────────────────────────


def test_delete_success(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
    """DELETE /{id} → 204."""
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.delete_profile",
            lambda db, user_id, profile_id: True,
        )
        r = client_with_token.delete(
            "/api/v1/admin/site-finder/weight-profiles/5",
            params={"user_id": "user-1"},
            headers=_HEADERS,
        )
        assert r.status_code == 204
    finally:
        _clear_overrides()


def test_delete_not_found(client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
    """DELETE /{id} несуществующего → 404."""
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.delete_profile",
            lambda db, user_id, profile_id: False,
        )
        r = client_with_token.delete(
            "/api/v1/admin/site-finder/weight-profiles/999",
            params={"user_id": "user-1"},
            headers=_HEADERS,
        )
        assert r.status_code == 404
    finally:
        _clear_overrides()


# ── include_system preset seed (Issue #114 / PR #229) ─────────────────────────


def test_list_include_system_calls_with_system(
    client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """GET ?include_system=true вызывает list_profiles_with_system, возвращает presets."""
    system_profile = _make_profile(
        100, user_id="__system__", profile_name="Комфорт", weights={"park": 1.8, "school": 1.5}
    )
    user_profile = _make_profile(1, user_id="user-1", profile_name="Мой профиль")
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.list_profiles_with_system",
            lambda db, user_id: [user_profile, system_profile],
        )
        r = client_with_token.get(
            "/api/v1/admin/site-finder/weight-profiles",
            params={"user_id": "user-1", "include_system": "true"},
            headers=_HEADERS,
        )
        assert r.status_code == 200
        body = r.json()
        assert len(body) == 2
        profile_names = {p["profile_name"] for p in body}
        assert "Комфорт" in profile_names
        assert "Мой профиль" in profile_names
    finally:
        _clear_overrides()


def test_list_without_include_system_does_not_call_with_system(
    client_with_token: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """GET без include_system → list_profiles (только пользовательские профили)."""
    user_profile = _make_profile(1, user_id="user-1")
    called_with_system = []
    mock = MagicMock()
    _override_db(mock)
    try:
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.list_profiles",
            lambda db, user_id: [user_profile],
        )
        monkeypatch.setattr(
            "app.api.v1.admin_weight_profiles.list_profiles_with_system",
            lambda db, user_id: called_with_system.append(True) or [],
        )
        r = client_with_token.get(
            "/api/v1/admin/site-finder/weight-profiles",
            params={"user_id": "user-1"},
            headers=_HEADERS,
        )
        assert r.status_code == 200
        assert len(r.json()) == 1
        # list_profiles_with_system НЕ должен вызываться без include_system=true
        assert called_with_system == [], "вызван list_profiles_with_system без флага"
    finally:
        _clear_overrides()


def test_list_profiles_with_system_service(monkeypatch: pytest.MonkeyPatch) -> None:
    """list_profiles_with_system передаёт system_user_id='__system__' в запрос."""
    from app.services.site_finder.weight_profiles import SYSTEM_USER_ID, list_profiles_with_system

    captured_params: list[dict] = []

    class FakeResult:
        def mappings(self) -> FakeResult:
            return self

        def all(self) -> list:
            return []

    class FakeDb:
        def execute(self, query: object, params: dict) -> FakeResult:
            captured_params.append(params)
            return FakeResult()

    list_profiles_with_system(FakeDb(), user_id="user-test")
    assert len(captured_params) == 1
    assert captured_params[0]["system_user_id"] == SYSTEM_USER_ID
    assert captured_params[0]["user_id"] == "user-test"


# ── Auth ───────────────────────────────────────────────────────────────────────


_TOKEN_REMOVED_REASON = (
    "App-level X-Admin-Token header удалён 2026-05-23 (см. docstring "
    "app/api/v1/admin_weight_profiles.py: Caddy basic_auth PR #426 + RBAC достаточны, "
    "двойная auth избыточна). Endpoint больше не несёт verify_admin_token dependency. "
    "В test-mode RBAC bypass'ится (CI-rehab 1/3) → 401 здесь недостижим без реверта "
    "security-решения. Тест проверял удалённое поведение."
)


@pytest.mark.skip(reason=_TOKEN_REMOVED_REASON)
def test_unauthorized_no_token(monkeypatch: pytest.MonkeyPatch) -> None:
    """Запрос без X-Admin-Token → 401 (устарело: токен-гейт удалён 2026-05-23)."""
    monkeypatch.setattr("app.core.config.settings.scrape_admin_token", _ADMIN_TOKEN)
    client = TestClient(app)
    r = client.get(
        "/api/v1/admin/site-finder/weight-profiles",
        params={"user_id": "user-1"},
        # без headers — нет X-Admin-Token
    )
    assert r.status_code == 401


@pytest.mark.skip(reason=_TOKEN_REMOVED_REASON)
def test_unauthorized_wrong_token(monkeypatch: pytest.MonkeyPatch) -> None:
    """Неверный X-Admin-Token → 401 (устарело: токен-гейт удалён 2026-05-23)."""
    monkeypatch.setattr("app.core.config.settings.scrape_admin_token", _ADMIN_TOKEN)
    client = TestClient(app)
    r = client.get(
        "/api/v1/admin/site-finder/weight-profiles",
        params={"user_id": "user-1"},
        headers={"X-Admin-Token": "wrong-token"},
    )
    assert r.status_code == 401
