//! `MockProvider` is a mock Ethereum provider that can be used for testing purposes.
//! It allows to simulate Ethereum state and behavior, by explicitly instructing
//! provider's responses on client requests.
//!
//! This can be useful for testing code that relies on providers without the need to
//! connect to a real network or spend real Ether. It also allows to test code in a
//! deterministic manner, as you can control the state and behavior of the provider.
//!
//! In these examples we use the common Arrange, Act, Assert (AAA) test approach.
//! It is a useful pattern for well-structured, understandable and maintainable tests.

use ethers::prelude::*;

#[tokio::main]
async fn main() -> eyre::Result<()> {
    mocked_block_number().await?;
    mocked_provider_dependency().await?;
    Ok(())
}

async fn mocked_block_number() -> eyre::Result<()> {
    // Arrange
    let mock = MockProvider::new();
    let block_num_1 = U64::from(1);
    let block_num_2 = U64::from(2);
    let block_num_3 = U64::from(3);
    // Mock responses are organized in a stack (LIFO)
    mock.push(block_num_1)?;
    mock.push(block_num_2)?;
    mock.push(block_num_3)?;

    // Act
    let ret_block_3: U64 = JsonRpcClient::request(&mock, "eth_blockNumber", ()).await?;
    let ret_block_2: U64 = JsonRpcClient::request(&mock, "eth_blockNumber", ()).await?;
    let ret_block_1: U64 = JsonRpcClient::request(&mock, "eth_blockNumber", ()).await?;

    // Assert
    assert_eq!(block_num_1, ret_block_1);
    assert_eq!(block_num_2, ret_block_2);
    assert_eq!(block_num_3, ret_block_3);

    Ok(())
}

/// Here we test the `OddBlockOracle` struct (defined below) that relies
/// on a Provider to perform some logics.
/// The Provider reference is expressed with trait bounds, enforcing lose coupling,
/// maintainability and testability.
async fn mocked_provider_dependency() -> eyre::Result<()> {
    // Arrange
    let (provider, mock) = crate::Provider::mocked();
    mock.push(U64::from(2))?;

    // Act
    // Let's mock the provider dependency (we ❤️ DI!) then ask for the answer
    let oracle = OddBlockOracle::new(provider);
    let answer: bool = oracle.is_odd_block().await?;

    // Assert
    assert!(answer);
    Ok(())
}

struct OddBlockOracle<P> {
    provider: Provider<P>,
}

impl<P> OddBlockOracle<P>
where
    P: JsonRpcClient,
{
    fn new(provider: Provider<P>) -> Self {
        Self { provider }
    }

    /// We want to test this!
    async fn is_odd_block(&self) -> eyre::Result<bool> {
        let block: U64 = self.provider.get_block_number().await?;
        Ok(block % 2 == U64::zero())
    }
}
