//! Test environment for contract testing.
//!
//! Provides a complete testing environment that wraps the VM engine
//! with test utilities for deploying and interacting with contracts.

use std::collections::HashMap;
use std::sync::Arc;

use parking_lot::RwLock;
use synor_types::{Address, Hash256, Network};
use synor_vm::{
    context::{BlockInfo, CallContext, ExecutionContext, TransactionInfo},
    engine::ContractModule,
    storage::{ContractStorage, MemoryStorage},
    ContractId, ExecutionResult, StorageKey, StorageValue, VmEngine,
};

use crate::{MockStorage, TestAccount, TestError, TestResult};

/// Configuration for the test environment.
#[derive(Clone, Debug)]
pub struct TestEnvironmentConfig {
    /// Default gas limit for calls.
    pub default_gas_limit: u64,
    /// Chain ID.
    pub chain_id: u64,
    /// Network type.
    pub network: Network,
    /// Whether to auto-commit storage changes.
    pub auto_commit: bool,
    /// Block time advancement per call (ms).
    pub block_time_ms: u64,
}

impl Default for TestEnvironmentConfig {
    fn default() -> Self {
        TestEnvironmentConfig {
            default_gas_limit: 10_000_000,
            chain_id: 31337, // Common test chain ID
            network: Network::Devnet,
            auto_commit: true,
            block_time_ms: 1000,
        }
    }
}

/// The main test environment for contract testing.
///
/// Provides a complete sandbox for testing smart contracts including:
/// - Contract deployment and execution
/// - Account management with balances
/// - Block and time simulation
/// - Storage inspection
///
/// # Example
///
/// ```rust,ignore
/// let mut env = TestEnvironment::builder()
///     .with_account(TestAccount::with_balance(1000))
///     .with_account(TestAccount::with_balance(500))
///     .build();
///
/// let contract = env.deploy_contract(&bytecode, &init_params)?;
/// let result = env.call_contract(contract, "transfer", &args)?;
/// ```
pub struct TestEnvironment {
    /// The VM engine.
    engine: VmEngine,
    /// Mock storage.
    storage: Arc<RwLock<MockStorage>>,
    /// Deployed contracts.
    contracts: HashMap<ContractId, Arc<ContractModule>>,
    /// Test accounts by address.
    accounts: HashMap<Address, TestAccount>,
    /// Account balances (separate tracking).
    balances: HashMap<Address, u64>,
    /// Current block info.
    block: BlockInfo,
    /// Configuration.
    config: TestEnvironmentConfig,
    /// Transaction counter (for unique tx hashes).
    tx_counter: u64,
}

impl TestEnvironment {
    /// Creates a new test environment with default settings.
    pub fn new() -> TestResult<Self> {
        let engine = VmEngine::new().map_err(TestError::VmError)?;
        let coinbase = Address::from_ed25519_pubkey(Network::Devnet, &[0; 32]);

        Ok(TestEnvironment {
            engine,
            storage: Arc::new(RwLock::new(MockStorage::new())),
            contracts: HashMap::new(),
            accounts: HashMap::new(),
            balances: HashMap::new(),
            block: BlockInfo {
                height: 1,
                timestamp: 1704067200000, // Jan 1, 2024 00:00:00 UTC
                hash: Hash256::default(),
                blue_score: 1,
                daa_score: 1,
                coinbase,
            },
            config: TestEnvironmentConfig::default(),
            tx_counter: 0,
        })
    }

    /// Creates a builder for configuring the test environment.
    pub fn builder() -> TestEnvironmentBuilder {
        TestEnvironmentBuilder::new()
    }

    /// Deploys a contract and returns its ID.
    ///
    /// # Arguments
    ///
    /// * `bytecode` - The WASM bytecode of the contract
    /// * `init_params` - Initialization parameters (serialized)
    ///
    /// # Returns
    ///
    /// The contract ID on success.
    pub fn deploy_contract(
        &mut self,
        bytecode: &[u8],
        init_params: &[u8],
    ) -> TestResult<ContractId> {
        self.deploy_contract_as(bytecode, init_params, None, 0)
    }

    /// Deploys a contract as a specific account.
    pub fn deploy_contract_as(
        &mut self,
        bytecode: &[u8],
        init_params: &[u8],
        deployer: Option<&Address>,
        value: u64,
    ) -> TestResult<ContractId> {
        // Compile the contract
        let module = self
            .engine
            .compile(bytecode.to_vec())
            .map_err(|e| TestError::InvalidBytecode(e.to_string()))?;

        let contract_id = module.id;

        // Get deployer address
        let deployer_addr = deployer
            .cloned()
            .unwrap_or_else(|| Address::from_ed25519_pubkey(self.config.network, &[0; 32]));

        // Check balance if value > 0
        if value > 0 {
            let balance = self.balances.get(&deployer_addr).copied().unwrap_or(0);
            if balance < value {
                return Err(TestError::InsufficientBalance {
                    have: balance,
                    need: value,
                });
            }
        }

        // Create execution context
        let call = CallContext::new(
            contract_id,
            deployer_addr.clone(),
            value,
            init_params.to_vec(),
        );

        let mut storage = MemoryStorage::new();
        // Copy existing storage for this contract if any
        for (key, val) in self.storage.read().entries(&contract_id) {
            storage.set(&contract_id, key, val);
        }
        storage.commit();

        let context = ExecutionContext::new(
            self.block.clone(),
            self.make_tx_info(&deployer_addr),
            call,
            self.config.default_gas_limit,
            storage,
            self.config.chain_id,
        );

        // Initialize the contract
        let result = self
            .engine
            .execute(
                &module,
                "__synor_init",
                init_params,
                context,
                self.config.default_gas_limit,
            )
            .map_err(|e| TestError::DeploymentFailed(e.to_string()))?;

        // Apply storage changes
        if self.config.auto_commit {
            self.apply_storage_changes(&result);
        }

        // Debit value from deployer
        if value > 0 {
            if let Some(balance) = self.balances.get_mut(&deployer_addr) {
                *balance -= value;
            }
        }

        // Cache the compiled module
        self.contracts.insert(contract_id, Arc::new(module));

        // Advance block
        self.advance_block_internal();

        Ok(contract_id)
    }

    /// Calls a method on a deployed contract.
    ///
    /// # Arguments
    ///
    /// * `contract` - The contract ID
    /// * `method` - The method name to call
    /// * `args` - Method arguments (serialized)
    ///
    /// # Returns
    ///
    /// The execution result.
    pub fn call_contract(
        &mut self,
        contract: ContractId,
        method: &str,
        args: &[u8],
    ) -> TestResult<ExecutionResult> {
        self.call_contract_as(contract, method, args, None, 0)
    }

    /// Calls a method on a contract as a specific account.
    pub fn call_contract_as(
        &mut self,
        contract: ContractId,
        method: &str,
        args: &[u8],
        caller: Option<&Address>,
        value: u64,
    ) -> TestResult<ExecutionResult> {
        let module = self
            .contracts
            .get(&contract)
            .ok_or_else(|| TestError::CallFailed(format!("Contract not found: {}", contract)))?
            .clone();

        let caller_addr = caller
            .cloned()
            .unwrap_or_else(|| Address::from_ed25519_pubkey(self.config.network, &[1; 32]));

        // Check balance
        if value > 0 {
            let balance = self.balances.get(&caller_addr).copied().unwrap_or(0);
            if balance < value {
                return Err(TestError::InsufficientBalance {
                    have: balance,
                    need: value,
                });
            }
        }

        // Build call data (method selector + args)
        let call_data = crate::encode_call_args(method, args);

        // Create call context
        let call = CallContext::new(contract, caller_addr.clone(), value, call_data.clone());

        // Prepare storage
        let mut storage = MemoryStorage::new();
        for (key, val) in self.storage.read().entries(&contract) {
            storage.set(&contract, key, val);
        }
        storage.commit();

        let context = ExecutionContext::new(
            self.block.clone(),
            self.make_tx_info(&caller_addr),
            call,
            self.config.default_gas_limit,
            storage,
            self.config.chain_id,
        );

        // Execute
        let result = self
            .engine
            .execute(
                &module,
                "__synor_call",
                &call_data,
                context,
                self.config.default_gas_limit,
            )
            .map_err(TestError::VmError)?;

        // Apply storage changes
        if self.config.auto_commit {
            self.apply_storage_changes(&result);
        }

        // Debit value from caller
        if value > 0 {
            if let Some(balance) = self.balances.get_mut(&caller_addr) {
                *balance -= value;
            }
        }

        // Advance block
        self.advance_block_internal();

        Ok(result)
    }

    /// Calls a contract method as a static call (read-only).
    pub fn static_call(
        &self,
        contract: ContractId,
        method: &str,
        args: &[u8],
    ) -> TestResult<ExecutionResult> {
        let module = self
            .contracts
            .get(&contract)
            .ok_or_else(|| TestError::CallFailed(format!("Contract not found: {}", contract)))?
            .clone();

        let caller_addr = Address::from_ed25519_pubkey(self.config.network, &[0; 32]);
        let call_data = crate::encode_call_args(method, args);

        // Create static call context
        let call = CallContext::static_call(contract, caller_addr.clone(), call_data.clone());

        // Prepare read-only storage view
        let mut storage = MemoryStorage::new();
        for (key, val) in self.storage.read().entries(&contract) {
            storage.set(&contract, key, val);
        }
        storage.commit();

        // For static calls, use a dummy tx info since we don't mutate state
        let tx_info = TransactionInfo {
            hash: Hash256::default(),
            origin: caller_addr.clone(),
            gas_price: 1,
            gas_limit: self.config.default_gas_limit,
        };

        let context = ExecutionContext::new(
            self.block.clone(),
            tx_info,
            call,
            self.config.default_gas_limit,
            storage,
            self.config.chain_id,
        );

        self.engine
            .execute(
                &module,
                "__synor_call",
                &call_data,
                context,
                self.config.default_gas_limit,
            )
            .map_err(TestError::VmError)
    }

    /// Sets the balance of an address.
    pub fn set_balance(&mut self, address: &Address, amount: u64) {
        self.balances.insert(address.clone(), amount);
    }

    /// Gets the balance of an address.
    pub fn get_balance(&self, address: &Address) -> u64 {
        self.balances.get(address).copied().unwrap_or(0)
    }

    /// Transfers balance between addresses.
    pub fn transfer(&mut self, from: &Address, to: &Address, amount: u64) -> TestResult<()> {
        let from_balance = self.get_balance(from);
        if from_balance < amount {
            return Err(TestError::InsufficientBalance {
                have: from_balance,
                need: amount,
            });
        }

        self.set_balance(from, from_balance - amount);
        let to_balance = self.get_balance(to);
        self.set_balance(to, to_balance + amount);

        Ok(())
    }

    /// Advances the block number by n blocks.
    pub fn advance_block(&mut self, n: u64) {
        for _ in 0..n {
            self.advance_block_internal();
        }
    }

    /// Gets a storage value for a contract.
    pub fn get_storage(&self, contract: &ContractId, key: &StorageKey) -> Option<StorageValue> {
        self.storage.read().get(contract, key)
    }

    /// Sets a storage value directly (for test setup).
    pub fn set_storage(&self, contract: &ContractId, key: StorageKey, value: StorageValue) {
        self.storage.write().set_direct(contract, key, value);
    }

    /// Checks if a contract has any storage.
    pub fn has_storage(&self, contract: &ContractId) -> bool {
        !self.storage.read().is_empty(contract)
    }

    /// Gets all storage entries for a contract.
    pub fn get_all_storage(&self, contract: &ContractId) -> Vec<(StorageKey, StorageValue)> {
        self.storage.read().entries(contract)
    }

    /// Clears all storage for a contract.
    pub fn clear_storage(&self, contract: &ContractId) {
        self.storage.read().clear_contract(contract);
    }

    /// Takes a storage snapshot.
    pub fn snapshot(&self) -> crate::mock_storage::StorageSnapshot {
        self.storage.read().snapshot()
    }

    /// Restores from a storage snapshot.
    pub fn restore(&self, snapshot: crate::mock_storage::StorageSnapshot) {
        self.storage.read().restore(snapshot);
    }

    /// Gets the current block height.
    pub fn block_height(&self) -> u64 {
        self.block.height
    }

    /// Gets the current block timestamp.
    pub fn block_timestamp(&self) -> u64 {
        self.block.timestamp
    }

    /// Sets the block timestamp.
    pub fn set_timestamp(&mut self, timestamp: u64) {
        self.block.timestamp = timestamp;
    }

    /// Advances time by milliseconds.
    pub fn advance_time(&mut self, ms: u64) {
        self.block.timestamp += ms;
    }

    /// Registers a test account.
    pub fn register_account(&mut self, account: TestAccount) {
        let address = account.address();
        self.balances.insert(address.clone(), account.balance());
        self.accounts.insert(address, account);
    }

    /// Gets a registered account by address.
    pub fn get_account(&self, address: &Address) -> Option<&TestAccount> {
        self.accounts.get(address)
    }

    /// Gets a mutable reference to a registered account.
    pub fn get_account_mut(&mut self, address: &Address) -> Option<&mut TestAccount> {
        self.accounts.get_mut(address)
    }

    /// Gets all registered accounts.
    pub fn accounts(&self) -> &HashMap<Address, TestAccount> {
        &self.accounts
    }

    /// Gets the configuration.
    pub fn config(&self) -> &TestEnvironmentConfig {
        &self.config
    }

    /// Sets the default gas limit.
    pub fn set_gas_limit(&mut self, limit: u64) {
        self.config.default_gas_limit = limit;
    }

    /// Gets storage change history.
    pub fn storage_history(&self) -> Vec<synor_vm::StorageChange> {
        self.storage.read().get_history()
    }

    /// Clears storage history.
    pub fn clear_storage_history(&self) {
        self.storage.read().clear_history();
    }

    // Internal helpers

    fn advance_block_internal(&mut self) {
        self.block.height += 1;
        self.block.timestamp += self.config.block_time_ms;
        self.block.blue_score += 1;
        self.block.daa_score += 1;
        // Update block hash
        let mut hash_input = Vec::new();
        hash_input.extend_from_slice(&self.block.height.to_le_bytes());
        hash_input.extend_from_slice(&self.block.timestamp.to_le_bytes());
        self.block.hash = Hash256::from_bytes(blake3::hash(&hash_input).into());
    }

    fn make_tx_info(&mut self, origin: &Address) -> TransactionInfo {
        self.tx_counter += 1;
        let mut hash_input = self.tx_counter.to_le_bytes().to_vec();
        hash_input.extend_from_slice(origin.payload());

        TransactionInfo {
            hash: Hash256::from_bytes(blake3::hash(&hash_input).into()),
            origin: origin.clone(),
            gas_price: 1,
            gas_limit: self.config.default_gas_limit,
        }
    }

    fn apply_storage_changes(&self, result: &ExecutionResult) {
        let storage = self.storage.write();
        for change in &result.storage_changes {
            match &change.new_value {
                Some(value) => {
                    storage.set_direct(&change.contract, change.key, value.clone());
                }
                None => {
                    storage.delete_direct(&change.contract, &change.key);
                }
            }
        }
    }
}

impl Default for TestEnvironment {
    fn default() -> Self {
        Self::new().expect("Failed to create default TestEnvironment")
    }
}

/// Builder for creating test environments with custom configuration.
#[derive(Default)]
pub struct TestEnvironmentBuilder {
    accounts: Vec<TestAccount>,
    config: TestEnvironmentConfig,
    initial_storage: HashMap<ContractId, Vec<(StorageKey, StorageValue)>>,
    initial_timestamp: Option<u64>,
    initial_block_height: Option<u64>,
}

impl TestEnvironmentBuilder {
    /// Creates a new builder with default settings.
    pub fn new() -> Self {
        TestEnvironmentBuilder {
            accounts: Vec::new(),
            config: TestEnvironmentConfig::default(),
            initial_storage: HashMap::new(),
            initial_timestamp: None,
            initial_block_height: None,
        }
    }

    /// Adds a test account.
    pub fn with_account(mut self, account: TestAccount) -> Self {
        self.accounts.push(account);
        self
    }

    /// Adds multiple test accounts.
    pub fn with_accounts(mut self, accounts: Vec<TestAccount>) -> Self {
        self.accounts.extend(accounts);
        self
    }

    /// Sets the default gas limit.
    pub fn with_gas_limit(mut self, limit: u64) -> Self {
        self.config.default_gas_limit = limit;
        self
    }

    /// Sets the chain ID.
    pub fn with_chain_id(mut self, chain_id: u64) -> Self {
        self.config.chain_id = chain_id;
        self
    }

    /// Sets the network.
    pub fn with_network(mut self, network: Network) -> Self {
        self.config.network = network;
        self
    }

    /// Sets auto-commit behavior.
    pub fn with_auto_commit(mut self, auto_commit: bool) -> Self {
        self.config.auto_commit = auto_commit;
        self
    }

    /// Sets the block time.
    pub fn with_block_time(mut self, ms: u64) -> Self {
        self.config.block_time_ms = ms;
        self
    }

    /// Sets initial storage for a contract.
    pub fn with_storage(
        mut self,
        contract: ContractId,
        entries: Vec<(StorageKey, StorageValue)>,
    ) -> Self {
        self.initial_storage.insert(contract, entries);
        self
    }

    /// Sets the initial timestamp.
    pub fn with_timestamp(mut self, timestamp: u64) -> Self {
        self.initial_timestamp = Some(timestamp);
        self
    }

    /// Sets the initial block height.
    pub fn with_block_height(mut self, height: u64) -> Self {
        self.initial_block_height = Some(height);
        self
    }

    /// Builds the test environment.
    pub fn build(self) -> TestEnvironment {
        let mut env = TestEnvironment::new().expect("Failed to create TestEnvironment");
        env.config = self.config;

        // Register accounts
        for account in self.accounts {
            env.register_account(account);
        }

        // Set initial storage
        for (contract, entries) in self.initial_storage {
            for (key, value) in entries {
                env.set_storage(&contract, key, value);
            }
        }

        // Set initial timestamp if specified
        if let Some(timestamp) = self.initial_timestamp {
            env.block.timestamp = timestamp;
        }

        // Set initial block height if specified
        if let Some(height) = self.initial_block_height {
            env.block.height = height;
        }

        env
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_account::predefined;

    #[test]
    fn test_environment_creation() {
        let env = TestEnvironment::new().unwrap();
        assert_eq!(env.block_height(), 1);
        assert!(env.block_timestamp() > 0);
    }

    #[test]
    fn test_environment_builder() {
        let alice = predefined::alice().set_balance(1000);
        let bob = predefined::bob().set_balance(500);

        let env = TestEnvironment::builder()
            .with_account(alice.clone())
            .with_account(bob.clone())
            .with_gas_limit(5_000_000)
            .with_chain_id(12345)
            .build();

        assert_eq!(env.get_balance(&alice.address()), 1000);
        assert_eq!(env.get_balance(&bob.address()), 500);
        assert_eq!(env.config().default_gas_limit, 5_000_000);
        assert_eq!(env.config().chain_id, 12345);
    }

    #[test]
    fn test_balance_operations() {
        let mut env = TestEnvironment::new().unwrap();
        let alice = predefined::alice();
        let bob = predefined::bob();

        env.set_balance(&alice.address(), 1000);
        assert_eq!(env.get_balance(&alice.address()), 1000);

        env.transfer(&alice.address(), &bob.address(), 300).unwrap();
        assert_eq!(env.get_balance(&alice.address()), 700);
        assert_eq!(env.get_balance(&bob.address()), 300);

        // Insufficient balance
        let result = env.transfer(&alice.address(), &bob.address(), 1000);
        assert!(result.is_err());
    }

    #[test]
    fn test_block_advancement() {
        let mut env = TestEnvironment::new().unwrap();
        let initial_height = env.block_height();
        let initial_timestamp = env.block_timestamp();

        env.advance_block(5);

        assert_eq!(env.block_height(), initial_height + 5);
        assert_eq!(
            env.block_timestamp(),
            initial_timestamp + 5 * env.config().block_time_ms
        );
    }

    #[test]
    fn test_time_manipulation() {
        let mut env = TestEnvironment::new().unwrap();

        env.set_timestamp(2000000000000);
        assert_eq!(env.block_timestamp(), 2000000000000);

        env.advance_time(5000);
        assert_eq!(env.block_timestamp(), 2000000005000);
    }

    #[test]
    fn test_storage_operations() {
        let env = TestEnvironment::new().unwrap();
        let contract = ContractId::from_bytes([0x42; 32]);
        let key = StorageKey::from_bytes(b"test_key");
        let value = StorageValue::from_u64(12345);

        env.set_storage(&contract, key, value.clone());
        assert_eq!(env.get_storage(&contract, &key), Some(value));
        assert!(env.has_storage(&contract));

        env.clear_storage(&contract);
        assert!(!env.has_storage(&contract));
    }

    #[test]
    fn test_storage_snapshot() {
        let env = TestEnvironment::new().unwrap();
        let contract = ContractId::from_bytes([0x42; 32]);
        let key = StorageKey::from_bytes(b"test_key");

        env.set_storage(&contract, key, StorageValue::from_u64(100));
        let snapshot = env.snapshot();

        env.set_storage(&contract, key, StorageValue::from_u64(200));
        assert_eq!(
            env.get_storage(&contract, &key),
            Some(StorageValue::from_u64(200))
        );

        env.restore(snapshot);
        assert_eq!(
            env.get_storage(&contract, &key),
            Some(StorageValue::from_u64(100))
        );
    }
}
