"""Tests for the wallet management module.

These tests use mocking to avoid actual Solana network calls
and keyring operations during unit testing.
"""

import pytest
from unittest.mock import patch, MagicMock, PropertyMock
import json

# Import the module under test
from botvegas_mcp.wallet import (
    CONFIG,
    WalletConfig,
    generate_keypair,
    get_address_from_keypair,
    store_keypair,
    retrieve_keypair,
    delete_keypair,
    get_wallet_balances,
    setup_wallet,
    check_wallet_balance,
    list_agent_wallets,
    WalletSetupResult,
    WalletBalanceResult,
    SOLDERS_AVAILABLE,
    KEYRING_AVAILABLE,
)


# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def mock_keypair_bytes():
    """Generate mock 64-byte keypair data."""
    # A valid keypair is 64 bytes: 32 bytes secret + 32 bytes public
    return list(range(64))


@pytest.fixture
def mock_wallet_address():
    """A mock Solana wallet address (valid base58, 44 chars)."""
    return "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"


# =============================================================================
# Test WalletConfig
# =============================================================================


class TestWalletConfig:
    """Test the WalletConfig class."""

    def test_default_config_values(self):
        """Test default configuration values."""
        assert CONFIG.BOTVEGAS_MINT is not None
        assert CONFIG.MIN_SOL_FOR_FEES > 0
        assert CONFIG.RPC_MAINNET.startswith("https://")
        assert CONFIG.RPC_DEVNET.startswith("https://")

    def test_service_name(self):
        """Test keyring service name is set."""
        assert CONFIG.SERVICE_NAME is not None
        assert len(CONFIG.SERVICE_NAME) > 0


# =============================================================================
# Test Keypair Generation (Mocked)
# =============================================================================


class TestKeypairGeneration:
    """Test keypair generation functions."""

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_generate_keypair_returns_64_bytes(self):
        """Test that generate_keypair returns 64 bytes."""
        keypair_bytes = generate_keypair()
        assert keypair_bytes is not None
        assert len(keypair_bytes) == 64
        assert all(isinstance(b, int) for b in keypair_bytes)
        assert all(0 <= b <= 255 for b in keypair_bytes)

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_generate_keypair_is_random(self):
        """Test that each keypair is unique."""
        kp1 = generate_keypair()
        kp2 = generate_keypair()
        assert kp1 != kp2

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_get_address_from_keypair(self):
        """Test address derivation from keypair."""
        # Generate a real keypair for this test
        real_keypair = generate_keypair()
        address = get_address_from_keypair(real_keypair)
        
        # Should be a base58 string of ~44 characters
        assert address is not None
        assert len(address) >= 32
        assert len(address) <= 50
        # Base58 characters only
        assert all(c in "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" for c in address)


# =============================================================================
# Test Keyring Storage (Mocked)
# =============================================================================


class TestKeyringStorage:
    """Test keyring storage functions with mocked keyring."""

    @pytest.mark.skipif(not KEYRING_AVAILABLE, reason="keyring not installed")
    def test_store_keypair(self, mock_keypair_bytes):
        """Test storing a keypair in keyring."""
        with patch('botvegas_mcp.wallet.keyring') as mock_keyring:
            result = store_keypair("test_identity", mock_keypair_bytes)
            
            assert result is True
            mock_keyring.set_password.assert_called_once()
            call_args = mock_keyring.set_password.call_args
            assert call_args[0][0] == CONFIG.SERVICE_NAME
            assert "test_identity" in call_args[0][1]

    @pytest.mark.skipif(not KEYRING_AVAILABLE, reason="keyring not installed")
    def test_retrieve_keypair(self, mock_keypair_bytes):
        """Test retrieving a keypair from keyring."""
        stored_json = json.dumps(mock_keypair_bytes)
        
        with patch('botvegas_mcp.wallet.keyring') as mock_keyring:
            mock_keyring.get_password.return_value = stored_json
            
            result = retrieve_keypair("test_identity")
            
            assert result == mock_keypair_bytes
            mock_keyring.get_password.assert_called_once()

    @pytest.mark.skipif(not KEYRING_AVAILABLE, reason="keyring not installed")
    def test_retrieve_keypair_not_found(self):
        """Test retrieving non-existent keypair."""
        with patch('botvegas_mcp.wallet.keyring') as mock_keyring:
            mock_keyring.get_password.return_value = None
            
            result = retrieve_keypair("nonexistent_identity")
            
            assert result is None

    @pytest.mark.skipif(not KEYRING_AVAILABLE, reason="keyring not installed")
    def test_delete_keypair(self):
        """Test deleting a keypair from keyring."""
        with patch('botvegas_mcp.wallet.keyring') as mock_keyring:
            result = delete_keypair("test_identity")
            
            assert result is True
            mock_keyring.delete_password.assert_called_once()

    @pytest.mark.skipif(not KEYRING_AVAILABLE, reason="keyring not installed")
    def test_delete_keypair_not_found(self):
        """Test deleting non-existent keypair."""
        with patch('botvegas_mcp.wallet.keyring') as mock_keyring:
            from keyring.errors import PasswordDeleteError
            mock_keyring.delete_password.side_effect = PasswordDeleteError()
            
            result = delete_keypair("nonexistent_identity")
            
            assert result is False


# =============================================================================
# Test Balance Checking (Mocked RPC)
# =============================================================================


class TestBalanceChecking:
    """Test balance checking with mocked RPC calls."""

    def test_get_wallet_balances_success(self, mock_wallet_address):
        """Test successful balance retrieval."""
        with patch('botvegas_mcp.wallet.SolanaClient') as MockClient:
            # Mock SOL balance response
            mock_client = MagicMock()
            mock_sol_response = MagicMock()
            mock_sol_response.value = 1_500_000_000  # 1.5 SOL in lamports
            mock_client.get_balance.return_value = mock_sol_response
            
            # Mock token balance response (empty)
            mock_token_response = MagicMock()
            mock_token_response.value = []
            mock_client.get_token_accounts_by_owner.return_value = mock_token_response
            
            MockClient.return_value = mock_client
            
            sol, botvegas = get_wallet_balances(mock_wallet_address)
            
            assert sol == 1.5
            # botvegas is 0 or None when no token accounts
            assert botvegas == 0.0 or botvegas is None

    def test_get_wallet_balances_error_handling(self, mock_wallet_address):
        """Test balance retrieval error handling."""
        with patch('botvegas_mcp.wallet.SolanaClient') as MockClient:
            mock_client = MagicMock()
            mock_client.get_balance.side_effect = Exception("RPC error")
            MockClient.return_value = mock_client
            
            sol, botvegas = get_wallet_balances(mock_wallet_address)
            
            # On error, returns None for both (but may fall through to real RPC in some cases)
            # The important thing is it doesn't raise an exception
            assert sol is None or isinstance(sol, float)
            assert botvegas is None or isinstance(botvegas, float)


# =============================================================================
# Test High-Level Functions
# =============================================================================


class TestSetupWallet:
    """Test the setup_wallet high-level function."""

    @pytest.mark.skipif(not SOLDERS_AVAILABLE or not KEYRING_AVAILABLE, 
                        reason="solders or keyring not installed")
    def test_setup_wallet_new_identity(self):
        """Test setting up a wallet for a new identity."""
        with patch('botvegas_mcp.wallet.retrieve_keypair', return_value=None), \
             patch('botvegas_mcp.wallet.generate_keypair') as mock_gen, \
             patch('botvegas_mcp.wallet.store_keypair', return_value=True), \
             patch('botvegas_mcp.wallet.get_address_from_keypair') as mock_addr, \
             patch('botvegas_mcp.wallet.get_wallet_balances', return_value=(0.0, 0.0)):
            
            mock_gen.return_value = list(range(64))
            mock_addr.return_value = "TestWalletAddress123456789012345678901234"
            
            result = setup_wallet("new_agent")
            
            # Result is a dataclass
            assert isinstance(result, WalletSetupResult)
            assert result.success is True
            assert result.wallet_address == "TestWalletAddress123456789012345678901234"
            assert result.is_new_wallet is True
            assert result.funding_instructions is not None

    @pytest.mark.skipif(not SOLDERS_AVAILABLE or not KEYRING_AVAILABLE,
                        reason="solders or keyring not installed")
    def test_setup_wallet_existing_identity(self, mock_keypair_bytes, mock_wallet_address):
        """Test setup_wallet with existing wallet."""
        with patch('botvegas_mcp.wallet.retrieve_keypair', return_value=mock_keypair_bytes), \
             patch('botvegas_mcp.wallet.get_address_from_keypair', return_value=mock_wallet_address), \
             patch('botvegas_mcp.wallet.get_wallet_balances', return_value=(1.0, 500.0)):
            
            result = setup_wallet("existing_agent")
            
            assert isinstance(result, WalletSetupResult)
            assert result.success is True
            assert result.wallet_address == mock_wallet_address
            assert result.is_new_wallet is False
            assert result.sol_balance == 1.0
            assert result.botvegas_balance == 500.0


class TestCheckWalletBalance:
    """Test the check_wallet_balance function."""

    def test_check_balance_no_wallet(self):
        """Test checking balance for identity without wallet raises error."""
        with patch('botvegas_mcp.wallet.retrieve_keypair', return_value=None):
            with pytest.raises(ValueError, match="No wallet"):
                check_wallet_balance("no_wallet_identity")

    def test_check_balance_with_wallet(self, mock_keypair_bytes, mock_wallet_address):
        """Test checking balance for identity with wallet."""
        with patch('botvegas_mcp.wallet.retrieve_keypair', return_value=mock_keypair_bytes), \
             patch('botvegas_mcp.wallet.get_address_from_keypair', return_value=mock_wallet_address), \
             patch('botvegas_mcp.wallet.get_wallet_balances', return_value=(2.5, 1000.0)):
            
            result = check_wallet_balance("test_identity")
            
            assert isinstance(result, WalletBalanceResult)
            assert result.wallet_address == mock_wallet_address
            assert result.sol_balance == 2.5
            assert result.botvegas_balance == 1000.0
            assert result.can_play_staked_games is True

    def test_check_balance_insufficient_sol(self, mock_keypair_bytes, mock_wallet_address):
        """Test balance check with insufficient SOL."""
        with patch('botvegas_mcp.wallet.retrieve_keypair', return_value=mock_keypair_bytes), \
             patch('botvegas_mcp.wallet.get_address_from_keypair', return_value=mock_wallet_address), \
             patch('botvegas_mcp.wallet.get_wallet_balances', return_value=(0.001, 1000.0)):
            
            result = check_wallet_balance("test_identity")
            
            assert isinstance(result, WalletBalanceResult)
            assert result.can_play_staked_games is False
            # Should have a warning about insufficient SOL
            assert len(result.warnings) > 0
            assert any("SOL" in w for w in result.warnings)


class TestListAgentWallets:
    """Test the list_agent_wallets function."""

    def test_list_wallets_returns_structure(self):
        """Test that list_wallets returns expected structure."""
        result = list_agent_wallets()
        
        # Should have expected keys
        assert "wallets" in result
        assert "wallet_count" in result
        assert "total_sol" in result
        assert "total_botvegas" in result
        
        # wallet_count should match wallets length
        assert result["wallet_count"] == len(result["wallets"])
