"""Unit tests for rosreestr_poll service (#888).

Coverage:
  (a) _next_quarter: boundary cases (Q4 → Q1 of next year, mid-year).
  (b) latest_loaded_quarter: derives correct (year, quarter) from a known deal_date.
  (c) poll_rosreestr_new_quarter available=True path (mocked httpx + fake DB).
  (d) poll_rosreestr_new_quarter available=False path.
  (e) Network error → available=False (never raises into caller).
  (f) No DB data → fallback baseline 2025Q4, checks 2026Q1.
"""

from __future__ import annotations

import os
import sys
from datetime import date
from unittest.mock import AsyncMock, MagicMock, patch

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.rosreestr_poll import (  # noqa: E402
    _next_quarter,
    _period_start_to_quarter,
    check_new_quarter_available,
    latest_loaded_quarter,
    poll_rosreestr_new_quarter,
    rosreestr_dataset_url,
)

# ---------------------------------------------------------------------------
# (a) _next_quarter
# ---------------------------------------------------------------------------


def test_next_quarter_mid_year() -> None:
    assert _next_quarter(2026, 1) == (2026, 2)
    assert _next_quarter(2026, 2) == (2026, 3)
    assert _next_quarter(2026, 3) == (2026, 4)


def test_next_quarter_year_rollover() -> None:
    assert _next_quarter(2026, 4) == (2027, 1)
    assert _next_quarter(2024, 4) == (2025, 1)


def test_period_start_to_quarter_from_date() -> None:
    assert _period_start_to_quarter(date(2026, 1, 1)) == (2026, 1)
    assert _period_start_to_quarter(date(2026, 4, 1)) == (2026, 2)
    assert _period_start_to_quarter(date(2026, 7, 15)) == (2026, 3)
    assert _period_start_to_quarter(date(2026, 10, 31)) == (2026, 4)
    assert _period_start_to_quarter(date(2025, 12, 31)) == (2025, 4)


def test_period_start_to_quarter_from_string() -> None:
    assert _period_start_to_quarter("2026-01-15") == (2026, 1)
    assert _period_start_to_quarter("2026-07-01") == (2026, 3)


def test_rosreestr_dataset_url_contains_year_quarter() -> None:
    url = rosreestr_dataset_url(2026, 2)
    assert "2026" in url
    assert "_2" in url or "q_2" in url  # quarter embedded


# ---------------------------------------------------------------------------
# Fake DB helpers
# ---------------------------------------------------------------------------


class _FakeRow:
    """Mimics a DB row with __getitem__."""

    def __init__(self, val: object) -> None:
        self._val = val

    def __getitem__(self, idx: int) -> object:
        return self._val


class _FakeDB:
    """Minimal fake Session returning a configurable deal_date via execute().fetchone()."""

    def __init__(self, max_deal_date: date | None) -> None:
        self._row = _FakeRow(max_deal_date) if max_deal_date is not None else None

    def execute(self, stmt: object, params: object = None) -> MagicMock:
        mock = MagicMock()
        mock.fetchone.return_value = self._row
        return mock

    def commit(self) -> None:
        pass


# ---------------------------------------------------------------------------
# (b) latest_loaded_quarter
# ---------------------------------------------------------------------------


def test_latest_loaded_quarter_returns_correct_year_quarter() -> None:
    """Q1 2026 deal_date → (2026, 1)."""
    db = _FakeDB(date(2026, 1, 31))
    result = latest_loaded_quarter(db)  # type: ignore[arg-type]
    assert result == (2026, 1)


def test_latest_loaded_quarter_q4() -> None:
    """Dec deal_date → Q4."""
    db = _FakeDB(date(2025, 12, 15))
    result = latest_loaded_quarter(db)  # type: ignore[arg-type]
    assert result == (2025, 4)


def test_latest_loaded_quarter_returns_none_when_empty() -> None:
    """No rows → None."""
    db = _FakeDB(None)
    result = latest_loaded_quarter(db)  # type: ignore[arg-type]
    assert result is None


# ---------------------------------------------------------------------------
# (c) poll_rosreestr_new_quarter — available=True
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_poll_available_true_returns_correct_next_quarter() -> None:
    """If latest loaded is Q1 2026 and head returns 200, returns available=True for Q2 2026."""
    db = _FakeDB(date(2026, 3, 31))  # latest = Q1 2026

    mock_response = MagicMock()
    mock_response.status_code = 200

    mock_client = AsyncMock()
    mock_client.head.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch("app.services.rosreestr_poll.httpx.AsyncClient", return_value=mock_client):
        result = await poll_rosreestr_new_quarter(db)  # type: ignore[arg-type]

    assert result["available"] is True
    assert result["year"] == 2026
    assert result["quarter"] == 2
    assert result["latest_loaded_year"] == 2026
    assert result["latest_loaded_quarter"] == 1


# ---------------------------------------------------------------------------
# (d) poll_rosreestr_new_quarter — available=False (404)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_poll_available_false_on_404() -> None:
    """404 from server → available=False."""
    db = _FakeDB(date(2026, 3, 15))  # Q1 2026

    mock_response = MagicMock()
    mock_response.status_code = 404

    mock_client = AsyncMock()
    mock_client.head.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch("app.services.rosreestr_poll.httpx.AsyncClient", return_value=mock_client):
        result = await poll_rosreestr_new_quarter(db)  # type: ignore[arg-type]

    assert result["available"] is False
    assert result["year"] == 2026
    assert result["quarter"] == 2


# ---------------------------------------------------------------------------
# (e) Network error → available=False (never raises into caller)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_poll_network_error_returns_false_no_raise() -> None:
    """Network error → available=False, no exception propagated."""
    import httpx

    db = _FakeDB(date(2026, 1, 20))  # Q1 2026

    mock_client = AsyncMock()
    mock_client.head.side_effect = httpx.ConnectError("simulated network error")
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch("app.services.rosreestr_poll.httpx.AsyncClient", return_value=mock_client):
        # Must not raise
        result = await poll_rosreestr_new_quarter(db)  # type: ignore[arg-type]

    assert result["available"] is False


@pytest.mark.asyncio
async def test_check_new_quarter_available_timeout_returns_false() -> None:
    """Timeout → returns False, no exception."""
    import httpx

    mock_client = AsyncMock()
    mock_client.head.side_effect = httpx.TimeoutException("timeout")

    result = await check_new_quarter_available(mock_client, 2026, 2)  # type: ignore[arg-type]
    assert result is False


# ---------------------------------------------------------------------------
# (f) No DB data → fallback baseline checks 2026Q1
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_poll_fallback_when_no_db_data() -> None:
    """No rosreestr deals in DB → fallback 2025Q4 baseline, checks 2026Q1."""
    db = _FakeDB(None)

    mock_response = MagicMock()
    mock_response.status_code = 404  # not yet available

    mock_client = AsyncMock()
    mock_client.head.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch("app.services.rosreestr_poll.httpx.AsyncClient", return_value=mock_client):
        result = await poll_rosreestr_new_quarter(db)  # type: ignore[arg-type]

    # Fallback: 2025Q4 → check 2026Q1
    assert result["year"] == 2026
    assert result["quarter"] == 1
    # latest_loaded_year/quarter should be None (no DB data)
    assert result["latest_loaded_year"] is None
    assert result["latest_loaded_quarter"] is None
