"""Tests for the transaction building and signing module.

These tests use mocking to avoid actual Solana network calls.
"""

import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from uuid import uuid4

from botvegas_mcp.transactions import (
    get_ata_address,
    check_ata_exists,
    build_anchor_instruction,
    build_create_ata_instruction,
    sign_and_submit_transaction,
    execute_deposit,
    execute_withdraw,
    TransactionResult,
    DepositResult,
    WithdrawResult,
    SOLDERS_AVAILABLE,
    SOLANA_RPC_AVAILABLE,
    SPL_TOKEN_AVAILABLE,
    TOKEN_PROGRAM_ID_STR,
    ASSOCIATED_TOKEN_PROGRAM_ID_STR,
)
from botvegas_mcp.wallet import CONFIG as WALLET_CONFIG


# =============================================================================
# Test Fixtures - Using valid Solana address format (32 bytes base58)
# =============================================================================


@pytest.fixture
def mock_wallet_address():
    """A valid Solana wallet address."""
    return "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"


@pytest.fixture
def mock_program_id():
    """A valid Solana program ID."""
    return "11111111111111111111111111111111"  # System program (valid 32-byte address)


@pytest.fixture
def mock_mint_address():
    """Mock token mint address (valid format)."""
    return "So11111111111111111111111111111111111111112"  # Wrapped SOL mint


@pytest.fixture
def mock_keypair_bytes():
    """Generate mock 64-byte keypair data."""
    return list(range(64))


@pytest.fixture
def mock_deposit_instruction(mock_program_id, mock_wallet_address):
    """Mock deposit instruction data from backend with valid addresses."""
    return {
        "program_id": mock_program_id,
        "instruction": "deposit",
        "accounts": {
            "game_escrow": "11111111111111111111111111111111",
            "token_vault": "11111111111111111111111111111111",
            "player_deposit": "11111111111111111111111111111111",
            "player": mock_wallet_address,
            "player_token_account": "",  # To be filled in
        },
        "data": {
            "game_id": str(uuid4()),
        },
    }


@pytest.fixture
def mock_withdraw_instruction(mock_program_id, mock_wallet_address):
    """Mock withdraw instruction data from backend with valid addresses."""
    return {
        "program_id": mock_program_id,
        "instruction": "withdraw",
        "accounts": {
            "config": "11111111111111111111111111111111",
            "game_escrow": "11111111111111111111111111111111",
            "token_vault": "11111111111111111111111111111111",
            "player_deposit": "11111111111111111111111111111111",
            "player": mock_wallet_address,
            "player_token_account": "",  # To be filled in
        },
        "data": {
            "game_id": str(uuid4()),
        },
    }


# =============================================================================
# Test ATA Utilities
# =============================================================================


class TestATAUtilities:
    """Test Associated Token Account utilities."""

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_get_ata_address_returns_valid_address(self, mock_wallet_address):
        """Test ATA derivation returns a valid base58 address."""
        # Use the actual BOTVEGAS mint from config
        ata = get_ata_address(mock_wallet_address, WALLET_CONFIG.BOTVEGAS_MINT)
        
        # Should be a valid base58 string
        assert ata is not None
        assert len(ata) >= 32
        assert len(ata) <= 50
        # Base58 characters only
        assert all(c in "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" for c in ata)

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_get_ata_address_deterministic(self, mock_wallet_address):
        """Test that ATA derivation is deterministic."""
        ata1 = get_ata_address(mock_wallet_address, WALLET_CONFIG.BOTVEGAS_MINT)
        ata2 = get_ata_address(mock_wallet_address, WALLET_CONFIG.BOTVEGAS_MINT)
        
        assert ata1 == ata2

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_get_ata_address_different_for_different_wallets(self):
        """Test that different wallets get different ATAs."""
        wallet1 = "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
        wallet2 = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
        
        ata1 = get_ata_address(wallet1, WALLET_CONFIG.BOTVEGAS_MINT)
        ata2 = get_ata_address(wallet2, WALLET_CONFIG.BOTVEGAS_MINT)
        
        assert ata1 != ata2

    def test_check_ata_exists_rpc_unavailable(self):
        """Test ATA existence check when RPC unavailable."""
        with patch('botvegas_mcp.transactions.SOLANA_RPC_AVAILABLE', False):
            result = check_ata_exists("SomeATAAddress", "https://api.mainnet-beta.solana.com")
            assert result is False

    def test_check_ata_exists_account_found(self):
        """Test ATA existence check when account exists."""
        with patch('botvegas_mcp.transactions.SOLANA_RPC_AVAILABLE', True), \
             patch('botvegas_mcp.transactions.SOLDERS_AVAILABLE', True), \
             patch('botvegas_mcp.transactions.SolanaClient') as MockClient, \
             patch('botvegas_mcp.transactions.Pubkey') as MockPubkey:
            
            mock_client = MagicMock()
            mock_response = MagicMock()
            mock_response.value = MagicMock()  # Non-None means account exists
            mock_client.get_account_info.return_value = mock_response
            MockClient.return_value = mock_client
            MockPubkey.from_string.return_value = MagicMock()
            
            result = check_ata_exists("SomeATAAddress", "https://api.mainnet-beta.solana.com")
            
            assert result is True

    def test_check_ata_exists_account_not_found(self):
        """Test ATA existence check when account doesn't exist."""
        with patch('botvegas_mcp.transactions.SOLANA_RPC_AVAILABLE', True), \
             patch('botvegas_mcp.transactions.SOLDERS_AVAILABLE', True), \
             patch('botvegas_mcp.transactions.SolanaClient') as MockClient, \
             patch('botvegas_mcp.transactions.Pubkey') as MockPubkey:
            
            mock_client = MagicMock()
            mock_response = MagicMock()
            mock_response.value = None  # None means account doesn't exist
            mock_client.get_account_info.return_value = mock_response
            MockClient.return_value = mock_client
            MockPubkey.from_string.return_value = MagicMock()
            
            result = check_ata_exists("SomeATAAddress", "https://api.mainnet-beta.solana.com")
            
            assert result is False


# =============================================================================
# Test Instruction Building
# =============================================================================


class TestInstructionBuilding:
    """Test Anchor instruction building."""

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_build_deposit_instruction(self, mock_deposit_instruction, mock_wallet_address):
        """Test building a deposit instruction."""
        # Fill in the player token account with valid address
        accounts = mock_deposit_instruction["accounts"].copy()
        accounts["player_token_account"] = "11111111111111111111111111111111"
        
        instruction = build_anchor_instruction(
            program_id=mock_deposit_instruction["program_id"],
            instruction_name=mock_deposit_instruction["instruction"],
            accounts=accounts,
            data=mock_deposit_instruction["data"],
            signer_pubkey=mock_wallet_address,
        )
        
        # Check instruction structure
        assert instruction is not None
        assert instruction.program_id is not None
        assert len(instruction.accounts) == 7  # deposit has 7 accounts
        assert len(instruction.data) == 8  # 8-byte discriminator

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_build_withdraw_instruction(self, mock_withdraw_instruction, mock_wallet_address):
        """Test building a withdraw instruction."""
        # Fill in the player token account with valid address
        accounts = mock_withdraw_instruction["accounts"].copy()
        accounts["player_token_account"] = "11111111111111111111111111111111"
        
        instruction = build_anchor_instruction(
            program_id=mock_withdraw_instruction["program_id"],
            instruction_name=mock_withdraw_instruction["instruction"],
            accounts=accounts,
            data=mock_withdraw_instruction["data"],
            signer_pubkey=mock_wallet_address,
        )
        
        # Check instruction structure
        assert instruction is not None
        assert instruction.program_id is not None
        assert len(instruction.accounts) == 7  # withdraw has 7 accounts
        assert len(instruction.data) == 8  # 8-byte discriminator

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_build_instruction_unknown_type(self, mock_wallet_address, mock_program_id):
        """Test that unknown instruction types raise error."""
        with pytest.raises(ValueError, match="Unknown instruction"):
            build_anchor_instruction(
                program_id=mock_program_id,
                instruction_name="unknown_instruction",
                accounts={},
                data={},
                signer_pubkey=mock_wallet_address,
            )

    def test_build_instruction_without_solders(self, mock_wallet_address):
        """Test graceful handling when solders not available."""
        with patch('botvegas_mcp.transactions.SOLDERS_AVAILABLE', False):
            with pytest.raises(ValueError, match="solders not available"):
                build_anchor_instruction(
                    program_id="11111111111111111111111111111111",
                    instruction_name="deposit",
                    accounts={},
                    data={},
                    signer_pubkey=mock_wallet_address,
                )

    @pytest.mark.skipif(not SOLDERS_AVAILABLE, reason="solders not installed")
    def test_build_create_ata_instruction(self, mock_wallet_address):
        """Test building a create ATA instruction."""
        instruction = build_create_ata_instruction(
            payer=mock_wallet_address,
            owner=mock_wallet_address,
            mint=WALLET_CONFIG.BOTVEGAS_MINT,
        )
        
        assert instruction is not None
        assert len(instruction.accounts) == 6  # create ATA has 6 accounts
        assert len(instruction.data) == 0  # no data for create ATA


# =============================================================================
# Test Transaction Signing
# =============================================================================


class TestTransactionSigning:
    """Test transaction signing and submission."""

    @pytest.mark.asyncio
    async def test_sign_and_submit_without_solders(self):
        """Test graceful handling when solders not available."""
        with patch('botvegas_mcp.transactions.SOLDERS_AVAILABLE', False):
            result = await sign_and_submit_transaction(
                instructions=[],
                keypair_bytes=list(range(64)),
                rpc_url="https://api.mainnet-beta.solana.com",
            )
            
            assert result.success is False
            assert "solders not available" in result.error

    @pytest.mark.asyncio
    async def test_sign_and_submit_without_rpc(self):
        """Test graceful handling when RPC not available."""
        with patch('botvegas_mcp.transactions.SOLDERS_AVAILABLE', True), \
             patch('botvegas_mcp.transactions.SOLANA_RPC_AVAILABLE', False):
            result = await sign_and_submit_transaction(
                instructions=[],
                keypair_bytes=list(range(64)),
                rpc_url="https://api.mainnet-beta.solana.com",
            )
            
            assert result.success is False
            assert "solana-py not available" in result.error


# =============================================================================
# Test Result Dataclasses
# =============================================================================


class TestResultDataclasses:
    """Test the result dataclasses."""

    def test_transaction_result_to_dict(self):
        """Test TransactionResult serialization."""
        result = TransactionResult(
            success=True,
            signature="abc123",
            error=None,
            solscan_url="https://solscan.io/tx/abc123",
        )
        
        d = result.to_dict()
        
        assert d["success"] is True
        assert d["signature"] == "abc123"
        assert d["error"] is None
        assert "solscan" in d["solscan_url"]

    def test_deposit_result_to_dict(self):
        """Test DepositResult serialization."""
        result = DepositResult(
            success=True,
            signature="def456",
            amount_deposited=100.0,
            wallet_address="WalletAddr",
            game_id="game-123",
            error=None,
            solscan_url="https://solscan.io/tx/def456",
            new_sol_balance=0.5,
            new_botvegas_balance=900.0,
        )
        
        d = result.to_dict()
        
        assert d["success"] is True
        assert d["amount_deposited"] == 100.0
        assert d["new_botvegas_balance"] == 900.0

    def test_withdraw_result_to_dict(self):
        """Test WithdrawResult serialization."""
        result = WithdrawResult(
            success=True,
            signature="ghi789",
            amount_withdrawn=150.0,
            wallet_address="WalletAddr",
            game_id="game-456",
            error=None,
            solscan_url="https://solscan.io/tx/ghi789",
            new_sol_balance=0.4,
            new_botvegas_balance=1150.0,
        )
        
        d = result.to_dict()
        
        assert d["success"] is True
        assert d["amount_withdrawn"] == 150.0
        assert d["new_botvegas_balance"] == 1150.0


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


class TestExecuteDeposit:
    """Test the execute_deposit function."""

    @pytest.mark.asyncio
    async def test_execute_deposit_no_wallet(self, mock_deposit_instruction):
        """Test deposit when identity has no wallet."""
        with patch('botvegas_mcp.transactions.retrieve_keypair', return_value=None):
            result = await execute_deposit(
                identity_name="no_wallet_identity",
                instruction_data=mock_deposit_instruction,
                buy_in_amount=100.0,
            )
            
            assert result.success is False
            assert "No wallet" in result.error

    @pytest.mark.asyncio
    async def test_execute_deposit_insufficient_sol(self, mock_deposit_instruction, mock_keypair_bytes, mock_wallet_address):
        """Test deposit with insufficient SOL for fees."""
        with patch('botvegas_mcp.transactions.retrieve_keypair', return_value=mock_keypair_bytes), \
             patch('botvegas_mcp.transactions.get_address_from_keypair', return_value=mock_wallet_address), \
             patch('botvegas_mcp.transactions.get_wallet_balances', return_value=(0.001, 1000.0)):
            
            result = await execute_deposit(
                identity_name="test_identity",
                instruction_data=mock_deposit_instruction,
                buy_in_amount=100.0,
            )
            
            assert result.success is False
            assert "Insufficient SOL" in result.error

    @pytest.mark.asyncio
    async def test_execute_deposit_insufficient_botvegas(self, mock_deposit_instruction, mock_keypair_bytes, mock_wallet_address):
        """Test deposit with insufficient $BOTVEGAS."""
        with patch('botvegas_mcp.transactions.retrieve_keypair', return_value=mock_keypair_bytes), \
             patch('botvegas_mcp.transactions.get_address_from_keypair', return_value=mock_wallet_address), \
             patch('botvegas_mcp.transactions.get_wallet_balances', return_value=(1.0, 50.0)):
            
            result = await execute_deposit(
                identity_name="test_identity",
                instruction_data=mock_deposit_instruction,
                buy_in_amount=100.0,  # More than balance
            )
            
            assert result.success is False
            assert "Insufficient $BOTVEGAS" in result.error


class TestExecuteWithdraw:
    """Test the execute_withdraw function."""

    @pytest.mark.asyncio
    async def test_execute_withdraw_no_wallet(self, mock_withdraw_instruction):
        """Test withdraw when identity has no wallet."""
        with patch('botvegas_mcp.transactions.retrieve_keypair', return_value=None):
            result = await execute_withdraw(
                identity_name="no_wallet_identity",
                instruction_data=mock_withdraw_instruction,
            )
            
            assert result.success is False
            assert "No wallet" in result.error

    @pytest.mark.asyncio
    async def test_execute_withdraw_insufficient_sol(self, mock_withdraw_instruction, mock_keypair_bytes, mock_wallet_address):
        """Test withdraw with insufficient SOL for fees."""
        with patch('botvegas_mcp.transactions.retrieve_keypair', return_value=mock_keypair_bytes), \
             patch('botvegas_mcp.transactions.get_address_from_keypair', return_value=mock_wallet_address), \
             patch('botvegas_mcp.transactions.get_wallet_balances', return_value=(0.001, 1000.0)):
            
            result = await execute_withdraw(
                identity_name="test_identity",
                instruction_data=mock_withdraw_instruction,
            )
            
            assert result.success is False
            assert "Insufficient SOL" in result.error
