"""Tests for Bittensor authentication utilities."""

import re
import time
from unittest.mock import MagicMock

import httpx
import pytest

from oro_sdk.bittensor_auth import (
    AsyncSigningTransport,
    BittensorAuthClient,
    SigningTransport,
    generate_auth_headers,
    is_public_endpoint,
)
from oro_sdk.retry import AsyncRetryTransport, RetryTransport


def create_mock_wallet(hotkey: str = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") -> MagicMock:
    """Create a mock Bittensor wallet for testing."""
    wallet = MagicMock()
    wallet.hotkey.ss58_address = hotkey
    wallet.hotkey.sign.return_value = bytes.fromhex("ab" * 64)  # 64-byte signature
    return wallet


class TestGenerateAuthHeaders:
    """Tests for generate_auth_headers function."""

    def test_returns_all_required_headers(self):
        """Should return X-Hotkey, X-Signature, X-Nonce, X-Timestamp."""
        wallet = create_mock_wallet()

        headers = generate_auth_headers(wallet)

        assert "X-Hotkey" in headers
        assert "X-Signature" in headers
        assert "X-Nonce" in headers
        assert "X-Timestamp" in headers

    def test_hotkey_matches_wallet(self):
        """X-Hotkey should match wallet's SS58 address."""
        hotkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
        wallet = create_mock_wallet(hotkey)

        headers = generate_auth_headers(wallet)

        assert headers["X-Hotkey"] == hotkey

    def test_signature_is_hex_with_0x_prefix(self):
        """X-Signature should be hex-encoded with 0x prefix."""
        wallet = create_mock_wallet()

        headers = generate_auth_headers(wallet)

        assert headers["X-Signature"].startswith("0x")
        # Remove 0x and verify it's valid hex
        hex_part = headers["X-Signature"][2:]
        assert re.match(r"^[0-9a-fA-F]+$", hex_part)

    def test_nonce_is_uuid_format(self):
        """X-Nonce should be a valid UUID."""
        wallet = create_mock_wallet()

        headers = generate_auth_headers(wallet)

        uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
        assert re.match(uuid_pattern, headers["X-Nonce"])

    def test_timestamp_is_recent_unix_epoch(self):
        """X-Timestamp should be a recent Unix timestamp."""
        wallet = create_mock_wallet()
        before = int(time.time())

        headers = generate_auth_headers(wallet)

        after = int(time.time())
        timestamp = int(headers["X-Timestamp"])
        assert before <= timestamp <= after

    def test_custom_nonce_is_used(self):
        """Should use provided nonce instead of generating one."""
        wallet = create_mock_wallet()
        custom_nonce = "my-custom-nonce-123"

        headers = generate_auth_headers(wallet, nonce=custom_nonce)

        assert headers["X-Nonce"] == custom_nonce

    def test_signs_correct_message_format(self):
        """Should sign message in format 'hotkey:timestamp:nonce'."""
        wallet = create_mock_wallet()
        custom_nonce = "test-nonce"

        generate_auth_headers(wallet, nonce=custom_nonce)

        # Verify sign was called
        wallet.hotkey.sign.assert_called_once()
        signed_bytes = wallet.hotkey.sign.call_args[0][0]
        signed_message = signed_bytes.decode()

        # Message format: hotkey:timestamp:nonce
        parts = signed_message.split(":")
        assert len(parts) == 3
        assert parts[0] == wallet.hotkey.ss58_address
        assert parts[1].isdigit()  # timestamp
        assert parts[2] == custom_nonce


class TestIsPublicEndpoint:
    """Tests for is_public_endpoint function."""

    def test_returns_true_for_public_paths(self):
        """Should return True for /v1/public/* paths."""
        assert is_public_endpoint("/v1/public/leaderboard") is True
        assert is_public_endpoint("/v1/public/suites/current") is True
        assert is_public_endpoint("/v1/public/agents/123") is True

    def test_returns_true_for_health_path(self):
        """Should return True for /health path."""
        assert is_public_endpoint("/health") is True

    def test_returns_true_for_auth_paths(self):
        """Should return True for /v1/auth/* paths."""
        assert is_public_endpoint("/v1/auth/challenge") is True
        assert is_public_endpoint("/v1/auth/session") is True
        assert is_public_endpoint("/v1/auth/logout") is True

    def test_returns_false_for_miner_paths(self):
        """Should return False for /v1/miner/* paths."""
        assert is_public_endpoint("/v1/miner/agents") is False
        assert is_public_endpoint("/v1/miner/submit") is False

    def test_returns_false_for_validator_paths(self):
        """Should return False for /v1/validator/* paths."""
        assert is_public_endpoint("/v1/validator/claim") is False
        assert is_public_endpoint("/v1/validator/results") is False

    def test_returns_false_for_admin_paths(self):
        """Should return False for /v1/admin/* paths."""
        assert is_public_endpoint("/v1/admin/ban") is False

    def test_handles_full_urls(self):
        """Should handle full URLs correctly."""
        assert is_public_endpoint("https://api.oro.ai/v1/public/leaderboard") is True
        assert is_public_endpoint("https://api.oro.ai/v1/miner/agents") is False
        assert is_public_endpoint("https://api.oro.ai/health") is True

    def test_handles_query_parameters(self):
        """Should handle URLs with query parameters."""
        assert is_public_endpoint("/v1/public/leaderboard?limit=10") is True
        assert is_public_endpoint("/v1/miner/agents?page=1") is False

    def test_returns_false_for_none_or_empty(self):
        """Should return False for None or empty paths."""
        assert is_public_endpoint(None) is False
        assert is_public_endpoint("") is False


class TestSigningTransport:
    """Tests for SigningTransport."""

    def test_adds_auth_headers_to_authenticated_endpoints(self):
        """Should add auth headers to authenticated endpoint requests."""
        wallet = create_mock_wallet()
        mock_wrapped = MagicMock()
        mock_wrapped.handle_request.return_value = httpx.Response(200)

        transport = SigningTransport(wallet, wrapped=mock_wrapped)
        request = httpx.Request("GET", "https://api.example.com/v1/miner/agents")

        transport.handle_request(request)

        # Verify auth headers were added
        assert "X-Hotkey" in request.headers
        assert "X-Signature" in request.headers
        assert "X-Nonce" in request.headers
        assert "X-Timestamp" in request.headers

    def test_skips_auth_headers_for_public_endpoints(self):
        """Should NOT add auth headers to public endpoint requests."""
        wallet = create_mock_wallet()
        mock_wrapped = MagicMock()
        mock_wrapped.handle_request.return_value = httpx.Response(200)

        transport = SigningTransport(wallet, wrapped=mock_wrapped)
        request = httpx.Request("GET", "https://api.example.com/v1/public/leaderboard")

        transport.handle_request(request)

        # Verify auth headers were NOT added
        assert "X-Hotkey" not in request.headers
        assert "X-Signature" not in request.headers
        assert "X-Nonce" not in request.headers
        assert "X-Timestamp" not in request.headers

    def test_skips_auth_headers_for_health_endpoint(self):
        """Should NOT add auth headers to health endpoint requests."""
        wallet = create_mock_wallet()
        mock_wrapped = MagicMock()
        mock_wrapped.handle_request.return_value = httpx.Response(200)

        transport = SigningTransport(wallet, wrapped=mock_wrapped)
        request = httpx.Request("GET", "https://api.example.com/health")

        transport.handle_request(request)

        # Verify auth headers were NOT added
        assert "X-Hotkey" not in request.headers

    def test_passes_request_to_wrapped_transport(self):
        """Should call the wrapped transport's handle_request."""
        wallet = create_mock_wallet()
        mock_wrapped = MagicMock()
        expected_response = httpx.Response(200, json={"status": "ok"})
        mock_wrapped.handle_request.return_value = expected_response

        transport = SigningTransport(wallet, wrapped=mock_wrapped)
        request = httpx.Request("GET", "https://api.example.com/test")

        response = transport.handle_request(request)

        mock_wrapped.handle_request.assert_called_once_with(request)
        assert response == expected_response


class TestAsyncSigningTransport:
    """Tests for AsyncSigningTransport."""

    @pytest.mark.asyncio
    async def test_adds_auth_headers_to_authenticated_endpoints(self):
        """Should add auth headers to authenticated async requests."""
        from unittest.mock import AsyncMock

        wallet = create_mock_wallet()
        mock_wrapped = MagicMock()
        mock_wrapped.handle_async_request = AsyncMock(return_value=httpx.Response(200))

        transport = AsyncSigningTransport(wallet, wrapped=mock_wrapped)
        request = httpx.Request("GET", "https://api.example.com/v1/miner/agents")

        await transport.handle_async_request(request)

        # Verify auth headers were added
        assert "X-Hotkey" in request.headers
        assert "X-Signature" in request.headers
        assert "X-Nonce" in request.headers
        assert "X-Timestamp" in request.headers

    @pytest.mark.asyncio
    async def test_skips_auth_headers_for_public_endpoints(self):
        """Should NOT add auth headers to public async requests."""
        from unittest.mock import AsyncMock

        wallet = create_mock_wallet()
        mock_wrapped = MagicMock()
        mock_wrapped.handle_async_request = AsyncMock(return_value=httpx.Response(200))

        transport = AsyncSigningTransport(wallet, wrapped=mock_wrapped)
        request = httpx.Request("GET", "https://api.example.com/v1/public/leaderboard")

        await transport.handle_async_request(request)

        # Verify auth headers were NOT added
        assert "X-Hotkey" not in request.headers
        assert "X-Signature" not in request.headers


class TestBittensorAuthClient:
    """Tests for BittensorAuthClient."""

    def test_creates_client_with_signing_transport(self):
        """Should create httpx client with RetryTransport wrapping SigningTransport."""
        wallet = create_mock_wallet()

        client = BittensorAuthClient(base_url="https://api.example.com", wallet=wallet)
        httpx_client = client.get_httpx_client()

        assert isinstance(httpx_client, httpx.Client)
        assert str(httpx_client.base_url) == "https://api.example.com"
        assert isinstance(httpx_client._transport, RetryTransport)
        assert isinstance(httpx_client._transport._wrapped, SigningTransport)

    def test_creates_async_client_with_async_signing_transport(self):
        """Should create async httpx client with AsyncRetryTransport wrapping AsyncSigningTransport."""
        wallet = create_mock_wallet()

        client = BittensorAuthClient(base_url="https://api.example.com", wallet=wallet)
        httpx_client = client.get_async_httpx_client()

        assert isinstance(httpx_client, httpx.AsyncClient)
        assert str(httpx_client.base_url) == "https://api.example.com"
        assert isinstance(httpx_client._transport, AsyncRetryTransport)
        assert isinstance(httpx_client._transport._wrapped, AsyncSigningTransport)

    def test_context_manager_sync(self):
        """Should work as sync context manager."""
        wallet = create_mock_wallet()
        client = BittensorAuthClient(base_url="https://api.example.com", wallet=wallet)

        with client as ctx:
            assert ctx is client

    @pytest.mark.asyncio
    async def test_context_manager_async(self):
        """Should work as async context manager."""
        wallet = create_mock_wallet()
        client = BittensorAuthClient(base_url="https://api.example.com", wallet=wallet)

        async with client as ctx:
            assert ctx is client

    def test_reuses_client_instance(self):
        """Should return same client on multiple get_httpx_client calls."""
        wallet = create_mock_wallet()
        client = BittensorAuthClient(base_url="https://api.example.com", wallet=wallet)

        client1 = client.get_httpx_client()
        client2 = client.get_httpx_client()

        assert client1 is client2
