"""Unit tests for the report data collector.

Tests the CollectedData dataclass defaults, _row_dict UUID conversion,
and collect_report_data with mocked asyncpg pool.

Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
"""
from __future__ import annotations

import uuid
from datetime import date
from unittest.mock import AsyncMock, MagicMock

import pytest

from services.reporting.collector import CollectedData, _row_dict, collect_report_data

# ===================================================================
# _row_dict tests
# ===================================================================


class TestRowDict:
    """Tests for _row_dict UUID→str conversion."""

    def test_uuid_fields_converted_to_str(self):
        """UUID values in the record are converted to strings."""
        test_uuid = uuid.uuid4()
        row = MagicMock()
        row.__iter__ = MagicMock(return_value=iter([("id", test_uuid), ("name", "test")]))
        row.keys = MagicMock(return_value=["id", "name"])
        row.values = MagicMock(return_value=[test_uuid, "test"])
        row.items = MagicMock(return_value=[("id", test_uuid), ("name", "test")])
        # dict(row) needs to work — use a real dict-like mock
        mock_dict = {"id": test_uuid, "name": "test"}
        row.__iter__ = MagicMock(return_value=iter(mock_dict))
        row.__getitem__ = lambda self, key: mock_dict[key]

        # Simpler approach: just pass a dict-like object
        class FakeRecord(dict):
            pass

        record = FakeRecord(id=test_uuid, name="test", count=42)
        result = _row_dict(record)

        assert result["id"] == str(test_uuid)
        assert result["name"] == "test"
        assert result["count"] == 42

    def test_no_uuid_fields_unchanged(self):
        """Non-UUID values pass through unchanged."""

        class FakeRecord(dict):
            pass

        record = FakeRecord(ticker="AAPL", price=185.50, active=True)
        result = _row_dict(record)

        assert result["ticker"] == "AAPL"
        assert result["price"] == 185.50
        assert result["active"] is True

    def test_multiple_uuid_fields(self):
        """Multiple UUID fields are all converted."""

        class FakeRecord(dict):
            pass

        id1 = uuid.uuid4()
        id2 = uuid.uuid4()
        record = FakeRecord(id=id1, recommendation_id=id2, ticker="MSFT")
        result = _row_dict(record)

        assert result["id"] == str(id1)
        assert result["recommendation_id"] == str(id2)
        assert result["ticker"] == "MSFT"

    def test_empty_record(self):
        """Empty record returns empty dict."""

        class FakeRecord(dict):
            pass

        record = FakeRecord()
        result = _row_dict(record)
        assert result == {}


# ===================================================================
# CollectedData defaults
# ===================================================================


class TestCollectedDataDefaults:
    """Tests for CollectedData dataclass default values."""

    def test_default_empty_lists(self):
        """All list fields default to empty lists."""
        data = CollectedData()
        assert data.trading_decisions == []
        assert data.orders == []
        assert data.open_positions == []
        assert data.closed_positions == []
        assert data.recommendations == []
        assert data.prediction_outcomes == []
        assert data.model_metric_snapshots == []
        assert data.circuit_breaker_events == []

    def test_default_none_snapshots(self):
        """Snapshot fields default to None."""
        data = CollectedData()
        assert data.portfolio_snapshot is None
        assert data.previous_portfolio_snapshot is None

    def test_default_zero_balance(self):
        """Reserve pool balance defaults to 0.0."""
        data = CollectedData()
        assert data.reserve_pool_balance == 0.0

    def test_independent_list_instances(self):
        """Each CollectedData instance has independent list instances."""
        data1 = CollectedData()
        data2 = CollectedData()
        data1.trading_decisions.append({"id": "test"})
        assert data2.trading_decisions == []


# ===================================================================
# collect_report_data with mocked pool
# ===================================================================


def _make_mock_pool():
    """Create a mock asyncpg pool with async context manager support."""
    pool = MagicMock()
    conn = AsyncMock()

    # pool.acquire() returns a sync object that supports async context manager
    ctx = MagicMock()
    ctx.__aenter__ = AsyncMock(return_value=conn)
    ctx.__aexit__ = AsyncMock(return_value=False)
    pool.acquire.return_value = ctx

    return pool, conn


class TestCollectReportData:
    """Tests for collect_report_data with mocked asyncpg."""

    @pytest.mark.asyncio
    async def test_zero_activity_returns_empty_lists(self):
        """When no data exists, all lists are empty and snapshots are None."""
        pool, conn = _make_mock_pool()

        # All queries return empty results
        conn.fetch.return_value = []
        conn.fetchrow.return_value = None

        result = await collect_report_data(
            pool, date(2025, 1, 15), date(2025, 1, 15)
        )

        assert isinstance(result, CollectedData)
        assert result.trading_decisions == []
        assert result.orders == []
        assert result.open_positions == []
        assert result.closed_positions == []
        assert result.portfolio_snapshot is None
        assert result.previous_portfolio_snapshot is None
        assert result.recommendations == []
        assert result.prediction_outcomes == []
        assert result.model_metric_snapshots == []
        assert result.circuit_breaker_events == []
        assert result.reserve_pool_balance == 0.0

    @pytest.mark.asyncio
    async def test_queries_use_correct_date_range(self):
        """Verify that queries are called with the correct period dates."""
        pool, conn = _make_mock_pool()
        conn.fetch.return_value = []
        conn.fetchrow.return_value = None

        start = date(2025, 1, 13)
        end = date(2025, 1, 17)

        await collect_report_data(pool, start, end)

        # Verify fetch was called (trading_decisions, orders, open_positions,
        # closed_positions, recommendations, prediction_outcomes,
        # model_metric_snapshots, circuit_breaker_events)
        assert conn.fetch.call_count == 8

        # Verify fetchrow was called (portfolio_snapshot, previous_snapshot,
        # reserve_pool_balance)
        assert conn.fetchrow.call_count == 3

    @pytest.mark.asyncio
    async def test_reserve_pool_balance_from_ledger(self):
        """Reserve pool balance is read from the latest ledger entry."""
        pool, conn = _make_mock_pool()
        conn.fetch.return_value = []

        # Mock fetchrow to return different values for different queries
        balance_row = {"balance_after": 450.75}

        call_count = 0

        async def mock_fetchrow(query, *args):
            nonlocal call_count
            call_count += 1
            if "reserve_pool_ledger" in query:
                return balance_row
            return None

        conn.fetchrow.side_effect = mock_fetchrow

        result = await collect_report_data(
            pool, date(2025, 1, 15), date(2025, 1, 15)
        )

        assert result.reserve_pool_balance == 450.75

    @pytest.mark.asyncio
    async def test_portfolio_snapshots_populated(self):
        """Portfolio snapshot and previous snapshot are populated when data exists."""
        pool, conn = _make_mock_pool()
        conn.fetch.return_value = []

        current_snapshot = {
            "id": uuid.uuid4(),
            "snapshot_date": date(2025, 1, 15),
            "portfolio_value": 10500.0,
            "active_pool": 8000.0,
            "reserve_pool": 2500.0,
            "cumulative_return": 0.05,
        }
        previous_snapshot = {
            "id": uuid.uuid4(),
            "snapshot_date": date(2025, 1, 14),
            "portfolio_value": 10000.0,
            "active_pool": 7500.0,
            "reserve_pool": 2500.0,
            "cumulative_return": 0.0,
        }

        call_count = 0

        async def mock_fetchrow(query, *args):
            nonlocal call_count
            call_count += 1
            if "reserve_pool_ledger" in query:
                return None
            if "snapshot_date >=" in query:
                # current snapshot query (snapshot_date >= $1 AND snapshot_date <= $2)
                return current_snapshot
            if "snapshot_date <" in query:
                # previous snapshot query (snapshot_date < $1)
                return previous_snapshot
            return None

        conn.fetchrow.side_effect = mock_fetchrow

        result = await collect_report_data(
            pool, date(2025, 1, 15), date(2025, 1, 15)
        )

        assert result.portfolio_snapshot is not None
        assert result.portfolio_snapshot["portfolio_value"] == 10500.0
        # UUID fields should be converted to str
        assert isinstance(result.portfolio_snapshot["id"], str)

        assert result.previous_portfolio_snapshot is not None
        assert result.previous_portfolio_snapshot["portfolio_value"] == 10000.0
