"""Tests for GET /api/v1/trade-in/history scoping (#656).

Закрывает cross-pilot data-leak: non-admin видит только свои оценки
(created_by = X-Authenticated-User), admin видит все либо фильтрует по ?account.

Проверяем САМ скоупинг через перехват SQL/params, переданных в db.execute —
реальная БД не нужна (DB + get_role мокируются).
"""

from __future__ import annotations

import os
import sys
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.
_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


@pytest.fixture(autouse=True)
def _restore_get_role():
    """Restore app.core.auth.get_role after each test.

    `_client_with` monkeypatches `get_role` via raw attribute assignment; without
    this teardown the patch leaked into the whole-suite run and broke test_rbac.py
    (kopylov resolved as 'admin', unknown users no longer raised KeyError).
    """
    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 _make_db_mock(rows: list[dict]) -> 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 _captured_sql_and_params(db_mock: MagicMock) -> tuple[str, dict]:
    """Extract the SQL text + params dict from the single db.execute call."""
    args, _ = db_mock.execute.call_args
    sql = str(args[0])
    params = args[1]
    return sql, params


def _client_with(app: FastAPI, db_mock: MagicMock, role: str) -> TestClient:
    """Override get_db with *db_mock* and patch get_role to return *role*."""
    from app.api.v1 import trade_in as trade_in_module
    from app.core.db import get_db

    def _override_db():
        yield db_mock

    app.dependency_overrides[get_db] = _override_db
    # estimate_history imports get_role from app.core.auth at call time → patch source.
    trade_in_module_auth = sys.modules["app.core.auth"]
    trade_in_module_auth.get_role = lambda _u: role  # type: ignore[assignment]
    _ = trade_in_module  # keep router import side-effect explicit
    return TestClient(app)


def test_history_requires_authenticated_user(trade_in_app: FastAPI) -> None:
    """No X-Authenticated-User header → 401 (mirror /me)."""
    db_mock = _make_db_mock([])
    from app.core.db import get_db

    def _override_db():
        yield db_mock

    trade_in_app.dependency_overrides[get_db] = _override_db
    client = TestClient(trade_in_app)
    resp = client.get("/api/v1/trade-in/history")
    assert resp.status_code == 401
    # DB must not be touched when unauthenticated.
    db_mock.execute.assert_not_called()


def test_non_admin_sees_only_own_rows(trade_in_app: FastAPI) -> None:
    """Pilot user → WHERE created_by = :owner with owner == header value."""
    db_mock = _make_db_mock(
        [
            {
                "id": "1",
                "address": "A",
                "rooms": 2,
                "area_m2": 50,
                "median_price": 5_000_000,
                "confidence": "medium",
                "n_analogs": 7,
                "created_at": "2026-05-29T00:00:00",
            }
        ]
    )
    client = _client_with(trade_in_app, db_mock, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/history",
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    sql, params = _captured_sql_and_params(db_mock)
    assert "created_by = :owner" in sql
    assert params["owner"] == "kopylov"
    assert "account" not in params


def test_non_admin_account_param_is_ignored(trade_in_app: FastAPI) -> None:
    """Pilot passing ?account=victim must still be scoped to themselves."""
    db_mock = _make_db_mock([])
    client = _client_with(trade_in_app, db_mock, role="pilot")
    resp = client.get(
        "/api/v1/trade-in/history",
        params={"account": "victim"},
        headers={"X-Authenticated-User": "kopylov"},
    )
    assert resp.status_code == 200
    sql, params = _captured_sql_and_params(db_mock)
    assert "created_by = :owner" in sql
    assert params["owner"] == "kopylov"
    # The attacker-supplied account must NOT reach the query.
    assert "account" not in params
    assert "victim" not in params.values()


def test_admin_sees_all_rows(trade_in_app: FastAPI) -> None:
    """Admin without ?account → no WHERE filter (all rows)."""
    db_mock = _make_db_mock([])
    client = _client_with(trade_in_app, db_mock, role="admin")
    resp = client.get(
        "/api/v1/trade-in/history",
        headers={"X-Authenticated-User": "admin"},
    )
    assert resp.status_code == 200
    sql, params = _captured_sql_and_params(db_mock)
    assert "WHERE" not in sql.upper()
    assert "owner" not in params
    assert "account" not in params


def test_admin_filters_by_account(trade_in_app: FastAPI) -> None:
    """Admin with ?account=kopylov → WHERE created_by = :account."""
    db_mock = _make_db_mock([])
    client = _client_with(trade_in_app, db_mock, role="admin")
    resp = client.get(
        "/api/v1/trade-in/history",
        params={"account": "kopylov"},
        headers={"X-Authenticated-User": "admin"},
    )
    assert resp.status_code == 200
    sql, params = _captured_sql_and_params(db_mock)
    assert "created_by = :account" in sql
    assert params["account"] == "kopylov"
    assert "owner" not in params


def test_history_returns_full_input_params(trade_in_app: FastAPI) -> None:
    """#2417: /history проецирует floor/total_floors/year_built/house_type/

    repair_state/has_balcony в дополнение к базовому подмножеству, включая
    NULL-passthrough для строк, где эти поля никогда не заполнялись.
    """
    db_mock = _make_db_mock(
        [
            {
                "id": "1",
                "address": "A",
                "rooms": 2,
                "area_m2": 50,
                "median_price": 5_000_000,
                "confidence": "medium",
                "n_analogs": 7,
                "created_at": "2026-05-29T00:00:00",
                "sources_used": ["avito", "cian"],
                "floor": 3,
                "total_floors": 9,
                "year_built": 2005,
                "house_type": "panel",
                "repair_state": "good",
                "has_balcony": True,
            },
            {
                "id": "2",
                "address": "B",
                "rooms": 1,
                "area_m2": 33,
                "median_price": 3_000_000,
                "confidence": "low",
                "n_analogs": 2,
                "created_at": "2026-05-28T00:00:00",
                "sources_used": [],
                # legacy row / поля никогда не заполнялись пользователем.
                "floor": None,
                "total_floors": None,
                "year_built": None,
                "house_type": None,
                "repair_state": None,
                "has_balcony": None,
            },
        ]
    )
    client = _client_with(trade_in_app, db_mock, role="admin")
    resp = client.get(
        "/api/v1/trade-in/history",
        headers={"X-Authenticated-User": "admin"},
    )
    assert resp.status_code == 200

    sql, _params = _captured_sql_and_params(db_mock)
    for col in (
        "floor",
        "total_floors",
        "year_built",
        "house_type",
        "repair_state",
        "has_balcony",
    ):
        assert col in sql

    body = resp.json()
    assert len(body) == 2

    filled, legacy = body[0], body[1]
    assert filled["floor"] == 3
    assert filled["total_floors"] == 9
    assert filled["year_built"] == 2005
    assert filled["house_type"] == "panel"
    assert filled["repair_state"] == "good"
    assert filled["has_balcony"] is True

    # NULL passthrough — не должно рейзить/подменяться дефолтами.
    assert legacy["floor"] is None
    assert legacy["total_floors"] is None
    assert legacy["year_built"] is None
    assert legacy["house_type"] is None
    assert legacy["repair_state"] is None
    assert legacy["has_balcony"] is None
