"""Tests for GET /api/v1/trade-in/sales-vs-listings endpoint (PR K, issue #564).

Covers:
  - Happy path: deal + matching listing → pair with discount_pct.
  - Empty result: extracted street, no DB rows.
  - Multiple deals: ordered by deal_date DESC.
  - LEFT JOIN semantics: deal без listing match → listing_* поля = None.
  - linkage_rate_pct computation.
  - median_discount_pct on subset с listing_id != None.
  - extract_street_name failure → returns empty response with street=None.
"""

import os
import sys
from unittest.mock import MagicMock

# psycopg v3 driver (psycopg2 not installed)
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.
_wp_mock = MagicMock()
sys.modules.setdefault("weasyprint", _wp_mock)
sys.modules.setdefault("weasyprint.CSS", _wp_mock)
sys.modules.setdefault("weasyprint.HTML", _wp_mock)

from datetime import date  # noqa: E402

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

# ── Helpers ───────────────────────────────────────────────────────────────────


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

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

    def _override_db():
        yield MagicMock()

    application.dependency_overrides[get_db] = _override_db
    return application


def _make_db_mock(rows) -> MagicMock:
    """DB session mock returning *rows* from .mappings().all()."""
    db = MagicMock()
    mapping_result = MagicMock()
    mapping_result.all.return_value = rows
    execute_result = MagicMock()
    execute_result.mappings.return_value = mapping_result
    db.execute.return_value = execute_result
    return db


def _make_pair_row(
    *,
    deal_id: int = 100,
    deal_date: date | None = None,
    deal_price_rub: int = 5_000_000,
    deal_price_per_m2: int = 100_000,
    deal_area_m2: float = 50.0,
    deal_rooms: int = 2,
    deal_floor: int | None = 5,
    deal_address: str = "г. Екатеринбург, ул. Малышева",
    listing_id: int | None = 200,
    listing_source: str | None = "avito",
    listing_source_url: str | None = "https://avito.ru/spb/123",
    listing_date: date | None = None,
    listing_price_rub: int | None = 5_200_000,
    listing_price_per_m2: int | None = 104_000,
    listing_area_m2: float | None = 50.0,
    days_listing_to_deal: int | None = 45,
    discount_pct: float | None = -3.85,
) -> dict:
    """Mock row из SQL-function street_sales_vs_listings()."""
    return {
        "deal_id": deal_id,
        "deal_date": deal_date or date(2025, 6, 1),
        "deal_price_rub": deal_price_rub,
        "deal_price_per_m2": deal_price_per_m2,
        "deal_area_m2": deal_area_m2,
        "deal_rooms": deal_rooms,
        "deal_floor": deal_floor,
        "deal_address": deal_address,
        "listing_id": listing_id,
        "listing_source": listing_source,
        "listing_source_url": listing_source_url,
        "listing_date": listing_date,
        "listing_price_rub": listing_price_rub,
        "listing_price_per_m2": listing_price_per_m2,
        "listing_area_m2": listing_area_m2,
        "days_listing_to_deal": days_listing_to_deal,
        "discount_pct": discount_pct,
    }


def _override_db(trade_in_app: FastAPI, db_mock: MagicMock) -> None:
    from app.core.db import get_db

    def _override():
        yield db_mock

    trade_in_app.dependency_overrides[get_db] = _override


# ── Test: street extraction failure → empty response ─────────────────────────


def test_sales_vs_listings_returns_empty_when_no_street(trade_in_app: FastAPI) -> None:
    """Address без recognisable улицы → street=None, total_deals=0, pairs=[]."""
    db_mock = _make_db_mock([])
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={"address": "Industrial zone X", "area_m2": 50.0, "rooms": 2},
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["street"] is None
    assert data["total_deals"] == 0
    assert data["deals_with_listings"] == 0
    assert data["linkage_rate_pct"] == 0.0
    assert data["pairs"] == []
    # DB не должна быть вызвана — early return при пустой улице
    db_mock.execute.assert_not_called()


# ── Test: empty DB result for valid street ───────────────────────────────────


def test_sales_vs_listings_no_matches(trade_in_app: FastAPI) -> None:
    """Valid street, но DB вернул 0 строк → пустой ответ с известной street."""
    db_mock = _make_db_mock([])
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Космонавтов, 50",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["street"] == "Космонавтов"
    assert data["total_deals"] == 0
    assert data["deals_with_listings"] == 0
    assert data["linkage_rate_pct"] == 0.0
    assert data["median_discount_pct"] is None
    assert data["pairs"] == []


# ── Test: happy path — one deal + one matching listing ───────────────────────


def test_sales_vs_listings_happy_path(trade_in_app: FastAPI) -> None:
    """Single (deal, listing) пара возвращается с правильным discount_pct."""
    fixture_rows = [
        _make_pair_row(
            deal_id=1001,
            deal_date=date(2025, 8, 15),
            deal_price_rub=4_900_000,
            deal_area_m2=50.0,
            listing_id=2001,
            listing_source="avito",
            listing_date=date(2025, 6, 30),
            listing_price_rub=5_200_000,
            days_listing_to_deal=46,
            discount_pct=-5.77,
        ),
    ]
    db_mock = _make_db_mock(fixture_rows)
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Малышева, 125",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["street"] == "Малышева"
    assert data["total_deals"] == 1
    assert data["deals_with_listings"] == 1
    assert data["linkage_rate_pct"] == 100.0
    assert data["median_discount_pct"] == -5.77
    assert len(data["pairs"]) == 1
    pair = data["pairs"][0]
    assert pair["deal_id"] == 1001
    assert pair["listing_id"] == 2001
    assert pair["listing_source"] == "avito"
    assert pair["days_listing_to_deal"] == 46
    assert pair["discount_pct"] == -5.77


# ── Test: LEFT JOIN — deal без listing match ─────────────────────────────────


def test_sales_vs_listings_left_join_no_listing(trade_in_app: FastAPI) -> None:
    """Сделка без matching listing → listing_* поля None, linkage_rate counted."""
    fixture_rows = [
        _make_pair_row(
            deal_id=1001,
            listing_id=2001,
            listing_source="avito",
            discount_pct=-5.0,
        ),
        _make_pair_row(
            deal_id=1002,
            deal_date=date(2025, 7, 1),
            listing_id=None,
            listing_source=None,
            listing_source_url=None,
            listing_date=None,
            listing_price_rub=None,
            listing_price_per_m2=None,
            listing_area_m2=None,
            days_listing_to_deal=None,
            discount_pct=None,
        ),
    ]
    db_mock = _make_db_mock(fixture_rows)
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Малышева, 125",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["total_deals"] == 2
    assert data["deals_with_listings"] == 1
    assert data["linkage_rate_pct"] == 50.0
    # median считается только по парам с discount_pct
    assert data["median_discount_pct"] == -5.0
    # Pair without listing
    pair_no_listing = next(p for p in data["pairs"] if p["deal_id"] == 1002)
    assert pair_no_listing["listing_id"] is None
    assert pair_no_listing["listing_source"] is None
    assert pair_no_listing["listing_price_rub"] is None
    assert pair_no_listing["discount_pct"] is None


# ── Test: median discount with multiple pairs ────────────────────────────────


def test_sales_vs_listings_median_discount(trade_in_app: FastAPI) -> None:
    """Median считается через _percentile(0.5) только по парам c discount_pct."""
    # Discounts: [-10, -5, 0, 3, 7] → median = 0
    fixture_rows = [
        _make_pair_row(deal_id=1, listing_id=11, discount_pct=-10.0),
        _make_pair_row(deal_id=2, listing_id=12, discount_pct=-5.0),
        _make_pair_row(deal_id=3, listing_id=13, discount_pct=0.0),
        _make_pair_row(deal_id=4, listing_id=14, discount_pct=3.0),
        _make_pair_row(deal_id=5, listing_id=15, discount_pct=7.0),
        # Сделка без listing — не учитывается в median.
        _make_pair_row(
            deal_id=6,
            listing_id=None,
            listing_price_rub=None,
            discount_pct=None,
        ),
    ]
    db_mock = _make_db_mock(fixture_rows)
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Космонавтов, 50",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["total_deals"] == 6
    assert data["deals_with_listings"] == 5
    assert round(data["linkage_rate_pct"], 1) == 83.3
    assert data["median_discount_pct"] == 0.0


# ── Test: SQL function called with proper params ─────────────────────────────


def test_sales_vs_listings_passes_proper_params(trade_in_app: FastAPI) -> None:
    """Endpoint должен вызвать street_sales_vs_listings() c кастомными window_days,
    area_tolerance, period_months."""
    db_mock = _make_db_mock([])
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Малышева, 125",
            "area_m2": 65.5,
            "rooms": 3,
            "window_days": 90,
            "area_tolerance": 0.05,
            "period_months": 12,
        },
    )
    assert resp.status_code == 200
    # Проверяем что DB вызвалась с правильными params
    assert db_mock.execute.called
    args, kwargs = db_mock.execute.call_args
    params = args[1] if len(args) > 1 else kwargs.get("parameters", {})
    assert params["street_pattern"] == "%Малышева%"
    assert params["area_m2"] == 65.5
    assert params["rooms"] == 3
    assert params["window_days"] == 90
    assert params["area_tolerance"] == 0.05
    assert params["period_months"] == 12


# ── Test: response shape (Pydantic validation) ───────────────────────────────


def test_sales_vs_listings_response_shape(trade_in_app: FastAPI) -> None:
    """Все required fields присутствуют в JSON response."""
    fixture_rows = [
        _make_pair_row(deal_id=1, listing_id=11, discount_pct=-3.0, listing_source="cian"),
    ]
    db_mock = _make_db_mock(fixture_rows)
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Космонавтов, 50",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    # Top-level keys
    expected_keys = {
        "street",
        "period_months",
        "window_days",
        "area_tolerance",
        "total_deals",
        "deals_with_listings",
        "linkage_rate_pct",
        "median_discount_pct",
        "pairs",
    }
    assert expected_keys.issubset(data.keys())
    # Pair keys
    pair = data["pairs"][0]
    pair_keys = {
        "deal_id",
        "deal_date",
        "deal_price_rub",
        "deal_price_per_m2",
        "deal_area_m2",
        "deal_rooms",
        "deal_floor",
        "deal_address",
        "listing_id",
        "listing_source",
        "listing_source_url",
        "listing_date",
        "listing_price_rub",
        "listing_price_per_m2",
        "listing_area_m2",
        "days_listing_to_deal",
        "discount_pct",
    }
    assert pair_keys.issubset(pair.keys())
    assert pair["listing_source"] == "cian"


# ── Test: honest deal_date_precision marking (#1995) ─────────────────────────


def test_sales_vs_listings_marks_deal_date_precision_quarter(
    trade_in_app: FastAPI,
) -> None:
    """#1995: street_sales_vs_listings() возвращает ТОЛЬКО rosreestr-сделки —
    deal_date = period_start_date (первый день квартала), не реальная дата
    регистрации (см. data/sql/01_schema_rosreestr_deals.sql). Live-аудит prod
    подтвердил: 9 загруженных кварталов, ровно 1 distinct deal_date на квартал —
    это НЕ баг/заглушка, а granularity источника. Каждая пара должна нести
    честный deal_date_precision="quarter", даже когда (как в prod) МНОГО разных
    сделок совпадают по deal_date."""
    same_quarter_date = date(2026, 1, 1)
    fixture_rows = [
        _make_pair_row(deal_id=1, deal_date=same_quarter_date, listing_id=11),
        _make_pair_row(deal_id=2, deal_date=same_quarter_date, listing_id=12),
        _make_pair_row(deal_id=3, deal_date=same_quarter_date, listing_id=13),
    ]
    db_mock = _make_db_mock(fixture_rows)
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Космонавтов, 50",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert len(data["pairs"]) == 3
    for pair in data["pairs"]:
        assert pair["deal_date"] == "2026-01-01"
        assert pair["deal_date_precision"] == "quarter"


# ── Test: default param values ───────────────────────────────────────────────


def test_sales_vs_listings_defaults(trade_in_app: FastAPI) -> None:
    """window_days=180, area_tolerance=0.15, period_months=24 — defaults."""
    db_mock = _make_db_mock([])
    _override_db(trade_in_app, db_mock)

    client = TestClient(trade_in_app)
    resp = client.get(
        "/api/v1/trade-in/sales-vs-listings",
        params={
            "address": "г. Екатеринбург, ул. Малышева, 125",
            "area_m2": 50.0,
            "rooms": 2,
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["window_days"] == 180
    assert data["area_tolerance"] == 0.15
    assert data["period_months"] == 24
