# Copyright (c) Subzero Labs, Inc.
# SPDX-License-Identifier: Apache-2.0

"""
Comprehensive error case testing for the Rialo Python CDK.

Tests all error conditions, edge cases, and recovery mechanisms.
"""

import pytest
from rialo_py_cdk import (
    # Core types
    PublicKey,
    Hash,
    Signature,
    Wallet,
    # Transaction types
    TransactionBuilder,
    AccountMeta,
    Instruction,
    # Utility functions
    generate_keypair,
    keypair_from_seed,
    keypair_from_mnemonic,
    generate_mnemonic,
    public_key_from_keypair,
    sign,
    verify,
    create_wallet_with_mnemonic,
    recover_wallet_from_mnemonic,
    transfer_instruction,
    rlo_to_kelvin,
    kelvin_to_rlo,
    # Constants and URLs
    get_localnet_url,
    KELVIN_PER_RLO,
    # Exception
    RialoException,
)

# Try to import optional components that might not be available
try:
    from rialo_py_cdk import RialoRPCClient
except ImportError:
    RialoRPCClient = None

try:
    from rialo_py_cdk import ProgramDeployment, ProgramInvocation
except ImportError:
    ProgramDeployment = None
    ProgramInvocation = None

try:
    from rialo_py_cdk import Config
except ImportError:
    Config = None


class TestCryptographicErrorCases:
    """Test error cases for cryptographic operations."""
    
    def test_invalid_keypair_sizes(self):
        """Test error handling for invalid keypair sizes."""
        # Too short
        with pytest.raises(RialoException) as exc_info:
            keypair_from_seed(b"too_short")
        assert "length" in str(exc_info.value).lower()
        assert exc_info.value.code > 0
        
        # Too long
        with pytest.raises(RialoException) as exc_info:
            keypair_from_seed(b"a" * 100)
        assert "length" in str(exc_info.value).lower()
        
        # Empty
        with pytest.raises(RialoException) as exc_info:
            keypair_from_seed(b"")
        assert "length" in str(exc_info.value).lower()
    
    def test_invalid_public_key_creation(self):
        """Test error handling for invalid public key creation."""
        # Wrong size
        with pytest.raises(RialoException) as exc_info:
            PublicKey(b"wrong_size")
        assert "32 bytes" in str(exc_info.value) or "length" in str(exc_info.value).lower()
        
        # Empty bytes
        with pytest.raises(RialoException) as exc_info:
            PublicKey(b"")
        assert "length" in str(exc_info.value).lower()
        
        # None input
        with pytest.raises((TypeError, RialoException)):
            PublicKey(None)
    
    def test_invalid_hash_creation(self):
        """Test error handling for invalid hash creation."""
        # Wrong size
        with pytest.raises(RialoException) as exc_info:
            Hash(b"wrong_size")
        assert "32 bytes" in str(exc_info.value) or "length" in str(exc_info.value).lower()
        
        # Empty bytes
        with pytest.raises(RialoException) as exc_info:
            Hash(b"")
        assert "length" in str(exc_info.value).lower()
    
    def test_invalid_signature_creation(self):
        """Test error handling for invalid signature creation."""
        # Wrong size
        with pytest.raises(RialoException) as exc_info:
            Signature(b"wrong_size")
        assert "64 bytes" in str(exc_info.value) or "length" in str(exc_info.value).lower()
        
        # Empty bytes
        with pytest.raises(RialoException) as exc_info:
            Signature(b"")
        assert "length" in str(exc_info.value).lower()
    
    def test_invalid_base58_parsing(self):
        """Test error handling for invalid base58 strings."""
        # Invalid base58 characters
        with pytest.raises(RialoException) as exc_info:
            PublicKey.from_string("invalid_base58_!@#$%")
        assert "base58" in str(exc_info.value).lower() or "invalid" in str(exc_info.value).lower()
        
        # Empty string
        with pytest.raises(RialoException) as exc_info:
            PublicKey.from_string("")
        assert exc_info.value.code > 0
        
        # Wrong length after decoding
        with pytest.raises(RialoException) as exc_info:
            PublicKey.from_string("11111111111111111111111111111111")  # Valid base58 but wrong length
        assert "length" in str(exc_info.value).lower()
    
    def test_signing_with_invalid_keypair(self):
        """Test error handling when signing with invalid keypairs."""
        message = b"test message"
        
        # Wrong keypair size
        with pytest.raises(RialoException) as exc_info:
            sign(b"wrong_size", message)
        assert "64 bytes" in str(exc_info.value) or "length" in str(exc_info.value).lower()
        
        # Empty keypair
        with pytest.raises(RialoException) as exc_info:
            sign(b"", message)
        assert "length" in str(exc_info.value).lower()
    
    def test_verification_edge_cases(self):
        """Test signature verification edge cases."""
        keypair = generate_keypair()
        pubkey = public_key_from_keypair(keypair)
        message = b"test message"
        signature = sign(keypair, message)
        
        # Verify with corrupted signature
        corrupted_sig_bytes = bytearray(signature.to_bytes())
        corrupted_sig_bytes[0] = (corrupted_sig_bytes[0] + 1) % 256  # Change one byte
        corrupted_signature = Signature(bytes(corrupted_sig_bytes))
        
        assert not verify(pubkey, message, corrupted_signature)
        
        # Verify with corrupted public key
        corrupted_pubkey_bytes = bytearray(pubkey.to_bytes())
        corrupted_pubkey_bytes[0] = (corrupted_pubkey_bytes[0] + 1) % 256
        corrupted_pubkey = PublicKey(bytes(corrupted_pubkey_bytes))
        
        assert not verify(corrupted_pubkey, message, signature)
        
        # Empty message should still verify if that's what was signed
        empty_message = b""
        empty_signature = sign(keypair, empty_message)
        assert verify(pubkey, empty_message, empty_signature)


class TestMnemonicErrorCases:
    """Test error cases for BIP39 mnemonic operations."""
    
    def test_invalid_mnemonic_formats(self):
        """Test error handling for invalid mnemonic formats."""
        # Too few words
        with pytest.raises(RialoException) as exc_info:
            keypair_from_mnemonic("abandon abandon abandon")
        assert "mnemonic" in str(exc_info.value).lower()
        
        # Too many words
        long_mnemonic = " ".join(["abandon"] * 25)
        with pytest.raises(RialoException) as exc_info:
            keypair_from_mnemonic(long_mnemonic)
        assert "mnemonic" in str(exc_info.value).lower()
        
        # Invalid words not in BIP39 wordlist
        with pytest.raises(RialoException) as exc_info:
            keypair_from_mnemonic("invalid words not in wordlist here definitely wrong format test mnemonic phrase validation")
        assert "mnemonic" in str(exc_info.value).lower()
        
        # Empty mnemonic
        with pytest.raises(RialoException) as exc_info:
            keypair_from_mnemonic("")
        assert "empty" in str(exc_info.value).lower() or "mnemonic" in str(exc_info.value).lower()
        
        # None mnemonic
        with pytest.raises((TypeError, RialoException)):
            keypair_from_mnemonic(None)
    
    def test_wallet_recovery_error_cases(self):
        """Test error cases for wallet recovery from mnemonic."""
        # Invalid mnemonic
        with pytest.raises(RialoException) as exc_info:
            recover_wallet_from_mnemonic("test_wallet", "invalid mnemonic phrase")
        assert "mnemonic" in str(exc_info.value).lower()
        
        # Empty wallet name (should be allowed but tested)
        valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        wallet = recover_wallet_from_mnemonic("", valid_mnemonic)
        assert wallet.name == ""


class TestWalletErrorCases:
    """Test error cases for wallet operations."""
    
    def test_wallet_creation_errors(self):
        """Test error cases in wallet creation."""
        # Invalid keypair size
        with pytest.raises(RialoException) as exc_info:
            Wallet("test", b"invalid_keypair")
        assert "64 bytes" in str(exc_info.value) or "length" in str(exc_info.value).lower()
        
        # Empty name (should be allowed)
        keypair = generate_keypair()
        wallet = Wallet("", keypair)
        assert wallet.name == ""
    
    def test_wallet_account_operations_errors(self):
        """Test error cases in wallet account operations."""
        keypair = generate_keypair()
        wallet = Wallet("test_wallet", keypair)
        
        # Try to set non-existent account as active
        with pytest.raises(RialoException) as exc_info:
            wallet.set_active_account(999)
        assert "account" in str(exc_info.value).lower() or "not found" in str(exc_info.value).lower()
        
        # Try to sign with non-existent account
        with pytest.raises(RialoException) as exc_info:
            wallet.sign_with_account(b"test message", 999)
        assert "account" in str(exc_info.value).lower() or "not found" in str(exc_info.value).lower()
        
        # Try to get non-existent account
        result = wallet.get_account(999)
        assert result is None  # Should return None, not raise


class TestWalletProviderErrorCases:
    """Test error cases for wallet provider operations."""
    
    
    


class TestRpcClientErrorCases:
    """Test error cases for RPC client operations."""
    
    def test_invalid_rpc_url(self):
        """Test RPC client with invalid URLs."""
        if RialoRPCClient is None:
            pytest.skip("RialoRPCClient not available")
            
        # Invalid URL format - creation succeeds but may fail later
        client = RialoRPCClient("not_a_url")
        assert client is not None
        
        # Test with None (should fail with TypeError)
        with pytest.raises(TypeError):
            RialoRPCClient(None)
        
        # Test with empty URL - creation succeeds
        client = RialoRPCClient("")
        assert client is not None
    
    


class TestTransactionErrorCases:
    """Test error cases for transaction operations."""
    
    def test_transaction_builder_invalid_inputs(self):
        """Test transaction builder with invalid inputs."""
        # Invalid payer (wrong size)
        payer_bytes = b"wrong_size"
        blockhash_bytes = b"a" * 32
        
        with pytest.raises(RialoException):
            payer = PublicKey(payer_bytes)  # This should fail first
        
        # Valid payer, invalid blockhash
        valid_payer = PublicKey(b"a" * 32)
        
        with pytest.raises(RialoException):
            blockhash = Hash(b"wrong_size")  # This should fail
    
    def test_instruction_creation_errors(self):
        """Test error cases in instruction creation."""
        # Invalid program ID
        with pytest.raises(RialoException):
            program_id = PublicKey(b"wrong_size")
        
        # Valid instruction creation but with wrong account meta
        program_id = PublicKey(b"a" * 32)
        account_pubkey = PublicKey(b"b" * 32)
        
        # Create valid account meta
        account_meta = AccountMeta(account_pubkey, True, True)
        
        # Create instruction (should work)
        instruction = Instruction(program_id, [account_meta], b"instruction_data")
        assert instruction is not None
    
    def test_transfer_instruction_errors(self):
        """Test error cases in transfer instruction creation."""
        from_pubkey = PublicKey(b"a" * 32)
        to_pubkey = PublicKey(b"b" * 32)
        
        # Negative amount (should be caught by type system, but test anyway)
        # Python's int type can't be negative for u64, but test with 0
        instruction = transfer_instruction(from_pubkey, to_pubkey, 0)
        assert instruction is not None  # 0 amount should be allowed
        
        # Very large amount (should be within u64 range)
        max_amount = 2**64 - 1
        instruction = transfer_instruction(from_pubkey, to_pubkey, max_amount)
        assert instruction is not None


class TestProgramManagementErrorCases:
    """Test error cases for program management operations."""
    
    def test_program_deployment_invalid_path(self):
        """Test program deployment with invalid file paths."""
        if ProgramDeployment is None:
            pytest.skip("ProgramDeployment not available")
            
        # Non-existent file - creation succeeds but may fail during deployment
        deployment = ProgramDeployment("/non/existent/file.so")
        assert deployment is not None
        
        # Empty path should fail immediately
        with pytest.raises(RialoException) as exc_info:
            ProgramDeployment("")
        assert "empty" in str(exc_info.value).lower()
    
    def test_program_invocation_consumed_state(self):
        """Test program invocation consumed state errors."""
        program_id = PublicKey(b"a" * 32)
        invocation = ProgramInvocation(program_id)
        
        # Add account
        account = PublicKey(b"b" * 32)
        invocation.add_account(account, True, False)
        
        # Set data
        invocation.with_data(b"test_data")
        
        # Convert to instruction (consumes the invocation)
        instruction = invocation.into_instruction()
        assert instruction is not None
        
        # Now the invocation is consumed and should error on further use
        with pytest.raises(RialoException) as exc_info:
            invocation.add_account(account, False, True)
        assert "consumed" in str(exc_info.value).lower() or "moved" in str(exc_info.value).lower()
        
        with pytest.raises(RialoException) as exc_info:
            invocation.with_data(b"more_data")
        assert "consumed" in str(exc_info.value).lower() or "moved" in str(exc_info.value).lower()


class TestUnitConversionErrorCases:
    """Test error cases for unit conversion operations."""
    
    def test_invalid_unit_conversions(self):
        """Test error cases in unit conversions."""
        # Negative values
        assert rlo_to_kelvin(-1) == -1 * KELVIN_PER_RLO  # Should handle negative
        assert kelvin_to_rlo(-KELVIN_PER_RLO) == -1.0  # Should handle negative
        
        # Very large values
        large_rlo = 1e10
        kelvin_result = rlo_to_kelvin(large_rlo)
        assert kelvin_result == large_rlo * KELVIN_PER_RLO
        
        # Very small values
        small_rlo = 1e-9
        kelvin_result = rlo_to_kelvin(small_rlo)
        rlo_back = kelvin_to_rlo(int(kelvin_result))
        # Due to precision, we just check it's reasonable
        assert rlo_back >= 0
        
        # Zero values
        assert rlo_to_kelvin(0) == 0
        assert kelvin_to_rlo(0) == 0.0


class TestConfigurationErrorCases:
    """Test error cases for configuration operations."""
    
    def test_config_invalid_settings(self):
        """Test configuration with invalid settings."""
        if Config is None:
            pytest.skip("Config not available")
            
        config = Config()
        
        # Test getting default values (should not error)
        # These are properties, not methods
        assert isinstance(config.network, str)
        assert isinstance(config.rpc_url, str)
        assert isinstance(config.default_balance_unit, str)
        assert isinstance(config.verbose_mode, bool)


class TestMemoryAndResourceErrorCases:
    """Test error cases related to memory and resource management."""
    
    def test_large_data_handling(self):
        """Test handling of large data structures."""
        # Create large instruction data
        large_data = b"x" * (1024 * 1024)  # 1MB of data
        
        program_id = PublicKey(b"a" * 32)
        account_meta = AccountMeta(PublicKey(b"b" * 32), False, False)
        
        # Should be able to create instruction with large data
        instruction = Instruction(program_id, [account_meta], large_data)
        assert instruction is not None
        assert len(instruction.data) == len(large_data)
    
    def test_many_account_metas(self):
        """Test handling of many account metas in instruction."""
        program_id = PublicKey(b"a" * 32)
        
        # Create many account metas
        account_metas = []
        for i in range(100):
            pubkey_bytes = bytes([(i % 256)] * 32)
            account_metas.append(AccountMeta(PublicKey(pubkey_bytes), i % 2 == 0, i % 3 == 0))
        
        # Should be able to create instruction with many accounts
        instruction = Instruction(program_id, account_metas, b"data")
        assert instruction is not None
        assert len(instruction.accounts) == 100
    




if __name__ == "__main__":
    pytest.main([__file__, "-v"])