"""Tests for yandex_address_backfill — pure regex extraction, no network/DB."""

from __future__ import annotations

import os
import re
import sys
from unittest.mock import AsyncMock, MagicMock, patch

# DATABASE_URL required by config before any app import.
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

# WeasyPrint stub — not installed in CI without GTK.
_wp_mock = MagicMock()
sys.modules.setdefault("weasyprint", _wp_mock)

import pytest  # noqa: E402

from app.services.yandex_address_backfill import (  # noqa: E402
    YandexAddressBackfillResult,
    _extract_address_from_title,
)

# Regex used to assert that an extracted address contains a house number.
_RE_HAS_HOUSE_NUMBER = re.compile(r",\s*\d+")


# ---------------------------------------------------------------------------
# Pure regex / extraction tests (no network, no DB)
# ---------------------------------------------------------------------------


def test_extract_address_standard_title() -> None:
    """Standard title: «Продажа квартиры — Екатеринбург, улица Горького, 36 — id 7654321»."""
    html = (
        "<title>Продажа квартиры — Екатеринбург, улица Горького, 36 "
        "— id 7654321 на Яндекс.Недвижимости</title>"
    )
    addr = _extract_address_from_title(html)
    assert addr is not None, "Should extract address from standard title"
    assert _RE_HAS_HOUSE_NUMBER.search(
        addr
    ), f"Extracted address should contain house number: {addr!r}"
    assert "Екатеринбург" in addr
    assert "Горького" in addr
    assert "36" in addr


def test_extract_address_with_zhk_prefix() -> None:
    """Title with optional ЖК prefix before city: «— ЖК Каменный Ручей, Екатеринбург, …»."""
    html = (
        "<title>Продажа квартиры — ЖК Каменный Ручей, "
        "Екатеринбург, проспект Космонавтов, 110А — id 99001 </title>"
    )
    addr = _extract_address_from_title(html)
    assert addr is not None, "Should extract address when ЖК prefix is present"
    assert _RE_HAS_HOUSE_NUMBER.search(
        addr
    ), f"Extracted address should contain house number: {addr!r}"
    assert "Екатеринбург" in addr


# ---------------------------------------------------------------------------
# City-agnostic tests — satellite towns (T10 live-bug fix)
# ---------------------------------------------------------------------------


def test_extract_address_verkhnaya_pyshma_with_zhk() -> None:
    """Real title from sweep #28: ЖК «Дуэт», Верхняя Пышма — was returning None before fix."""
    html = (
        "<title>Купить квартиру 102.4 м²"
        " — ЖК «Дуэт», Верхняя Пышма, улица Мальцева, 14"
        " — id 7533242317391171630</title>"
    )
    addr = _extract_address_from_title(html)
    assert addr is not None, "Верхняя Пышма title should produce an address"
    assert _RE_HAS_HOUSE_NUMBER.search(addr), f"Should contain house number: {addr!r}"
    assert "Верхняя Пышма" in addr
    assert "Мальцева" in addr
    assert "14" in addr


def test_extract_address_beryozovsky_with_zhk() -> None:
    """Real title from sweep #28: ЖК «Уют-Сити», Берёзовский — was returning None before fix."""
    html = (
        "<title>Купить квартиру 139.2 м²"
        " — ЖК «Уют-Сити», Берёзовский, Александровский проспект, 3"
        " — id 3988368535316036933</title>"
    )
    addr = _extract_address_from_title(html)
    assert addr is not None, "Берёзовский title should produce an address"
    assert _RE_HAS_HOUSE_NUMBER.search(addr), f"Should contain house number: {addr!r}"
    assert "Берёзовский" in addr
    assert "Александровский" in addr
    assert "3" in addr


def test_extract_address_ekb_no_zhk_still_matches() -> None:
    """EKB variant from docstring (no ЖК prefix) still matches after city-agnostic change."""
    html = "<title>Продажа квартиры — Екатеринбург, улица Горького, 36" " — id 7654321</title>"
    addr = _extract_address_from_title(html)
    assert addr is not None, "EKB no-ЖК title should still match"
    assert _RE_HAS_HOUSE_NUMBER.search(addr), f"Should contain house number: {addr!r}"
    assert "Екатеринбург" in addr
    assert "Горького" in addr
    assert "36" in addr


def test_extract_address_no_house_number_guard() -> None:
    """Street-only title (no house number): extractor may return value, but guard rejects it.

    _RE_HAS_HOUSE_NUMBER must NOT match → backfill skips the listing to avoid overwriting
    with another street-only address.
    """
    html = "<title>Продажа квартиры — Екатеринбург, улица Мира — id 999888</title>"
    addr = _extract_address_from_title(html)
    # If the regex matches at all, the extracted value must not contain a house number.
    if addr is not None:
        assert not _RE_HAS_HOUSE_NUMBER.search(
            addr
        ), f"Street-only addr must fail house-number guard: {addr!r}"


def test_extract_address_nbsp_replaced() -> None:
    """\\xa0 (non-breaking space) in title should be normalised before regex match."""
    html = (
        "<title>Продажа\xa0квартиры\xa0—\xa0Екатеринбург,\xa0улица\xa0Ленина,\xa012"
        "\xa0—\xa0id\xa0123456</title>"
    )
    addr = _extract_address_from_title(html)
    assert addr is not None, "\\xa0 should be replaced before regex"
    assert _RE_HAS_HOUSE_NUMBER.search(
        addr
    ), f"Extracted address should contain house number: {addr!r}"


def test_extract_address_no_title_returns_none() -> None:
    """HTML without a matching <title> returns None."""
    html = "<html><body>Объявление снято</body></html>"
    addr = _extract_address_from_title(html)
    assert addr is None


def test_extract_address_title_without_id_returns_none() -> None:
    """Title that matches city but has no '— id NNN' sentinel returns None."""
    html = "<title>Екатеринбург, улица Горького, 36</title>"
    addr = _extract_address_from_title(html)
    assert addr is None


# ---------------------------------------------------------------------------
# Task-level smoke tests (no real DB, no real HTTP)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_run_yandex_address_backfill_no_eligible_listings() -> None:
    """No eligible listings → run marked done with zeroed counters, no HTTP calls."""
    from app.tasks.yandex_address_backfill import run_yandex_address_backfill

    db = MagicMock()

    empty_result = YandexAddressBackfillResult(
        checked=0, saved=0, skipped=0, errors=0, duration_sec=0.01
    )

    with (
        patch(
            "app.tasks.yandex_address_backfill.backfill_yandex_addresses",
            new_callable=AsyncMock,
            return_value=empty_result,
        ) as mock_backfill,
        patch("app.tasks.yandex_address_backfill.runs_mod") as mock_runs,
    ):
        result = await run_yandex_address_backfill(db, run_id=42, params={})

    mock_backfill.assert_awaited_once()
    mock_runs.update_heartbeat.assert_called_once_with(
        db, 42, {"checked": 0, "saved": 0, "skipped": 0, "errors": 0}
    )
    mock_runs.mark_done.assert_called_once()
    mock_runs.mark_failed.assert_not_called()

    assert result.checked == 0
    assert result.saved == 0


@pytest.mark.asyncio
async def test_run_yandex_address_backfill_saves_enriched_count() -> None:
    """5 checked, 3 saved → result reflects correct counters."""
    from app.tasks.yandex_address_backfill import run_yandex_address_backfill

    db = MagicMock()

    backfill_result = YandexAddressBackfillResult(
        checked=5, saved=3, skipped=1, errors=1, duration_sec=15.0
    )

    with (
        patch(
            "app.tasks.yandex_address_backfill.backfill_yandex_addresses",
            new_callable=AsyncMock,
            return_value=backfill_result,
        ),
        patch("app.tasks.yandex_address_backfill.runs_mod") as mock_runs,
    ):
        result = await run_yandex_address_backfill(
            db, run_id=99, params={"batch_size": 100, "request_delay_sec": 2.0}
        )

    mock_runs.mark_done.assert_called_once()
    done_counters = mock_runs.mark_done.call_args[0][2]  # positional: (db, run_id, counters)
    assert done_counters["checked"] == 5
    assert done_counters["saved"] == 3
    assert done_counters["errors"] == 1

    assert result.saved == 3
    assert result.errors == 1


@pytest.mark.asyncio
async def test_run_yandex_address_backfill_marks_failed_on_exception() -> None:
    """backfill_yandex_addresses raises → mark_failed called, exception re-raised."""
    from app.tasks.yandex_address_backfill import run_yandex_address_backfill

    db = MagicMock()

    with (
        patch(
            "app.tasks.yandex_address_backfill.backfill_yandex_addresses",
            new_callable=AsyncMock,
            side_effect=RuntimeError("network timeout"),
        ),
        patch("app.tasks.yandex_address_backfill.runs_mod") as mock_runs,
    ):
        with pytest.raises(RuntimeError, match="network timeout"):
            await run_yandex_address_backfill(db, run_id=7, params={})

    mock_runs.mark_failed.assert_called_once()
    mock_runs.mark_done.assert_not_called()
