"""#2464 cluster E — domrf_kn_objects snapshot dedup regression tests.

domrf_kn_objects retains MULTIPLE historical rows per obj_id (one per
snapshot_date, UNIQUE(obj_id, snapshot_date), no retention). Prod measurement:
4090 rows / 482 distinct obj_id for site_status='Строящиеся' (8.49x inflation).

Three read-side queries counted/returned every retained snapshot instead of the
latest one per obj_id:
  - _active_competitors_count (via its _q closure)
  - developer_portfolio
  - _competitors_two_dim (its `active` CTE)

All fixed with the established repo dedup pattern: DISTINCT ON (obj_id) ...
ORDER BY obj_id, snapshot_date DESC NULLS LAST (sibling: _L3_FUTURE_SQL #1212 in
app/services/site_finder/supply_layers.py, cmp_rows/latest_obj CTE in
analytics_queries.recommend_mix).

VOLATILE-FILTER PLACEMENT (authoritative prod measurement, coordinator
2026-07-08 — per-obj_id variance across snapshots): dev_id=0, region_cd=0
(STABLE) vs district_name=1, obj_class=29, site_status=11 (VOLATILE). Volatile
predicates MUST be applied to the true-latest row (outer/downstream WHERE), NOT
pre-filtered inside the DISTINCT ON CTE — else DISTINCT ON returns "the latest
snapshot that still MATCHED the filter", not the object's true latest snapshot
(a ЖК that sold out / changed class / moved districts would be miscounted by a
stale snapshot). Only the stable region_cd stays inside the CTE as the dedup
partition-scope. dev_id (developer_portfolio) is likewise stable → kept in-CTE.

Mock-based — mirrors the SQL-shape assertion convention used in
tests/services/site_finder/test_supply_layers.py (TestLayer2Hidden.
test_sql_dedups_latest_snapshot) since there's no SQLite stand-in for
Postgres-only `DISTINCT ON` syntax and no throwaway-schema DML fixture in this
repo's integration-test conventions (tests/integration/conftest.py is
EXPLAIN-only / read-only against a real tunnel DB — safe for plan-checks, not
for INSERT fixtures). A second class (`TestDedupAlgorithmSpec`) hand-replicates
the exact dedup+filter semantics the SQL implements in pure Python and asserts
the two behaviors the epic calls out explicitly: multi-snapshot obj_id counted
once, and a sold-out object with only an OLD 'Строящиеся' snapshot excluded.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock

from app.services.analytics_queries import _active_competitors_count, developer_portfolio

# ── shared helpers (mirrors test_supply_layers.py _mock_db/_executed_sql) ─────


def _mock_scalar_db(value: int) -> MagicMock:
    """Session stand-in: every db.execute(...).scalar() returns `value`."""
    db = MagicMock()
    db.execute.return_value.scalar.return_value = value
    return db


def _mock_mapping_db(rows: list[dict[str, Any]]) -> MagicMock:
    db = MagicMock()
    db.execute.return_value.mappings.return_value.all.return_value = rows
    return db


def _executed_sql(db: MagicMock, call_index: int = 0) -> str:
    args, _kwargs = db.execute.call_args_list[call_index]
    return str(args[0])


def _executed_params(db: MagicMock, call_index: int = 0) -> dict:
    args, _kwargs = db.execute.call_args_list[call_index]
    return args[1]


def _split_latest_cte(sql: str) -> tuple[str, str]:
    """Split whitespace-normalized SQL into (CTE body, outer body).

    CTE body = `WITH latest AS ( ... )` up to the `)` that closes the CTE,
    which is the first `)` after the ORDER BY feeding DISTINCT ON. Outer body =
    everything after. Lets tests assert WHICH side a predicate lives on.
    """
    norm = " ".join(sql.split())
    cte_open = norm.index("WITH latest AS (")
    order_by_idx = norm.index("ORDER BY obj_id, snapshot_date DESC NULLS LAST")
    cte_close = norm.index(")", order_by_idx)
    return norm[cte_open:cte_close], norm[cte_close:]


# ── _active_competitors_count SQL shape ────────────────────────────────────


class TestActiveCompetitorsCountSqlShape:
    def test_dedups_via_distinct_on_obj_id_latest_snapshot(self) -> None:
        # n never reaches the >=2 early-return threshold with scalar()==0, so
        # all 3 cascade tiers execute — call_index 0 is tier1 (district+class).
        db = _mock_scalar_db(0)
        _active_competitors_count(
            db, region_code=66, district_name="Ленинский", target_class="Комфорт"
        )
        sql = _executed_sql(db, 0)
        norm = " ".join(sql.split())
        assert "DISTINCT ON (obj_id)" in norm
        assert "ORDER BY obj_id, snapshot_date DESC NULLS LAST" in norm

    def test_site_status_filter_is_volatile_applied_after_distinct_on(self) -> None:
        # Regression guard for the exact bug shape #2464 warns about: a
        # sold-out object whose ONLY 'Строящиеся' row is an old snapshot must
        # not be counted. That requires site_status to be filtered on the
        # OUTER (post-DISTINCT-ON) query, never inside the `latest` CTE's
        # WHERE — else DISTINCT ON would pick "the latest snapshot that still
        # matched site_status='Строящиеся'" instead of the object's true
        # latest snapshot. (Prod per-obj_id variance: site_status=11.)
        db = _mock_scalar_db(0)
        _active_competitors_count(
            db, region_code=66, district_name="Ленинский", target_class="Комфорт"
        )
        cte_body, outer_body = _split_latest_cte(_executed_sql(db, 0))
        # site_status legitimately appears in the CTE's SELECT list (it's the
        # column DISTINCT ON needs to expose) -- what must NOT appear is a
        # filter predicate on it inside the CTE's WHERE.
        assert (
            "site_status = 'Строящиеся'" not in cte_body
        ), f"site_status must not pre-filter the DISTINCT ON CTE (volatile field):\n{cte_body}"
        assert "site_status = 'Строящиеся'" in outer_body

    def test_district_and_class_are_volatile_applied_after_distinct_on(self) -> None:
        # Companion to the site_status guard: authoritative prod measurement
        # (coordinator 2026-07-08) — per-obj_id variance across snapshots is
        # district_name=1 and obj_class=29 (NON-zero → VOLATILE), unlike
        # dev_id/region_cd (0 → stable). So district_name/obj_class predicates
        # must ALSO be applied on the true-latest row (outer WHERE), never
        # pre-filtered inside the DISTINCT ON CTE — else a ЖК that changed
        # class/district gets counted by a stale snapshot. Tier-1
        # (district+class) is call_index 0 when target_class is given.
        db = _mock_scalar_db(0)
        _active_competitors_count(
            db, region_code=66, district_name="Ленинский", target_class="Комфорт"
        )
        cte_body, outer_body = _split_latest_cte(_executed_sql(db, 0))
        # CTE WHERE must scope on ONLY the stable region_cd — no volatile predicate.
        assert (
            "district_name = :dn" not in cte_body
        ), f"district_name (volatile) must not pre-filter the DISTINCT ON CTE:\n{cte_body}"
        assert (
            "COALESCE(obj_class, obj_class_fallback) = :cls" not in cte_body
        ), f"obj_class (volatile) must not pre-filter the DISTINCT ON CTE:\n{cte_body}"
        # Both live in the outer WHERE, applied to the deduped true-latest row.
        assert "district_name = :dn" in outer_body
        assert "COALESCE(obj_class, obj_class_fallback) = :cls" in outer_body

    def test_cte_where_scopes_only_stable_region_cd(self) -> None:
        # Belt-and-suspenders: the DISTINCT ON CTE's WHERE partition-scope is
        # exactly `region_cd = :rc` and nothing else (all volatile predicates
        # relocated). Guards against a future re-introduction of a volatile
        # pre-filter that would silently pick a stale-but-matching snapshot.
        db = _mock_scalar_db(0)
        _active_competitors_count(
            db, region_code=66, district_name="Ленинский", target_class="Комфорт"
        )
        cte_body, _outer = _split_latest_cte(_executed_sql(db, 0))
        # extract the CTE's WHERE clause (between WHERE and the feeding ORDER BY,
        # which _split_latest_cte leaves at the tail of the CTE body).
        where_start = cte_body.index("WHERE ") + len("WHERE ")
        where_end = cte_body.index("ORDER BY", where_start)
        where_clause = cte_body[where_start:where_end].strip()
        assert (
            where_clause == "region_cd = :rc"
        ), f"CTE WHERE must scope on ONLY stable region_cd, got: {where_clause!r}"

    def test_no_double_colon_cast(self) -> None:
        import re

        db = _mock_scalar_db(0)
        _active_competitors_count(db, region_code=66, district_name="Ленинский", target_class=None)
        sql = _executed_sql(db, 0)
        assert re.search(r":[a-z_]+::[a-z]", sql) is None

    def test_cascade_still_stops_early_when_tier_hits_threshold(self) -> None:
        # Sanity: fallback cascade logic (n>=2 -> stop) is untouched by the dedup fix.
        db = _mock_scalar_db(5)
        n, scope = _active_competitors_count(
            db, region_code=66, district_name="Ленинский", target_class="Комфорт"
        )
        assert n == 5
        assert scope == "district+class"
        assert db.execute.call_count == 1


# ── developer_portfolio SQL shape ──────────────────────────────────────────


class TestDeveloperPortfolioSqlShape:
    def test_dedups_via_distinct_on_obj_id_latest_snapshot(self) -> None:
        db = _mock_mapping_db([])
        developer_portfolio(db, "6208_0")
        sql = _executed_sql(db, 0)
        norm = " ".join(sql.split())
        assert "DISTINCT ON (obj_id)" in norm
        assert "ORDER BY obj_id, snapshot_date DESC NULLS LAST" in norm

    def test_preserves_final_ready_dt_ordering(self) -> None:
        # DISTINCT ON's own ORDER BY must start with (obj_id, snapshot_date) —
        # the caller-facing ready_dt ordering has to live in the OUTER SELECT.
        db = _mock_mapping_db([])
        developer_portfolio(db, "6208_0")
        sql = _executed_sql(db, 0)
        norm = " ".join(sql.split())
        assert norm.rstrip().endswith("ORDER BY ready_dt DESC NULLS LAST")

    def test_dev_id_filter_unchanged(self) -> None:
        db = _mock_mapping_db([])
        developer_portfolio(db, "6208_0")
        params = _executed_params(db, 0)
        assert params == {"dev": "6208_0"}

    def test_returned_columns_unchanged(self) -> None:
        # Fix must not add/drop columns from the caller-facing shape.
        rows = [
            {
                "obj_id": 1,
                "comm_name": "ЖК Тест",
                "addr": "ул. Тест, 1",
                "region_cd": 66,
                "flat_count": 100,
                "square_living": 5000.0,
                "ready_dt": None,
                "obj_class": "Комфорт",
                "escrow": True,
                "problem_flag": None,
                "latitude": 56.8,
                "longitude": 60.6,
                "is_ekb": True,
            }
        ]
        db = _mock_mapping_db(rows)
        out = developer_portfolio(db, "6208_0")
        assert len(out) == 1
        assert set(out[0].keys()) == {
            "obj_id",
            "comm_name",
            "addr",
            "region_cd",
            "flat_count",
            "square_living",
            "ready_dt",
            "obj_class",
            "escrow",
            "problem_flag",
            "lat",
            "lon",
            "is_ekb",
        }


# ── _competitors_two_dim `active`/`latest` CTE shape ───────────────────────


def _mock_two_dim_db(radius_n: int, district_only_n: int) -> MagicMock:
    """Session stand-in for _competitors_two_dim: call 0 = centroid lookup
    (returns a non-null WKT so the main query runs), call 1 = the radius/
    district aggregate. radius_n/district_only_n chosen so total_weighted >= 1
    keeps execution off the single-dim fallback path (which would add calls)."""
    db = MagicMock()
    centroid_res = MagicMock()
    centroid_res.mappings.return_value.first.return_value = {"centroid_wkt": "POINT(60.6 56.8)"}
    main_res = MagicMock()
    main_res.mappings.return_value.first.return_value = {
        "radius_n": radius_n,
        "district_only_n": district_only_n,
    }
    db.execute.side_effect = [centroid_res, main_res]
    return db


class TestCompetitorsTwoDimActiveCte:
    """#2464 sibling: _competitors_two_dim's `active` CTE had site_status /
    district_name / obj_class ALL inside its DISTINCT ON — same latest-matching-
    not-latest bug. Fix dedups to true-latest per obj_id first (region_cd-only
    CTE scope), then applies the volatile predicates."""

    def _main_sql(self, db: MagicMock) -> str:
        # call_index 1 is the radius/district aggregate (0 is the centroid lookup).
        return " ".join(_executed_sql(db, 1).split())

    def test_dedups_via_distinct_on_before_volatile_filters(self) -> None:
        from app.services.analytics_queries import _competitors_two_dim

        db = _mock_two_dim_db(radius_n=3, district_only_n=0)
        _competitors_two_dim(db, region_code=66, district_name="Ленинский", target_class="Комфорт")
        sql = self._main_sql(db)
        assert "DISTINCT ON (obj_id)" in sql
        assert "ORDER BY obj_id, snapshot_date DESC NULLS LAST" in sql

    def test_volatile_filters_are_post_distinct_on(self) -> None:
        from app.services.analytics_queries import _competitors_two_dim

        db = _mock_two_dim_db(radius_n=3, district_only_n=0)
        _competitors_two_dim(db, region_code=66, district_name="Ленинский", target_class="Комфорт")
        sql = self._main_sql(db)
        # Split the DISTINCT ON CTE (`latest`) from everything downstream. Its
        # WHERE must scope on ONLY stable region_cd; site_status/district_name/
        # obj_class predicates live in the downstream `active` CTE.
        latest_open = sql.index("WITH latest AS (")
        order_by_idx = sql.index("ORDER BY obj_id, snapshot_date DESC NULLS LAST")
        latest_close = sql.index(")", order_by_idx)
        latest_body = sql[latest_open:latest_close]
        downstream = sql[latest_close:]

        assert "site_status = 'Строящиеся'" not in latest_body
        assert "district_name = :dn" not in latest_body
        assert "COALESCE(obj_class, obj_class_fallback) = :cls" not in latest_body

        assert "site_status = 'Строящиеся'" in downstream
        assert "district_name = :dn" in downstream
        assert "COALESCE(obj_class, obj_class_fallback) = :cls" in downstream

    def test_output_shape_preserved(self) -> None:
        from app.services.analytics_queries import _competitors_two_dim

        db = _mock_two_dim_db(radius_n=3, district_only_n=2)
        radius_n, district_only_n, total_weighted, scope = _competitors_two_dim(
            db, region_code=66, district_name="Ленинский", target_class="Комфорт"
        )
        assert radius_n == 3
        assert district_only_n == 2
        # 3*1.0 + 2*0.6 = 4.2
        assert total_weighted == 4.2
        assert scope == "district_2d"


# ── Pure-Python spec replica: exact dedup + volatile-filter algorithm ─────
#
# MagicMock can't execute real SQL (and DISTINCT ON has no SQLite equivalent),
# so this class locks in the ALGORITHM the SQL is required to implement:
# "take the row with MAX(snapshot_date) per obj_id, THEN filter on that row's
# site_status" — as opposed to the buggy "filter rows on site_status, then
# take MAX(snapshot_date) among survivors". It's a spec/regression guard, not
# a live-DB execution test (see module docstring for why).


def _latest_snapshot_active_count(rows: list[dict[str, Any]]) -> int:
    """Reference implementation of what the fixed `_q` SQL computes."""
    latest_by_obj: dict[int, dict[str, Any]] = {}
    for row in rows:
        obj_id = row["obj_id"]
        cur = latest_by_obj.get(obj_id)
        if cur is None or (row["snapshot_date"] or "") > (cur["snapshot_date"] or ""):
            latest_by_obj[obj_id] = row
    return sum(1 for r in latest_by_obj.values() if r["site_status"] == "Строящиеся")


class TestDedupAlgorithmSpec:
    def test_multi_snapshot_same_obj_id_counted_once(self) -> None:
        rows = [
            {"obj_id": 1, "snapshot_date": "2026-04-27", "site_status": "Строящиеся"},
            {"obj_id": 1, "snapshot_date": "2026-05-25", "site_status": "Строящиеся"},
            {"obj_id": 1, "snapshot_date": "2026-06-28", "site_status": "Строящиеся"},
        ]
        assert _latest_snapshot_active_count(rows) == 1

    def test_sold_out_object_with_old_active_snapshot_not_counted(self) -> None:
        # obj_id=2's LATEST snapshot says "Реализован" (sold out) even though
        # older snapshots said "Строящиеся" — must NOT count as active.
        rows = [
            {"obj_id": 2, "snapshot_date": "2026-04-27", "site_status": "Строящиеся"},
            {"obj_id": 2, "snapshot_date": "2026-05-25", "site_status": "Строящиеся"},
            {"obj_id": 2, "snapshot_date": "2026-06-28", "site_status": "Реализован"},
        ]
        assert _latest_snapshot_active_count(rows) == 0

    def test_mixed_objects_only_latest_active_ones_counted(self) -> None:
        rows = [
            # obj 1: still active across all 3 snapshots -> counts once.
            {"obj_id": 1, "snapshot_date": "2026-04-27", "site_status": "Строящиеся"},
            {"obj_id": 1, "snapshot_date": "2026-06-28", "site_status": "Строящиеся"},
            # obj 2: sold out on the latest snapshot -> excluded.
            {"obj_id": 2, "snapshot_date": "2026-04-27", "site_status": "Строящиеся"},
            {"obj_id": 2, "snapshot_date": "2026-06-28", "site_status": "Реализован"},
            # obj 3: single snapshot, active -> counts once.
            {"obj_id": 3, "snapshot_date": "2026-06-28", "site_status": "Строящиеся"},
        ]
        assert _latest_snapshot_active_count(rows) == 2
