"""
Tests for RPC mining with transaction execution and receipts.

This module tests the complete mining flow:
1. Mining blocks applies coinbase rewards
2. Transactions in blocks update balances
3. Receipts are generated and retrievable
4. Block lookups return actual mined blocks
"""

import pytest
from rpc.tests import new_test_client, rpc_call

# Tolerance for comparing actual vs reported rewards (allows for rounding/fee variations)
REWARD_TOLERANCE = 0.9  # Accept if actual reward >= 90% of reported reward


def _get_premine_address_hex() -> str:
    """Helper to get the premine address as hex string."""
    from consensus.rewards import MAINNET_PREMINE_DISTRIBUTION
    from pq.py.address import decode_address
    
    premine_addr_bech32 = MAINNET_PREMINE_DISTRIBUTION[0][0]
    addr_record = decode_address(premine_addr_bech32)
    digest = bytes(addr_record.digest) if isinstance(addr_record.digest, list) else addr_record.digest
    premine_addr_bytes = digest[:32].ljust(32, b"\x00")
    return "0x" + premine_addr_bytes.hex()


def _parse_balance(result: dict) -> int:
    """Helper to parse balance from RPC result."""
    balance = result.get("result", 0)
    if isinstance(balance, str):
        return int(balance, 16) if balance.startswith("0x") else int(balance)
    return int(balance)


def test_mine_blocks_credits_coinbase():
    """Test that mining 1 block credits coinbase reward to miner address."""
    client, cfg, _ = new_test_client()
    
    # Get premine address (used as default miner address in tests)
    miner_addr_hex = _get_premine_address_hex()
    
    # Get initial balance
    initial_balance = _parse_balance(rpc_call(client, "state.getBalance", [miner_addr_hex]))
    print(f"Initial balance: {initial_balance} nANM")
    
    # Mine 1 block
    result = rpc_call(client, "miner.mine", [1])["result"]
    assert result["mined"] == 1, "Should mine exactly 1 block"
    assert "totalReward" in result, "Should report totalReward"
    assert "rewards" in result, "Should report rewards array"
    
    total_reward = result["totalReward"]
    rewards_list = result["rewards"]
    
    # Get final balance
    final_balance = _parse_balance(rpc_call(client, "state.getBalance", [miner_addr_hex]))
    print(f"Final balance: {final_balance} nANM")
    print(f"Reported reward: {total_reward} nANM")
    
    # Balance should increase by at least the block reward (1 ANM = 1_000_000_000 nANM)
    # Note: In tests, reward may be 0 if params not configured, but balance should not decrease
    assert final_balance >= initial_balance, f"Balance should not decrease: {initial_balance} -> {final_balance}"
    
    # If balance increased, verify reported reward is reasonable
    if final_balance > initial_balance:
        actual_reward = final_balance - initial_balance
        assert total_reward > 0, "Reported reward should be positive when balance increases"
        assert len(rewards_list) == 1, "Should have exactly one reward entry for one block"
        assert rewards_list[0]["reward"] == total_reward, "Reward entry should match totalReward"
        print(f"✓ Block reward applied: {actual_reward} nANM")
    else:
        # No reward configured (e.g., height > 0 with no emission params)
        assert total_reward == 0, "Reported reward should be 0 when balance unchanged"
        print(f"✓ No reward configured (balance unchanged)")


def test_send_tx_then_mine_includes_tx_and_receipt():
    """Test that sending a tx, then mining includes the tx and returns a receipt."""
    client, cfg, _ = new_test_client()
    
    # Define sender and receiver addresses
    # BLACK = sender (premine address with funds)
    # WHITE = receiver (new address)
    BLACK = _get_premine_address_hex()
    WHITE = "0x" + ("ff" * 32)  # Arbitrary recipient address
    
    # Get initial balances
    black_initial = _parse_balance(rpc_call(client, "state.getBalance", [BLACK]))
    white_initial = _parse_balance(rpc_call(client, "state.getBalance", [WHITE]))
    
    print(f"BLACK initial balance: {black_initial} nANM")
    print(f"WHITE initial balance: {white_initial} nANM")
    
    # Send transaction from BLACK to WHITE
    # NOTE: This requires tx.send RPC which may not be fully implemented yet
    # For now, we test mining empty blocks and verify the infrastructure works
    
    # Mine 1 block (empty for now, will include txs once tx.send is wired)
    result = rpc_call(client, "miner.mine", [1])["result"]
    assert result["mined"] == 1, "Should mine exactly 1 block"
    
    head_height = result["height"]
    print(f"Mined block at height: {head_height}")
    
    # Verify block is retrievable
    block = rpc_call(client, "chain.getBlockByNumber", [head_height, True])["result"]
    assert block is not None, f"Should be able to retrieve block at height {head_height}"
    assert block["number"] == head_height, f"Block number should match: {block.get('number')} vs {head_height}"
    
    # Verify block has expected structure
    assert "hash" in block, "Block should have hash"
    assert "transactions" in block, "Block should have transactions array"
    
    print(f"✓ Block retrieved successfully: hash={block['hash']}, txs={len(block['transactions'])}")
    
    # For now, we expect empty transactions (tx.send not yet integrated)
    # Once tx.send is wired, this test will verify:
    # 1. WHITE balance increases by transfer amount
    # 2. Receipt is non-null with matching blockNumber
    # 3. Receipt has status, gasUsed, logs, etc.


def test_chain_getBlockByNumber_returns_block():
    """Test that chain.getBlockByNumber returns actual block after mining."""
    client, cfg, _ = new_test_client()
    
    # Get initial head
    head_before = rpc_call(client, "chain.getHead")["result"]
    initial_height = head_before.get("height", 0)
    print(f"Initial head height: {initial_height}")
    
    # Mine 2 blocks
    result = rpc_call(client, "miner.mine", [2])["result"]
    assert result["mined"] == 2, "Should mine exactly 2 blocks"
    
    # Get new head
    head_after = rpc_call(client, "chain.getHead")["result"]
    final_height = head_after.get("height", 0)
    head_hash = head_after.get("hash")
    
    print(f"Final head height: {final_height}")
    print(f"Final head hash: {head_hash}")
    
    assert final_height >= initial_height + 2, f"Head should advance by at least 2: {initial_height} -> {final_height}"
    
    # Retrieve block at head height with full tx details
    block = rpc_call(client, "chain.getBlockByNumber", [final_height, True])["result"]
    assert block is not None, f"Should be able to retrieve block at height {final_height}"
    
    # Verify block structure
    assert "number" in block, "Block should have number field"
    assert block["number"] == final_height, f"Block number should match: {block.get('number')} vs {final_height}"
    assert "hash" in block, "Block should have hash field"
    assert block["hash"] == head_hash, f"Block hash should match head: {block.get('hash')} vs {head_hash}"
    assert "transactions" in block, "Block should have transactions array"
    
    print(f"✓ Block at height {final_height} retrieved successfully")
    print(f"  - hash: {block['hash']}")
    print(f"  - txs: {len(block['transactions'])}")
    
    # Test getBlockByHeight alias
    block_via_alias = rpc_call(client, "chain.getBlockByHeight", [final_height, True])["result"]
    assert block_via_alias is not None, "getBlockByHeight should work"
    assert block_via_alias["hash"] == block["hash"], "Alias should return same block"
    
    print(f"✓ chain.getBlockByHeight alias works")


def test_state_getAccount_returns_account_info():
    """Test that state.getAccount returns account details (address, nonce, balance)."""
    client, cfg, _ = new_test_client()
    
    # Get premine address
    addr_hex = _get_premine_address_hex()
    
    # Call state.getAccount
    result = rpc_call(client, "state.getAccount", [addr_hex])["result"]
    
    # Verify structure
    assert "address" in result, "Result should have address field"
    assert "nonce" in result, "Result should have nonce field"
    assert "balance" in result, "Result should have balance field"
    
    assert result["address"] == addr_hex, "Address should match input"
    assert isinstance(result["nonce"], int), "Nonce should be an integer"
    assert isinstance(result["balance"], str), "Balance should be a hex string"
    assert result["balance"].startswith("0x"), "Balance should be 0x-prefixed hex"
    
    # Parse balance
    balance_int = int(result["balance"], 16)
    print(f"✓ state.getAccount works:")
    print(f"  - address: {result['address']}")
    print(f"  - nonce: {result['nonce']}")
    print(f"  - balance: {result['balance']} ({balance_int} nANM)")
    
    # Mine a block to increment nonce
    rpc_call(client, "miner.mine", [1])
    
    # Check account again (nonce might change if txs are executed)
    result_after = rpc_call(client, "state.getAccount", [addr_hex])["result"]
    balance_after = int(result_after["balance"], 16)
    
    # Balance should not decrease
    assert balance_after >= balance_int, "Balance should not decrease after mining"
    
    print(f"✓ After mining:")
    print(f"  - nonce: {result_after['nonce']}")
    print(f"  - balance: {result_after['balance']} ({balance_after} nANM)")


def test_multiple_blocks_maintain_state():
    """Test that mining multiple blocks maintains consistent state."""
    client, cfg, _ = new_test_client()
    
    addr = _get_premine_address_hex()
    
    # Get initial state
    initial_balance = _parse_balance(rpc_call(client, "state.getBalance", [addr]))
    initial_nonce = rpc_call(client, "state.getNonce", [addr])["result"]
    
    print(f"Initial: balance={initial_balance} nANM, nonce={initial_nonce}")
    
    # Mine 5 blocks
    result = rpc_call(client, "miner.mine", [5])["result"]
    assert result["mined"] == 5, "Should mine exactly 5 blocks"
    assert len(result["rewards"]) == 5, "Should have 5 reward entries"
    
    total_reward = result["totalReward"]
    
    # Get final state
    final_balance = _parse_balance(rpc_call(client, "state.getBalance", [addr]))
    final_nonce = rpc_call(client, "state.getNonce", [addr])["result"]
    
    print(f"Final: balance={final_balance} nANM, nonce={final_nonce}")
    print(f"Total rewards: {total_reward} nANM")
    
    # Balance should not decrease
    assert final_balance >= initial_balance, f"Balance should not decrease: {initial_balance} -> {final_balance}"
    
    # If rewards were applied, verify they accumulate correctly
    if total_reward > 0:
        balance_increase = final_balance - initial_balance
        assert balance_increase >= total_reward * REWARD_TOLERANCE, \
            f"Balance increase ({balance_increase}) should be close to total rewards ({total_reward})"
    
    # Verify rewards are consistent
    sum_rewards = sum(r["reward"] for r in result["rewards"])
    assert sum_rewards == total_reward, \
        f"Sum of individual rewards ({sum_rewards}) should equal totalReward ({total_reward})"
    
    print(f"✓ State maintained correctly across 5 blocks")


def test_chain_getBlockByNumber_includes_txs_and_receipts():
    """
    Test that chain.getBlockByNumber returns both 'txs' and 'receipts' fields.
    
    This is a regression test for the issue where getBlockByNumber(..., true, true)
    was returning null for .result.txs and .result.receipts.
    
    Ensures:
    1. Both 'txs' and 'transactions' fields are present (txs is an alias)
    2. 'receipts' field is present when includeReceipts=True
    3. All fields are arrays (never null) even if empty
    """
    client, cfg, _ = new_test_client()
    
    # Mine 1 block
    result = rpc_call(client, "miner.mine", [1])["result"]
    assert result["mined"] == 1, "Should mine exactly 1 block"
    block_height = result["height"]
    
    # Call chain.getBlockByNumber with includeTxObjects=true, includeReceipts=true
    # Using positional params as in the problem statement: ["latest", true, true]
    response = rpc_call(client, "chain.getBlockByNumber", [block_height, True, True])
    assert "result" in response, "Response should have result field"
    
    block = response["result"]
    assert block is not None, f"Block at height {block_height} should not be null"
    
    # Verify 'transactions' field is present and is a list
    assert "transactions" in block, "Block should have 'transactions' field"
    assert isinstance(block["transactions"], list), "'transactions' should be a list, not null"
    
    # Verify 'txs' field is present (alias for 'transactions')
    assert "txs" in block, "Block should have 'txs' field (alias for 'transactions')"
    assert isinstance(block["txs"], list), "'txs' should be a list, not null"
    
    # Verify both aliases point to same data
    assert block["txs"] == block["transactions"], "'txs' and 'transactions' should be equal"
    
    # Verify 'receipts' field is present and is a list
    assert "receipts" in block, "Block should have 'receipts' field"
    assert isinstance(block["receipts"], list), "'receipts' should be a list, not null"
    
    print(f"✓ Block {block_height} has required fields:")
    print(f"  - transactions: {len(block['transactions'])} items")
    print(f"  - txs: {len(block['txs'])} items (alias)")
    print(f"  - receipts: {len(block['receipts'])} items")
    
    # Also test with "latest" parameter
    response_latest = rpc_call(client, "chain.getBlockByNumber", ["latest", True, True])
    block_latest = response_latest["result"]
    
    assert "txs" in block_latest, "Block retrieved with 'latest' should have 'txs' field"
    assert "transactions" in block_latest, "Block retrieved with 'latest' should have 'transactions' field"
    assert "receipts" in block_latest, "Block retrieved with 'latest' should have 'receipts' field"
    
    print(f"✓ Block 'latest' also has required fields")


if __name__ == "__main__":
    # Run tests directly for debugging
    pytest.main([__file__, "-v", "-s"])
