"""Tests for utility modules: retry, rate_limit, signing."""

from __future__ import annotations

import asyncio
from unittest.mock import AsyncMock, patch

import pytest

from openhedge.exceptions import NetworkError, RateLimitError, ServerError
from openhedge.utils.rate_limit import check_rate_limit, extract_retry_after
from openhedge.utils.retry import with_retry
from openhedge.utils.signing import build_transaction, get_account, sign_transaction


# ---------------------------------------------------------------------------
# Rate limit
# ---------------------------------------------------------------------------

class TestRateLimit:
    def test_extract_retry_after_present(self):
        assert extract_retry_after({"Retry-After": "5"}) == 5.0

    def test_extract_retry_after_missing(self):
        assert extract_retry_after({}) == 1.0

    def test_extract_retry_after_invalid(self):
        assert extract_retry_after({"Retry-After": "abc"}) == 1.0

    def test_check_rate_limit_429(self):
        with pytest.raises(RateLimitError) as exc_info:
            check_rate_limit(429, {"Retry-After": "3"})
        assert exc_info.value.retry_after == 3.0

    def test_check_rate_limit_200(self):
        check_rate_limit(200, {})  # should not raise


# ---------------------------------------------------------------------------
# Retry
# ---------------------------------------------------------------------------

class TestRetry:
    async def test_succeeds_first_try(self):
        fn = AsyncMock(return_value="ok")
        result = await with_retry(fn, max_retries=3, base_delay=0.01)
        assert result == "ok"
        assert fn.call_count == 1

    async def test_retries_on_network_error(self):
        fn = AsyncMock(side_effect=[NetworkError("fail"), "ok"])
        result = await with_retry(fn, max_retries=3, base_delay=0.01)
        assert result == "ok"
        assert fn.call_count == 2

    async def test_exhausts_retries(self):
        fn = AsyncMock(side_effect=NetworkError("persistent"))
        with pytest.raises(NetworkError):
            await with_retry(fn, max_retries=2, base_delay=0.01)
        assert fn.call_count == 3  # initial + 2 retries

    async def test_non_retryable_raises_immediately(self):
        fn = AsyncMock(side_effect=ValueError("bad"))
        with pytest.raises(ValueError):
            await with_retry(fn, max_retries=3, base_delay=0.01)
        assert fn.call_count == 1

    async def test_rate_limit_uses_retry_after(self):
        fn = AsyncMock(
            side_effect=[RateLimitError(retry_after=0.01), "ok"]
        )
        result = await with_retry(fn, max_retries=3, base_delay=0.01)
        assert result == "ok"


# ---------------------------------------------------------------------------
# Signing
# ---------------------------------------------------------------------------

FAKE_KEY = "0x" + "ab" * 32


class TestSigning:
    def test_get_account(self):
        account = get_account(FAKE_KEY)
        assert account.address.startswith("0x")
        assert len(account.address) == 42

    def test_build_transaction_defaults(self):
        tx = build_transaction(
            to="0x" + "11" * 20,
            data="0x1234",
            chain_id=84532,
            nonce=5,
        )
        assert tx["chainId"] == 84532
        assert tx["nonce"] == 5
        assert tx["type"] == 2
        assert "maxFeePerGas" in tx
        assert "maxPriorityFeePerGas" in tx

    def test_build_transaction_custom_gas(self):
        tx = build_transaction(
            to="0x" + "11" * 20,
            data="0x",
            chain_id=84532,
            nonce=0,
            gas=100_000,
            max_fee_per_gas=2_000_000_000,
            max_priority_fee_per_gas=1_000_000_000,
        )
        assert tx["gas"] == 100_000
        assert tx["maxFeePerGas"] == 2_000_000_000

    def test_sign_transaction(self):
        tx = build_transaction(
            to="0x" + "11" * 20,
            data="0x",
            chain_id=84532,
            nonce=0,
        )
        raw = sign_transaction(tx, FAKE_KEY)
        assert isinstance(raw, str)
        assert len(raw) > 0
