"""Tests for /api/v1/search — golden path + filters + cache hit/miss."""

from __future__ import annotations

from unittest.mock import AsyncMock

import pytest
from fastapi.testclient import TestClient

from app.main import app
from app.schemas.search import SearchParams
from app.services.cache import get_search_cache
from app.services.search_query import build_count_query, build_search_query


def test_build_query_minimal():
    sql, args = build_search_query(SearchParams())
    assert "FROM listings_search_mv" in sql
    assert "WHERE 1=1" in sql
    assert "scraped_at DESC" in sql
    assert args["limit"] == 50
    assert args["offset"] == 0


def test_build_query_geo_radius():
    sql, args = build_search_query(
        SearchParams(lat=56.838, lon=60.594, radius_m=1500, sort="dist_asc")
    )
    assert "ST_DWithin" in sql
    assert "CAST(:lat AS double precision)" in sql
    assert args["lat"] == 56.838
    assert args["radius_m"] == 1500


def test_build_query_all_filters():
    params = SearchParams(
        rooms=2,
        area_m2_min=40,
        area_m2_max=80,
        price_rub_max=10_000_000,
        year_built_min=2000,
        has_kadastr=True,
        multi_source_only=True,
        require_avito=True,
        address_query="Малышева",
    )
    sql, args = build_search_query(params)
    assert "rooms = CAST(:rooms AS integer)" in sql
    assert "total_area >= CAST(:area_min AS double precision)" in sql
    assert "price_rub <= CAST(:price_max AS bigint)" in sql
    assert "year_built >= CAST(:yb_min AS integer)" in sql
    assert "cadastral_number IS NOT NULL" in sql
    assert "source_count >= 2" in sql
    assert "has_avito = true" in sql
    assert "address ILIKE" in sql
    assert args["addr_like"] == "%Малышева%"


def test_build_query_sources_array():
    sql, args = build_search_query(SearchParams(sources=["avito", "cian"]))
    assert "sources && CAST(:sources AS text[])" in sql
    assert args["sources"] == ["avito", "cian"]


def test_build_query_segment_default_vtorichka():
    """Без параметра segment — back-compat: canon-предикат вторички (#1186/#1188)."""
    sql, _ = build_search_query(SearchParams())
    assert SearchParams().segment == "vtorichka"
    assert "listing_id IN (SELECT id FROM listings WHERE" in sql
    assert "(listing_segment IS NULL OR listing_segment = 'vtorichka')" in sql


def test_build_query_segment_novostroyki():
    """segment=novostroyki — только первичка, без NULL-вторички."""
    sql, _ = build_search_query(SearchParams(segment="novostroyki"))
    assert "listing_segment = 'novostroyki'" in sql
    # Не должен попасть canon-guard вторички.
    assert "(listing_segment IS NULL OR listing_segment = 'vtorichka')" not in sql


def test_build_query_segment_all_no_filter():
    """segment=all — без фильтрации по сегменту."""
    sql, _ = build_search_query(SearchParams(segment="all"))
    assert "listing_segment" not in sql


def test_build_count_query_inherits_segment():
    """COUNT-запрос наследует segment-фильтр из build_search_query."""
    sql, _ = build_count_query(SearchParams(segment="novostroyki"))
    assert "listing_segment = 'novostroyki'" in sql
    sql_v, _ = build_count_query(SearchParams())
    assert "(listing_segment IS NULL OR listing_segment = 'vtorichka')" in sql_v


def test_segment_invalid_rejected():
    """Невалидное значение segment отклоняется валидацией."""
    with pytest.raises(ValueError):
        SearchParams(segment="kommercia")


def test_build_count_query_strips_limit():
    sql, args = build_count_query(SearchParams(rooms=2, page=3))
    assert "count(*)" in sql
    assert "ORDER BY" not in sql
    assert "LIMIT" not in sql
    assert "offset" not in args
    assert "limit" not in args


def test_geo_pair_required():
    with pytest.raises(ValueError, match="lat and lon"):
        SearchParams(lat=56.0)


def test_dist_sort_requires_geo():
    with pytest.raises(ValueError, match="dist_asc"):
        SearchParams(sort="dist_asc")


def test_price_range_consistency():
    with pytest.raises(ValueError, match="price_rub_min > price_rub_max"):
        SearchParams(price_rub_min=10_000_000, price_rub_max=5_000_000)


def test_page_size_capped():
    with pytest.raises(ValueError):
        SearchParams(page_size=500)


@pytest.fixture(autouse=True)
def _reset_cache_singleton(monkeypatch):
    get_search_cache.cache_clear()
    mock = AsyncMock()
    mock.get = AsyncMock(return_value=None)
    mock.set = AsyncMock()
    mock.search_key = lambda p: "tradein:search:test"
    mock.TTL_SEARCH = 300
    monkeypatch.setattr("app.api.v1.search.get_search_cache", lambda: mock)
    return mock


def test_search_cache_hit(_reset_cache_singleton):
    cached_payload = {
        "items": [],
        "total": 0,
        "page": 1,
        "page_size": 50,
        "elapsed_ms": 0.0,
        "cache_hit": True,
    }
    _reset_cache_singleton.get = AsyncMock(return_value=cached_payload)
    client = TestClient(app)
    r = client.post("/api/v1/search", json={"rooms": 2})
    assert r.status_code == 200
    assert r.json()["cache_hit"] is True
    _reset_cache_singleton.set.assert_not_awaited()
