"""Tests for GET /api/v1/trade-in/location-index (replaces test_location_coef_endpoint.py).

Mirrors the IDOR + mocked-DB conventions of test_estimate_idor.py / the deleted
test_location_coef_endpoint.py (no live Postgres needed). Covers:
  - IDOR guard (owner / other pilot 404 / admin / unauthenticated 401)
  - honest degradation: no lat/lon, point outside EKB coverage, insufficient comparable
    sample, empty osm_poi_ekb_local mirror (independent of the index status)
  - happy path: status="ok", location_index_pct computed, nearby_poi 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"
_LAT_IN_EKB = 56.838
_LON_IN_EKB = 60.605


@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_all_result(rows: list[dict]) -> MagicMock:
    r = MagicMock()
    r.mappings.return_value.all.return_value = rows
    return r


def _mapping_one_result(row: dict | None) -> MagicMock:
    r = MagicMock()
    r.mappings.return_value.first.return_value = row
    return r


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


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


def test_location_index_other_pilot_gets_404(trade_in_app: FastAPI) -> None:
    """Non-owner pilot must NOT read someone else's location-index → 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-index",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "attacker"},
    )
    assert resp.status_code == 404


def test_location_index_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-index",
        params={"estimate_id": _ESTIMATE_ID},
    )
    assert resp.status_code == 401


def test_location_index_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-index",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 404


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


# ── Honest degradation ────────────────────────────────────────────────────────


def test_location_index_no_lat_lon_returns_out_of_coverage(trade_in_app: FastAPI) -> None:
    """Estimate without lat/lon → out_of_coverage, no compute_location_index DB call at all."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=None, lon=None)),  # target
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-index",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "out_of_coverage"
    assert body["location_index_pct"] is None
    assert body["nearby_poi"] == []
    assert body["poi_status"] == "unavailable"
    # Short-circuits before compute_location_index: only guard + target queries ran.
    assert db.execute.call_count == 2


def test_location_index_point_outside_ekb_bbox_returns_out_of_coverage(
    trade_in_app: FastAPI,
) -> None:
    """Estimate has lat/lon, but outside the EKB coverage bbox (e.g. Nizhny Tagil)."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=57.910, lon=59.970)),  # target — Nizhny Tagil
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-index",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "out_of_coverage"
    assert body["location_index_pct"] is None
    # compute_location_index short-circuits — no further DB calls beyond guard + target.
    assert db.execute.call_count == 2


def test_location_index_insufficient_sample_returns_no_fabricated_number(
    trade_in_app: FastAPI,
) -> None:
    """Comparable sample stays below MIN_SAMPLE_SIZE even at max radius → insufficient_data,
    never a noisy number computed from a handful of listings."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=_LAT_IN_EKB, lon=_LON_IN_EKB)),  # target
        _scalar_result(0),  # osm_poi_ekb_local count
        _mapping_one_result({"median_ppm2": 150_000.0, "n": 4000}),  # citywide
        _mapping_one_result({"median_ppm2": 200_000.0, "n": 5}),  # 800m
        _mapping_one_result({"median_ppm2": 195_000.0, "n": 12}),  # 1500m
        _mapping_one_result({"median_ppm2": 190_000.0, "n": 15}),  # 2500m — still short
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-index",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "insufficient_data"
    assert body["location_index_pct"] is None
    assert body["local_median_price_per_m2"] is None
    assert body["sample_size"] == 15
    assert body["city_median_price_per_m2"] == 150_000


def test_location_index_poi_mirror_empty_does_not_block_index(trade_in_app: FastAPI) -> None:
    """poi_status="unavailable" is independent of status="ok" — an un-refreshed POI mirror
    must not prevent a perfectly computable price-based index."""
    db = _db_with(
        _fetchone_result(SimpleNamespace(created_by="kopylov")),  # guard
        _fetchone_result(SimpleNamespace(lat=_LAT_IN_EKB, lon=_LON_IN_EKB)),  # target
        _scalar_result(0),  # osm_poi_ekb_local count == 0
        _mapping_one_result({"median_ppm2": 150_000.0, "n": 4000}),  # citywide
        _mapping_one_result({"median_ppm2": 172_500.0, "n": 40}),  # 800m — sufficient
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-index",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "ok"
    assert body["location_index_pct"] == 15.0
    assert body["poi_status"] == "unavailable"
    assert body["nearby_poi"] == []


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


def test_location_index_happy_path(trade_in_app: FastAPI) -> None:
    """Comparable sample found → index computed, nearby POI surfaced as qualitative info."""
    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=_LAT_IN_EKB, lon=_LON_IN_EKB)),  # target
        _scalar_result(1000),  # osm_poi_ekb_local count
        _mapping_all_result(poi_rows),  # nearest POI
        _mapping_one_result({"median_ppm2": 150_000.0, "n": 4000}),  # citywide
        _mapping_one_result({"median_ppm2": 165_000.0, "n": 25}),  # 800m — sufficient
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-index",
        params={"estimate_id": _ESTIMATE_ID},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "ok"
    assert body["poi_status"] == "ok"
    assert body["location_index_pct"] == 10.0
    assert body["local_median_price_per_m2"] == 165_000
    assert body["city_median_price_per_m2"] == 150_000
    assert body["sample_size"] == 25
    assert body["radius_m"] == 800
    assert len(body["nearby_poi"]) == 2
    poi_types = {f["poi_type"] for f in body["nearby_poi"]}
    assert poi_types == {"school", "metro_stop"}
    # New contract must NOT resurrect the misleading price-multiplier fields.
    assert "coef" not in body
    assert "base_price_rub" not in body
    assert "result_price_rub" not in body


def test_location_index_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=_LAT_IN_EKB, lon=_LON_IN_EKB)),  # target
        _scalar_result(0),  # osm_poi_ekb_local count
        _mapping_one_result({"median_ppm2": 150_000.0, "n": 4000}),  # citywide
        _mapping_one_result({"median_ppm2": 160_000.0, "n": 50}),  # explicit radius query
    )
    client = _client_with(trade_in_app, db, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/location-index",
        params={"estimate_id": _ESTIMATE_ID, "radius_m": 50},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["radius_m"] == 500
    # 5th execute() call (index 4) is the explicit-radius local-median query.
    nearest_call = db.execute.call_args_list[4]
    params = (
        nearest_call.args[1] if len(nearest_call.args) > 1 else nearest_call.kwargs.get("params")
    )
    assert params["radius_m"] == 500
