"""Tests for cian_detail.py — Stage 5 of CianScraper v1."""
from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from app.services.scrapers.cian_detail import (
    DetailEnrichment,
    _extract_agent_profile,
    _extract_price_changes,
    _extract_repair_state,
    _parse_views,
    fetch_detail,
)

# ── _parse_views ──────────────────────────────────────────────────────────────


def test_parse_views_space_formatted():
    assert _parse_views("1 234") == 1234


def test_parse_views_plain():
    assert _parse_views("567") == 567


def test_parse_views_none():
    assert _parse_views(None) is None


def test_parse_views_invalid():
    assert _parse_views("invalid") is None


def test_parse_views_comma_formatted():
    assert _parse_views("1,234") == 1234


# ── _extract_repair_state — структурное поле в приоритете, fallback на текст (#622) ──


def test_repair_state_structured_wins_over_description():
    """repairType присутствует → используется он, описание игнорируется."""
    offer = {
        "repairType": "cosmetic",  # → standard
        "description": "Шикарный дизайнерский ремонт",  # инференс дал бы excellent
    }
    assert _extract_repair_state(offer) == "standard"


def test_repair_state_falls_back_to_description_when_structured_missing():
    """repairType отсутствует → инференс из описания."""
    offer = {
        "repairType": None,
        "description": "Сделан свежий евроремонт, заезжай и живи",
    }
    assert _extract_repair_state(offer) == "good"


def test_repair_state_none_when_both_missing():
    """Нет structured и нет сигнала в тексте → None (не фабрикуем)."""
    offer = {"description": "Светлая квартира рядом с парком"}
    assert _extract_repair_state(offer) is None


# ── _extract_price_changes ────────────────────────────────────────────────────


def test_extract_price_changes_normalizes():
    offer = {
        "priceChanges": [
            {"changeTime": "2026-04-01T00:00:00Z", "price": 5000000, "diffPercent": -2.5},
            {"changeTime": "2026-04-10T00:00:00Z", "price": 4900000, "diffPercent": -2.0},
        ]
    }
    result = _extract_price_changes(offer, {})
    assert len(result) == 2
    assert result[0]["change_time"] == "2026-04-01T00:00:00Z"
    assert result[0]["price_rub"] == 5000000
    assert result[0]["diff_percent"] == -2.5


def test_extract_price_changes_handles_empty_offer():
    assert _extract_price_changes({}, {}) == []


def test_extract_price_changes_handles_none_value():
    assert _extract_price_changes({"priceChanges": None}, {}) == []


def test_extract_price_changes_falls_back_to_state():
    state = {
        "priceChanges": [
            {"changeTime": "2026-03-01T00:00:00Z", "price": 6000000, "diffPercent": 0},
        ]
    }
    result = _extract_price_changes({}, state)
    assert len(result) == 1
    assert result[0]["price_rub"] == 6000000


def test_extract_price_changes_skips_non_dict_entries():
    offer = {"priceChanges": [{"changeTime": "2026-04-01T00:00:00Z", "price": 5000000}, "bad"]}
    result = _extract_price_changes(offer, {})
    assert len(result) == 1


# ── _extract_agent_profile ────────────────────────────────────────────────────


def test_extract_agent_profile_from_agent_key():
    offer = {
        "agent": {
            "id": 12345,
            "companyName": "Новосёл",
            "isPro": True,
            "skills": [{"id": 1, "name": "Продажа"}],
            "accountType": "specialist",
        }
    }
    profile = _extract_agent_profile(offer)
    assert profile is not None
    assert profile["ext_agent_id"] == 12345
    assert profile["company_name"] == "Новосёл"
    assert profile["is_pro"] is True
    assert profile["account_type"] == "specialist"


def test_extract_agent_profile_from_user_key():
    offer = {
        "user": {
            "cianUserId": 99999,
            "companyName": "Агентство Икс",
            "isPro": False,
            "accountType": "agency",
        }
    }
    profile = _extract_agent_profile(offer)
    assert profile is not None
    assert profile["ext_agent_id"] == 99999
    assert profile["company_name"] == "Агентство Икс"


def test_extract_agent_profile_returns_none_when_no_agent():
    assert _extract_agent_profile({}) is None
    assert _extract_agent_profile({"agent": {}, "user": {}}) is None


# ── DetailEnrichment defaults ─────────────────────────────────────────────────


def test_dataclass_defaults():
    enrich = DetailEnrichment()
    assert enrich.cian_id is None
    assert enrich.price_changes == []
    assert enrich.raw_sister_states == {}
    assert enrich.bti_data is None
    assert enrich.agent_profile is None
    assert enrich.newbuilding_data is None


# ── fetch_detail integration (mocked) ────────────────────────────────────────


@pytest.mark.asyncio
async def test_fetch_detail_returns_none_on_no_state(monkeypatch):
    """If state extraction returns None → fetch_detail returns None gracefully."""
    monkeypatch.setattr(
        "app.services.scrapers.cian_detail.extract_state",
        lambda html, mfe, key: None,
    )

    response = MagicMock()
    response.status_code = 200
    response.text = "<html></html>"

    session = MagicMock()
    session.get = AsyncMock(return_value=response)
    session.close = AsyncMock()

    result = await fetch_detail("https://ekb.cian.ru/sale/flat/123/", session=session)
    assert result is None


@pytest.mark.asyncio
async def test_fetch_detail_returns_none_on_non_200(monkeypatch):
    """Non-200 HTTP response → returns None, no exception raised."""
    response = MagicMock()
    response.status_code = 403
    response.text = ""

    session = MagicMock()
    session.get = AsyncMock(return_value=response)
    session.close = AsyncMock()

    result = await fetch_detail("https://ekb.cian.ru/sale/flat/456/", session=session)
    assert result is None


@pytest.mark.asyncio
async def test_fetch_detail_extracts_core_fields(monkeypatch):
    """Verify core fields are extracted correctly from mocked state."""
    fake_state = {
        "offerData": {
            "offer": {
                "cianId": 327830237,
                "windowsViewType": "yardAndStreet",
                "separateWcsCount": 1,
                "combinedWcsCount": 0,
                "ceilingHeight": 2.65,
                "repairType": "cosmetic",
                "priceChanges": [
                    {
                        "changeTime": "2026-04-01T00:00:00Z",
                        "price": 5500000,
                        "diffPercent": -1.8,
                    }
                ],
                "agent": {
                    "id": 7654321,
                    "companyName": "Первичка Плюс",
                    "isPro": True,
                    "skills": [],
                    "accountType": "specialist",
                },
            },
            "stats": {
                "totalViewsFormattedString": "1 042",
                "todayViewsFormattedString": "7",
            },
        }
    }

    monkeypatch.setattr(
        "app.services.scrapers.cian_detail.extract_state",
        lambda html, mfe, key: fake_state if key == "defaultState" else None,
    )
    monkeypatch.setattr(
        "app.services.scrapers.cian_detail.extract_all_states",
        lambda html: {},
    )

    response = MagicMock()
    response.status_code = 200
    response.text = "<html></html>"

    session = MagicMock()
    session.get = AsyncMock(return_value=response)
    session.close = AsyncMock()

    result = await fetch_detail("https://ekb.cian.ru/sale/flat/327830237/", session=session)
    assert result is not None
    assert result.cian_id == 327830237
    assert result.windows_view_type == "yardAndStreet"
    assert result.separate_wcs_count == 1
    assert result.combined_wcs_count == 0
    assert result.ceiling_height == 2.65
    assert result.repair_type == "cosmetic"
    assert result.views_total == 1042
    assert result.views_today == 7
    assert len(result.price_changes) == 1
    assert result.price_changes[0]["price_rub"] == 5500000
    assert result.agent_profile is not None
    assert result.agent_profile["ext_agent_id"] == 7654321
    assert result.agent_profile["company_name"] == "Первичка Плюс"


@pytest.mark.asyncio
async def test_fetch_detail_extracts_bti_from_sister_states(monkeypatch):
    """BTI sister state is captured into bti_data."""
    fake_state = {
        "offerData": {
            "offer": {
                "cianId": 111111,
            }
        }
    }
    fake_all_states = {
        "frontend-offer-card": {
            "bti": {
                "houseData": {
                    "seriesName": "П-44Т",
                    "floorsCount": 17,
                    "flatCount": 120,
                }
            }
        }
    }

    monkeypatch.setattr(
        "app.services.scrapers.cian_detail.extract_state",
        lambda html, mfe, key: fake_state if key == "defaultState" else None,
    )
    monkeypatch.setattr(
        "app.services.scrapers.cian_detail.extract_all_states",
        lambda html: fake_all_states,
    )

    response = MagicMock()
    response.status_code = 200
    response.text = "<html></html>"

    session = MagicMock()
    session.get = AsyncMock(return_value=response)
    session.close = AsyncMock()

    result = await fetch_detail("https://ekb.cian.ru/sale/flat/111111/", session=session)
    assert result is not None
    assert result.bti_data is not None
    assert result.bti_data["seriesName"] == "П-44Т"
    assert "bti" in result.raw_sister_states


@pytest.mark.asyncio
async def test_fetch_detail_extracts_newbuilding_link(monkeypatch):
    """Newbuilding reference is captured from offer.newbuilding."""
    fake_state = {
        "offerData": {
            "offer": {
                "cianId": 222222,
                "newbuilding": {
                    "id": 54016,
                    "name": "Екатерининский парк",
                    "url": "https://zhk-ekaterininskiy-park-ekb-i.cian.ru/",
                },
            }
        }
    }

    monkeypatch.setattr(
        "app.services.scrapers.cian_detail.extract_state",
        lambda html, mfe, key: fake_state if key == "defaultState" else None,
    )
    monkeypatch.setattr(
        "app.services.scrapers.cian_detail.extract_all_states",
        lambda html: {},
    )

    response = MagicMock()
    response.status_code = 200
    response.text = "<html></html>"

    session = MagicMock()
    session.get = AsyncMock(return_value=response)
    session.close = AsyncMock()

    result = await fetch_detail("https://ekb.cian.ru/sale/flat/222222/", session=session)
    assert result is not None
    assert result.newbuilding_data is not None
    assert result.newbuilding_data["id"] == 54016
