"""Tests for GET /api/v1/trade-in/location-coef (#2045 BE-3, LocationDrawer).

Mirrors the IDOR + mocked-DB conventions of test_estimate_idor.py / test_street_deals_endpoint.py
(no live Postgres needed). Covers:
  - IDOR guard (owner / other pilot 404 / admin / unauthenticated 401)
  - graceful fallback: osm_poi_ekb_local empty/not-refreshed, estimate has no lat/lon
  - happy path: base_price_rub * coef → result_price_rub, factors surfaced
  - radius_m query-param clamping
"""

from __future__ import annotations

import os
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock

# psycopg v3 driver required; stub DATABASE_URL before any app import.
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

# WeasyPrint requires GTK — not present in CI/Windows. Stub before any app import
# (trade_in.py imports generate_trade_in_pdf at module load).
_wp_mock = MagicMock()
sys.modules.setdefault("weasyprint", _wp_mock)
sys.modules.setdefault("weasyprint.CSS", _wp_mock)
sys.modules.setdefault("weasyprint.HTML", _wp_mock)

import pytest  # noqa: E402
from fastapi import FastAPI  # noqa: E402
from fastapi.testclient import TestClient  # noqa: E402

_ESTIMATE_ID = "22222222-2222-2222-2222-222222222222"


@pytest.fixture(autouse=True)
def _restore_get_role():
    """Restore app.core.auth.get_role after each test (mirror test_estimate_idor)."""
    from app.core import auth as auth_mod

    original = auth_mod.get_role
    yield
    auth_mod.get_role = original


@pytest.fixture()
def trade_in_app() -> FastAPI:
    """Minimal FastAPI app mounting only the trade-in router."""
    from app.api.v1 import trade_in as trade_in_module

    application = FastAPI()
    application.include_router(trade_in_module.router, prefix="/api/v1/trade-in")
    return application


def _client_with(app: FastAPI, db_mock: MagicMock, role: str | None) -> TestClient:
    """Override get_db with *db_mock*; patch get_role to return *role* (or raise KeyError)."""
    from app.core.db import get_db

    def _override_db():
        yield db_mock

    app.dependency_overrides[get_db] = _override_db

    auth_mod = sys.modules["app.core.auth"]
    if role is None:

        def _raise_keyerror(_u: str):
            raise KeyError(_u)

        auth_mod.get_role = _raise_keyerror  # type: ignore[assignment]
    else:
        auth_mod.get_role = lambda _u: role  # type: ignore[assignment]
    return TestClient(app)


def _fetchone_result(row: object) -> MagicMock:
    r = MagicMock()
    r.fetchone.return_value = row
    return r


def _scalar_result(value: object) -> MagicMock:
    r = MagicMock()
    r.scalar.return_value = value
    return r


def _mapping_result(rows: list[dict]) -> MagicMock:
    r = MagicMock()
    r.mappings.return_value.all.return_value = rows
    return r


def _db_with(*results: MagicMock) -> MagicMock:
    db = MagicMock()
    db.execute.side_effect = list(results)
    return db


# ── IDOR guard ────────────────────────────────────────────────────────────────


def test_location_coef_other_pilot_gets_404(trade_in_app: FastAPI) -> None:
    """Non-owner pilot must NOT read someone else's location-coef → 404."""
    db = _db_with(_fetchone_result(SimpleNamespace(created_by="victim")))
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "attacker"},
    )
    assert resp.status_code == 404


def test_location_coef_requires_authenticated_user(trade_in_app: FastAPI) -> None:
    """No X-Authenticated-User header → 401 (guard query already ran, header check fails)."""
    db = _db_with(_fetchone_result(SimpleNamespace(created_by="kopylov")))
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID},
    )
    assert resp.status_code == 401


def test_location_coef_unknown_estimate_returns_404(trade_in_app: FastAPI) -> None:
    db = _db_with(_fetchone_result(None))
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 404


def test_location_coef_admin_can_read_any(trade_in_app: FastAPI) -> None:
    """Admin reads any estimate's location-coef regardless of owner → 200."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="someone_else")),  # guard
        _fetchone_result(SimpleNamespace(lat=None, lon=None, median_price=5_000_000)),  # target
    )
    client = _client_with(trade_in_app, db, role="admin")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "admin"},
    )
    assert resp.status_code == 200


# ── Graceful fallback ───────────────────────────────────────────────────────


def test_location_coef_no_lat_lon_returns_unavailable(trade_in_app: FastAPI) -> None:
    """Estimate without lat/lon → unavailable fallback, no osm_poi_ekb_local query at all."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=None, lon=None, median_price=5_000_000)),  # target
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["geo_source"] == "unavailable"
    assert body["coef"] == 1.0
    assert body["factors"] == []
    assert body["base_price_rub"] == 5_000_000
    assert body["result_price_rub"] == 5_000_000
    # Short-circuits before compute_location_coef: only guard + target queries ran.
    assert db.execute.call_count == 2


def test_location_coef_empty_mirror_returns_unavailable(trade_in_app: FastAPI) -> None:
    """osm_poi_ekb_local not yet refreshed (count=0) → unavailable, no fabricated factors."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=56.84, lon=60.6, median_price=5_000_000)),  # target
        _scalar_result(0),  # osm_poi_ekb_local count
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["geo_source"] == "unavailable"
    assert body["coef"] == 1.0
    assert body["factors"] == []
    assert body["result_price_rub"] == body["base_price_rub"]


# ── Happy path ────────────────────────────────────────────────────────────────


def test_location_coef_happy_path_computes_result_price(trade_in_app: FastAPI) -> None:
    """POI found within radius → coef applied to base_price_rub, factors surfaced."""
    poi_rows = [
        {"name": "Школа №1", "category": "school", "distance_m": 300.0},
        {"name": "Метро Ботаническая", "category": "metro_stop", "distance_m": 150.0},
    ]
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=56.84, lon=60.6, median_price=5_000_000)),  # target
        _scalar_result(1000),  # osm_poi_ekb_local count
        _mapping_result(poi_rows),  # nearest POI
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["geo_source"] == "osm_poi_ekb"
    assert body["base_price_rub"] == 5_000_000
    assert 0.95 <= body["coef"] <= 1.05
    assert body["result_price_rub"] == round(5_000_000 * body["coef"])
    assert len(body["factors"]) == 2
    poi_types = {f["poi_type"] for f in body["factors"]}
    assert poi_types == {"school", "metro_stop"}


def test_location_coef_radius_m_clamped_to_bounds(trade_in_app: FastAPI) -> None:
    """Explicit radius_m outside [500, 3000] is clamped before hitting the DB query."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=56.84, lon=60.6, median_price=5_000_000)),  # target
        _scalar_result(10),  # osm_poi_ekb_local count
        _mapping_result([]),  # nearest POI query
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-coef",
        params={"estimate_id": _ESTIMATE_ID, "radius_m": 50},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    # 4th execute() call is the nearest-POI query — inspect its bound radius_m param.
    nearest_call = db.execute.call_args_list[3]
    params = (
        nearest_call.args[1] if len(nearest_call.args) > 1 else nearest_call.kwargs.get("params")
    )
    assert params["radius_m"] == 500
