"""Regression test: listings block of cian_history_backfill must rollback on
save_detail_enrichment failure, same as the houses block already does.

Bug (audit finding #1, fix/tradein-audit-backfill-quota): save_detail_enrichment()
runs several unprotected db.execute() calls and only commits at the very end. If
any of them fails (e.g. a malformed `change_time` from Cian hits
`CAST(:ct AS timestamptz)` with a DataError), the session is left in a failed
in-transaction state (PendingRollbackError). Without `db.rollback()` in the except
branch, EVERY subsequent listing in the batch (up to 49 more) raises the same
PendingRollbackError — one real failure looks like N independent failures in the
logs, and the batch effectively stops processing after the first bad row.

This test proves: (a) db.rollback() is called after a save failure, (b) the batch
continues past the failing row and still succeeds on the next one — the exact
"cascading failure" scenario this fix prevents.
"""

from __future__ import annotations

import os
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")

from app.tasks import cian_history_backfill


class _FakeBrowserFetcher:
    def __init__(self, **kwargs: Any) -> None:
        pass

    async def __aenter__(self) -> _FakeBrowserFetcher:
        return self

    async def __aexit__(self, *_: object) -> None:
        return None


def _enrichment() -> SimpleNamespace:
    return SimpleNamespace(price_changes=[])


async def test_listings_save_failure_rolls_back_session() -> None:
    """save_detail_enrichment raising -> db.rollback() called (session un-poisoned)."""
    db = MagicMock()
    db.execute.return_value.mappings.return_value.all.return_value = [
        {"id": 1, "source_url": "https://cian.ru/1"},
    ]

    with (
        patch.object(cian_history_backfill, "BrowserFetcher", _FakeBrowserFetcher),
        patch.object(cian_history_backfill, "fetch_detail", AsyncMock(return_value=_enrichment())),
        patch.object(
            cian_history_backfill,
            "save_detail_enrichment",
            side_effect=Exception("DataError: invalid timestamptz"),
        ),
        patch("asyncio.sleep", new_callable=AsyncMock),
    ):
        result = await cian_history_backfill.backfill_cian_history(
            db, do_listings=True, do_houses=False, do_valuations=False
        )

    db.rollback.assert_called_once()
    assert result.listings_failed_save == 1
    assert result.listings_succeeded == 0


async def test_listings_batch_continues_after_one_bad_row() -> None:
    """The exact cascading-failure repro: row 1 fails save, row 2 must still
    succeed — proves rollback actually un-poisons the session for later rows,
    not just that rollback was called."""
    db = MagicMock()
    db.execute.return_value.mappings.return_value.all.return_value = [
        {"id": 1, "source_url": "https://cian.ru/1"},
        {"id": 2, "source_url": "https://cian.ru/2"},
    ]

    save_mock = MagicMock(side_effect=[Exception("DataError: invalid timestamptz"), None])

    with (
        patch.object(cian_history_backfill, "BrowserFetcher", _FakeBrowserFetcher),
        patch.object(cian_history_backfill, "fetch_detail", AsyncMock(return_value=_enrichment())),
        patch.object(cian_history_backfill, "save_detail_enrichment", save_mock),
        patch("asyncio.sleep", new_callable=AsyncMock),
    ):
        result = await cian_history_backfill.backfill_cian_history(
            db, do_listings=True, do_houses=False, do_valuations=False
        )

    assert save_mock.call_count == 2
    db.rollback.assert_called_once()
    assert result.listings_failed_save == 1
    assert result.listings_succeeded == 1
    assert result.listings_processed == 2


async def test_listings_rollback_failure_itself_does_not_crash_the_batch() -> None:
    """If db.rollback() ALSO raises (e.g. connection already dropped), the batch
    logs a warning and keeps going — mirrors the houses block's same guard."""
    db = MagicMock()
    db.rollback.side_effect = Exception("connection already closed")
    db.execute.return_value.mappings.return_value.all.return_value = [
        {"id": 1, "source_url": "https://cian.ru/1"},
    ]

    with (
        patch.object(cian_history_backfill, "BrowserFetcher", _FakeBrowserFetcher),
        patch.object(cian_history_backfill, "fetch_detail", AsyncMock(return_value=_enrichment())),
        patch.object(
            cian_history_backfill,
            "save_detail_enrichment",
            side_effect=Exception("DataError: invalid timestamptz"),
        ),
        patch("asyncio.sleep", new_callable=AsyncMock),
    ):
        result = await cian_history_backfill.backfill_cian_history(
            db, do_listings=True, do_houses=False, do_valuations=False
        )

    assert result.listings_failed_save == 1


async def test_listings_no_rollback_on_success() -> None:
    """Successful save must NOT trigger a rollback (would discard the commit
    that save_detail_enrichment already made)."""
    db = MagicMock()
    db.execute.return_value.mappings.return_value.all.return_value = [
        {"id": 1, "source_url": "https://cian.ru/1"},
    ]

    with (
        patch.object(cian_history_backfill, "BrowserFetcher", _FakeBrowserFetcher),
        patch.object(cian_history_backfill, "fetch_detail", AsyncMock(return_value=_enrichment())),
        patch.object(cian_history_backfill, "save_detail_enrichment", MagicMock()),
        patch("asyncio.sleep", new_callable=AsyncMock),
    ):
        result = await cian_history_backfill.backfill_cian_history(
            db, do_listings=True, do_houses=False, do_valuations=False
        )

    db.rollback.assert_not_called()
    assert result.listings_succeeded == 1
