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

"""
Basic tests for the Rialo Python CDK.
"""

import pytest
from rialo_py_cdk import (
    generate_keypair,
    keypair_from_seed,
    generate_mnemonic,
    keypair_from_mnemonic,
    public_key_from_keypair,
    sign,
    verify,
    Wallet,
    PublicKey,
    Hash,
    Signature,
    get_localnet_url,
    KELVIN_PER_RLO,
    rlo_to_kelvin,
    kelvin_to_rlo,
    create_wallet_with_mnemonic,
    recover_wallet_from_mnemonic,
)


class TestCryptographyBasics:
    """Test basic cryptographic operations."""
    
    def test_generate_keypair(self):
        """Test keypair generation."""
        keypair = generate_keypair()
        assert len(keypair) == 64  # 32 bytes secret + 32 bytes public
        
        # Generate another one and ensure they're different
        keypair2 = generate_keypair()
        assert keypair != keypair2
    
    def test_keypair_from_seed(self):
        """Test deterministic keypair generation from seed."""
        seed = b"a" * 32
        keypair1 = keypair_from_seed(seed)
        keypair2 = keypair_from_seed(seed)
        
        # Should be deterministic
        assert keypair1 == keypair2
        assert len(keypair1) == 64
    
    def test_keypair_from_seed_invalid_length(self):
        """Test error handling for invalid seed length."""
        with pytest.raises(Exception):
            keypair_from_seed(b"too_short")
    
    def test_public_key_from_keypair(self):
        """Test extracting public key from keypair."""
        keypair = generate_keypair()
        pubkey = public_key_from_keypair(keypair)
        
        assert isinstance(pubkey, PublicKey)
        
        # Public key should be the same as the last 32 bytes of keypair
        expected_pubkey_bytes = bytes(keypair[32:])
        actual_pubkey_bytes = pubkey.to_bytes()
        assert actual_pubkey_bytes == expected_pubkey_bytes
    
    def test_sign_and_verify(self):
        """Test message signing and verification."""
        keypair = generate_keypair()
        pubkey = public_key_from_keypair(keypair)
        message = b"Hello, Rialo!"
        
        # Sign the message
        signature = sign(keypair, message)
        assert isinstance(signature, Signature)
        
        # Verify the signature
        assert verify(pubkey, message, signature)
        
        # Verify should fail with different message
        assert not verify(pubkey, b"Different message", signature)
        
        # Verify should fail with different public key
        other_keypair = generate_keypair()
        other_pubkey = public_key_from_keypair(other_keypair)
        assert not verify(other_pubkey, message, signature)


class TestMnemonics:
    """Test BIP39 mnemonic functionality."""
    
    def test_generate_mnemonic(self):
        """Test mnemonic generation."""
        mnemonic = generate_mnemonic()
        assert isinstance(mnemonic, str)
        words = mnemonic.split()
        assert len(words) == 12  # Standard BIP39 12-word mnemonic
    
    def test_keypair_from_mnemonic(self):
        """Test deterministic keypair from mnemonic."""
        mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        
        keypair1 = keypair_from_mnemonic(mnemonic)
        keypair2 = keypair_from_mnemonic(mnemonic)
        
        # Should be deterministic
        assert keypair1 == keypair2
        assert len(keypair1) == 64
    
    def test_keypair_from_mnemonic_with_passphrase(self):
        """Test mnemonic with passphrase."""
        mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        
        keypair_no_passphrase = keypair_from_mnemonic(mnemonic)
        keypair_with_passphrase = keypair_from_mnemonic(mnemonic, "password123")
        
        # Should be different with different passphrases
        assert keypair_no_passphrase != keypair_with_passphrase


class TestCoreTypes:
    """Test core type wrappers."""
    
    def test_public_key_creation(self):
        """Test PublicKey creation and operations."""
        keypair = generate_keypair()
        pubkey_bytes = keypair[32:]  # Extract public key bytes
        
        # Create from bytes
        pubkey = PublicKey(pubkey_bytes)
        assert pubkey.to_bytes() == bytes(pubkey_bytes)
        
        # Create from string should work with base58
        pubkey_str = str(pubkey)
        pubkey_from_str = PublicKey.from_string(pubkey_str)
        assert pubkey_from_str.to_bytes() == bytes(pubkey_bytes)
    
    def test_hash_creation(self):
        """Test Hash creation and operations."""
        hash_bytes = b"a" * 32
        
        # Create from bytes
        hash_obj = Hash(hash_bytes)
        assert hash_obj.to_bytes() == hash_bytes
        
        # Create from string should work with base58
        hash_str = str(hash_obj)
        hash_from_str = Hash.from_string(hash_str)
        assert hash_from_str.to_bytes() == hash_bytes
    
    def test_signature_creation(self):
        """Test Signature creation and operations."""
        keypair = generate_keypair()
        message = b"test message"
        
        signature = sign(keypair, message)
        sig_bytes = signature.to_bytes()
        assert len(sig_bytes) == 64
        
        # Create from bytes
        sig_from_bytes = Signature(sig_bytes)
        assert sig_from_bytes.to_bytes() == sig_bytes
        
        # Create from string should work with base58
        sig_str = str(signature)
        sig_from_str = Signature.from_string(sig_str)
        assert sig_from_str.to_bytes() == sig_bytes


class TestWallet:
    """Test wallet functionality."""
    
    def test_wallet_creation(self):
        """Test basic wallet creation."""
        keypair = generate_keypair()
        wallet = Wallet("test_wallet", keypair)
        
        assert wallet.name == "test_wallet"
        assert isinstance(wallet.get_public_key(), PublicKey)
        assert wallet.get_mnemonic() is None  # No mnemonic provided
    
    def test_wallet_with_mnemonic(self):
        """Test wallet creation with mnemonic."""
        mnemonic = generate_mnemonic()
        keypair = keypair_from_mnemonic(mnemonic)
        wallet = Wallet("test_wallet", keypair, mnemonic)
        
        assert wallet.get_mnemonic() == mnemonic
    
    def test_wallet_signing(self):
        """Test wallet message signing."""
        keypair = generate_keypair()
        wallet = Wallet("test_wallet", keypair)
        message = b"test message"
        
        signature_bytes = wallet.sign(message)
        assert len(signature_bytes) == 64
        
        # Verify signature
        signature = Signature(signature_bytes)
        pubkey = wallet.get_public_key()
        assert verify(pubkey, message, signature)


class TestConstants:
    """Test constants and utility functions."""
    
    def test_network_urls(self):
        """Test network URL constants."""
        localhost = get_localnet_url()
        
        assert isinstance(localhost, str)
        assert localhost.startswith("http")
    
    def test_kelvin_per_rlo(self):
        """Test RLO/kelvin constants."""
        assert KELVIN_PER_RLO > 0
        assert isinstance(KELVIN_PER_RLO, int)
    
    def test_unit_conversions(self):
        """Test RLO/kelvin conversions."""
        # 1 RLO should convert to KELVIN_PER_RLO kelvin
        assert rlo_to_kelvin(1) == KELVIN_PER_RLO
        assert rlo_to_kelvin(0.5) == KELVIN_PER_RLO // 2
        
        # Reverse conversion
        assert kelvin_to_rlo(KELVIN_PER_RLO) == 1.0
        assert kelvin_to_rlo(KELVIN_PER_RLO // 2) == 0.5


class TestConvenienceFunctions:
    """Test convenience functions."""
    
    def test_create_wallet_with_mnemonic(self):
        """Test wallet creation with generated mnemonic."""
        wallet, mnemonic = create_wallet_with_mnemonic("test_wallet")
        
        assert isinstance(wallet, Wallet)
        assert isinstance(mnemonic, str)
        assert wallet.name == "test_wallet"
        assert wallet.get_mnemonic() == mnemonic
        
        # Mnemonic should be valid
        words = mnemonic.split()
        assert len(words) == 12
    
    def test_recover_wallet_from_mnemonic(self):
        """Test wallet recovery from mnemonic."""
        # First create a wallet
        original_wallet, mnemonic = create_wallet_with_mnemonic("original")
        original_pubkey = original_wallet.get_public_key_string()
        
        # Now recover it
        recovered_wallet = recover_wallet_from_mnemonic("recovered", mnemonic)
        recovered_pubkey = recovered_wallet.get_public_key_string()
        
        # Should have the same public key
        assert original_pubkey == recovered_pubkey
        assert recovered_wallet.get_mnemonic() == mnemonic
    
    def test_recover_wallet_from_mnemonic_edge_cases(self):
        """Test wallet recovery edge cases and error conditions."""
        from rialo_py_cdk import RialoException
        
        # Test with invalid mnemonic (wrong number of words)
        with pytest.raises(RialoException):
            recover_wallet_from_mnemonic("test", "invalid short mnemonic")
        
        # Test with invalid mnemonic (invalid words)
        with pytest.raises(RialoException):
            invalid_mnemonic = "invalid words that are not in bip39 wordlist at all here"
            recover_wallet_from_mnemonic("test", invalid_mnemonic)
        
        # Test with empty name
        valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        recovered = recover_wallet_from_mnemonic("", valid_mnemonic)
        assert recovered.name == ""
        
        # Test deterministic recovery (same mnemonic should produce same wallet)
        wallet1 = recover_wallet_from_mnemonic("wallet1", valid_mnemonic)
        wallet2 = recover_wallet_from_mnemonic("wallet2", valid_mnemonic)
        assert wallet1.get_public_key_string() == wallet2.get_public_key_string()
        
    def test_recover_wallet_with_passphrase(self):
        """Test wallet recovery with passphrase support."""
        valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        
        # Recover without passphrase
        wallet_no_passphrase = recover_wallet_from_mnemonic("no_pass", valid_mnemonic)
        
        # Recover with empty passphrase (should be same as no passphrase)
        wallet_empty_passphrase = recover_wallet_from_mnemonic("empty_pass", valid_mnemonic, "")
        assert wallet_no_passphrase.get_public_key_string() == wallet_empty_passphrase.get_public_key_string()
        
        # Recover with actual passphrase (should be different)
        wallet_with_passphrase = recover_wallet_from_mnemonic("with_pass", valid_mnemonic, "test_passphrase")
        assert wallet_no_passphrase.get_public_key_string() != wallet_with_passphrase.get_public_key_string()
        
        # Same passphrase should be deterministic
        wallet_same_passphrase = recover_wallet_from_mnemonic("same_pass", valid_mnemonic, "test_passphrase")
        assert wallet_with_passphrase.get_public_key_string() == wallet_same_passphrase.get_public_key_string()


class TestPhase2Improvements:
    """Test Phase 2 improvements including error recovery mechanisms."""
    
    def test_program_deployment_error_recovery(self):
        """Test that program deployment handles errors gracefully."""
        from rialo_py_cdk import ProgramDeployment, RialoException
        
        # Create deployment
        deployment = ProgramDeployment("/fake/path/program.so")
        
        # Test setting authority (should work)
        fake_authority = PublicKey(b"a" * 32)
        deployment.with_authority(fake_authority)  # Should not raise
        
        # Test that after authority is set, we can still deploy (will fail but gracefully)
        # Note: This will fail due to fake path, but should not crash
        from rialo_py_cdk import RialoRPCClient, InMemoryWalletProvider
        client = RialoRPCClient(get_localnet_url())
        
        provider = InMemoryWalletProvider()
        wallet, _ = create_wallet_with_mnemonic("test_payer")
        
        # Deployment should fail gracefully with proper error
        # Note: deploy() is a PyO3 method that requires the raw transport client
        with pytest.raises(RialoException):
            deployment.deploy(client._inner, wallet)
    
    def test_program_invocation_error_recovery(self):
        """Test that program invocation handles consumed state properly."""
        from rialo_py_cdk import ProgramInvocation, RialoException
        
        # Create invocation
        program_id = PublicKey(b"a" * 32)
        invocation = ProgramInvocation(program_id)
        
        # Add account (should work)
        account = PublicKey(b"b" * 32)
        invocation.add_account(account, False, False)  # Should not raise
        
        # Set data (should work)
        invocation.with_data(b"test_data")  # Should not raise
        
        # Convert to instruction (consumes the invocation)
        instruction = invocation.into_instruction()
        assert instruction is not None
        
        # Now trying to use the invocation again should fail gracefully
        with pytest.raises(RialoException) as exc_info:
            invocation.add_account(account, False, False)
        assert "consumed" 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()


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