"""Tests for boxtime.clients.cointime_client — Cointime."""

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from boxtime.clients.cointime_client import Cointime
from boxtime.models import BlockHeader, EmissionInfo, Transaction
from boxtime.models.price.price import PriceHistory, PricePoint
from tests.conftest import BLOCK_HEADER_JSON, EMISSION_JSON, TRANSACTION_JSON


# ------------------------------------------------------------------
# Constructor & properties
# ------------------------------------------------------------------


def test_default_constructor():
    c = Cointime()
    assert c._node_urls == ["http://127.0.0.1:9053"]
    assert c._max_concurrent == 50
    assert c.price_client is None


def test_custom_constructor():
    c = Cointime(
        node_urls=["http://n1:9053", "http://n2:9053"],
        max_concurrent=100,
    )
    assert c._node_urls == ["http://n1:9053", "http://n2:9053"]
    assert c._max_concurrent == 100


# ------------------------------------------------------------------
# Round-robin
# ------------------------------------------------------------------


def test_node_url_for_single_node():
    c = Cointime(node_urls=["http://n1:9053"])
    assert c._node_url_for(0) == "http://n1:9053"
    assert c._node_url_for(99) == "http://n1:9053"


def test_node_url_for_multi_node():
    c = Cointime(node_urls=["http://n1:9053", "http://n2:9053", "http://n3:9053"])
    assert c._node_url_for(0) == "http://n1:9053"
    assert c._node_url_for(1) == "http://n2:9053"
    assert c._node_url_for(2) == "http://n3:9053"
    assert c._node_url_for(3) == "http://n1:9053"


# ------------------------------------------------------------------
# _run_concurrent
# ------------------------------------------------------------------


def test_run_concurrent():
    async def coro():
        return 42

    assert Cointime._run_concurrent(coro()) == 42


# ------------------------------------------------------------------
# _require_price_client
# ------------------------------------------------------------------


def test_require_price_client_raises():
    c = Cointime()
    with pytest.raises(ValueError, match="CoinGecko API key required"):
        c._require_price_client()


# ------------------------------------------------------------------
# coinblocks_created
# ------------------------------------------------------------------


def _mock_emission(height):
    """Return a canned EmissionInfo for any height."""
    return EmissionInfo(**EMISSION_JSON)


def test_coinblocks_created():
    c = Cointime()
    with patch.object(
        Cointime, "_run_concurrent",
        side_effect=lambda coro: asyncio.get_event_loop().run_until_complete(coro)
        if False else 50_000_000_000_000_000,
    ):
        # Simpler: directly mock the async path
        pass

    # More direct approach: mock _async_with_session
    c = Cointime()
    emission = EmissionInfo(**EMISSION_JSON)

    async def fake_session(coro_fn):
        return emission.total_coins_issued

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.coinblocks_created(1000)
    assert result == 50_000_000_000_000_000


def test_coinblocks_created_failure():
    c = Cointime()

    async def fake_session(coro_fn):
        return None

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.coinblocks_created(1000)
    assert result is None


# ------------------------------------------------------------------
# coinblocks_destroyed
# ------------------------------------------------------------------


def test_coinblocks_destroyed():
    c = Cointime()
    # height=1000, input at inclusion_height=900, value=500M
    # CBD = 500_000_000 * (1000 - 900) = 50_000_000_000
    expected_cbd = 500_000_000 * (1000 - 900)

    async def fake_session(coro_fn):
        return expected_cbd

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.coinblocks_destroyed(1000)
    assert result == expected_cbd


def test_coinblocks_destroyed_failure():
    c = Cointime()

    async def fake_session(coro_fn):
        return None

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.coinblocks_destroyed(1000)
    assert result is None


# ------------------------------------------------------------------
# coinblocks_stored
# ------------------------------------------------------------------


def test_coinblocks_stored():
    c = Cointime()
    cbc = 50_000_000_000_000_000
    cbd = 50_000_000_000

    async def fake_session(coro_fn):
        return cbc - cbd

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.coinblocks_stored(1000)
    assert result == cbc - cbd


def test_coinblocks_stored_failure():
    c = Cointime()

    async def fake_session(coro_fn):
        return None

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.coinblocks_stored(1000)
    assert result is None


# ------------------------------------------------------------------
# total_coinblocks_created (concurrent batch)
# ------------------------------------------------------------------


def test_total_coinblocks_created():
    c = Cointime()

    async def fake_fetch(start, end, fn):
        return [100, 200, 300]

    with patch.object(c, "_fetch_concurrent", side_effect=fake_fetch):
        result = c.total_coinblocks_created(0, 2)
    assert result == 600


def test_total_coinblocks_created_failure():
    c = Cointime()

    async def fake_fetch(start, end, fn):
        return None

    with patch.object(c, "_fetch_concurrent", side_effect=fake_fetch):
        result = c.total_coinblocks_created(0, 2)
    assert result is None


# ------------------------------------------------------------------
# total_coinblocks_destroyed (concurrent batch)
# ------------------------------------------------------------------


def test_total_coinblocks_destroyed():
    c = Cointime()

    async def fake_fetch(start, end, fn):
        return [10, 20, 30]

    with patch.object(c, "_fetch_concurrent", side_effect=fake_fetch):
        result = c.total_coinblocks_destroyed(0, 2)
    assert result == 60


def test_total_coinblocks_destroyed_failure():
    c = Cointime()

    async def fake_fetch(start, end, fn):
        return None

    with patch.object(c, "_fetch_concurrent", side_effect=fake_fetch):
        result = c.total_coinblocks_destroyed(0, 2)
    assert result is None


# ------------------------------------------------------------------
# total_coinblocks_stored (concurrent batch)
# ------------------------------------------------------------------


def test_total_coinblocks_stored():
    c = Cointime()

    async def fake_fetch(start, end, fn):
        return [90, 180, 270]

    with patch.object(c, "_fetch_concurrent", side_effect=fake_fetch):
        result = c.total_coinblocks_stored(0, 2)
    assert result == 540


def test_total_coinblocks_stored_failure():
    c = Cointime()

    async def fake_fetch(start, end, fn):
        return None

    with patch.object(c, "_fetch_concurrent", side_effect=fake_fetch):
        result = c.total_coinblocks_stored(0, 2)
    assert result is None


# ------------------------------------------------------------------
# Async helpers (unit tests via direct async calls)
# ------------------------------------------------------------------


async def test_async_coinblocks_created():
    c = Cointime()
    client = AsyncMock()
    client.get_emission_at.return_value = EmissionInfo(**EMISSION_JSON)
    result = await c._async_coinblocks_created(client, 1000)
    assert result == 50_000_000_000_000_000


async def test_async_coinblocks_created_failure():
    c = Cointime()
    client = AsyncMock()
    client.get_emission_at.return_value = None
    result = await c._async_coinblocks_created(client, 1000)
    assert result is None


async def test_async_coinblocks_destroyed():
    c = Cointime()
    client = AsyncMock()
    client.get_block_transaction_ids.return_value = ["tx123"]
    client.get_transaction.return_value = Transaction(**TRANSACTION_JSON)
    result = await c._async_coinblocks_destroyed(client, 1000)
    # input: value=500M, inclusion_height=900, height=1000 → 500M * 100
    assert result == 500_000_000 * 100


async def test_async_coinblocks_destroyed_no_tx_ids():
    c = Cointime()
    client = AsyncMock()
    client.get_block_transaction_ids.return_value = None
    result = await c._async_coinblocks_destroyed(client, 1000)
    assert result is None


async def test_async_coinblocks_destroyed_tx_fetch_failure():
    c = Cointime()
    client = AsyncMock()
    client.get_block_transaction_ids.return_value = ["tx123"]
    client.get_transaction.return_value = None
    result = await c._async_coinblocks_destroyed(client, 1000)
    assert result is None


async def test_async_coinblocks_stored():
    c = Cointime()
    client = AsyncMock()
    client.get_emission_at.return_value = EmissionInfo(**EMISSION_JSON)
    client.get_block_transaction_ids.return_value = ["tx123"]
    client.get_transaction.return_value = Transaction(**TRANSACTION_JSON)

    result = await c._async_coinblocks_stored(client, 1000)
    cbc = 50_000_000_000_000_000
    cbd = 500_000_000 * 100
    assert result == cbc - cbd


# ------------------------------------------------------------------
# get_price_history
# ------------------------------------------------------------------


def test_get_price_history():
    with patch("boxtime.clients.cointime_client.CoinGeckoClient"):
        c = Cointime(coingecko_api_key="fake")

    header1 = {**BLOCK_HEADER_JSON, "timestamp": 1700000000000, "height": 100}
    header2 = {**BLOCK_HEADER_JSON, "timestamp": 1700100000000, "height": 200}
    headers = [BlockHeader(**header1), BlockHeader(**header2)]

    async def fake_session(coro_fn):
        return headers

    price_history = PriceHistory(
        coin_id="ergo",
        vs_currency="usd",
        prices=[PricePoint(timestamp=1700000000000, price=1.5)],
    )
    c._price_client.get_price_history_range = MagicMock(return_value=price_history)

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.get_price_history(100, 200)
    assert len(result) == 1
    assert result[0].price == 1.5


def test_get_price_history_no_headers():
    with patch("boxtime.clients.cointime_client.CoinGeckoClient"):
        c = Cointime(coingecko_api_key="fake")

    async def fake_session(coro_fn):
        return []

    with patch.object(c, "_async_with_session", side_effect=fake_session):
        result = c.get_price_history(100, 200)
    assert result is None
