use anyhow::{Context, Result};
use std::fs;
use zint::utils::{find_up, FoundryConfig};
use zint::Contract;

pub fn create_ztests_crate() -> Result<()> {
    // Find foundry.toml in the current directory or parent directories
    let foundry_toml_path = find_up("foundry.toml")?;
    let foundry_toml_content =
        fs::read_to_string(&foundry_toml_path).context("Failed to read foundry.toml")?;
    let foundry_config: FoundryConfig =
        toml::from_str(&foundry_toml_content).context("Failed to parse foundry.toml")?;

    // Determine the output directory (default to "out" if not specified)
    let out_dir = foundry_config
        .profile
        .default
        .out
        .unwrap_or_else(|| "out".to_string());
    let _out_path = foundry_toml_path.parent().unwrap().join(&out_dir);

    // Create ztests directory
    let ztests_path = foundry_toml_path.parent().unwrap().join("ztests");
    fs::create_dir_all(&ztests_path).context("Failed to create ztests directory")?;

    // Fetch the zink version from the environment variable set by zint-cli's build.rs
    let zink_version = std::env::var("ZINK_VERSION").unwrap_or_else(|_| "0.1.12".to_string());

    // Write ztests/Cargo.toml with workspace dependencies
    let cargo_toml_content = format!(
        r#"
[package]
name = "ztests"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"
zink = "{zink_version}"
zint = "^0.1"


[lib]
doctest = false

[features]
abi-import = ["zink/abi-import"]

[workspace]
"#,
        zink_version = zink_version
    );
    fs::write(ztests_path.join("Cargo.toml"), cargo_toml_content)
        .context("Failed to write ztests/Cargo.toml")?;

    // Create ztests/src directory
    let ztests_src_path = ztests_path.join("src");
    fs::create_dir_all(&ztests_src_path).context("Failed to create ztests/src directory")?;

    // Use find_foundry_outputs() to get the list of contract artifacts
    let outputs = Contract::find_foundry_outputs()?;
    if outputs.is_empty() {
        println!("No Foundry outputs found");
    }

    // Find all .json files in the out directory (compiled contract ABIs)
    let mut test_file_content = String::from(
        r#"#[cfg(test)]
mod tests {
    #[cfg(feature = "abi-import")]
    use zink::import;
    
    #[allow(unused_imports)]
    use zink::primitives::address::Address;
    #[allow(unused_imports)]
    use zink::primitives::u256::U256;

"#,
    );

    for (contract_name, abi_path, _bytecode) in outputs {
        let file_name = abi_path.file_name().unwrap().to_str().unwrap();
        let contract_struct_name = contract_name;

        // Parse the ABI to generate specific tests
        let abi_content = fs::read_to_string(&abi_path)?;
        let abi: serde_json::Value = match serde_json::from_str(&abi_content) {
            Ok(abi) => abi,
            Err(e) => {
                println!("Failed to parse ABI for {}: {}", file_name, e);
                continue;
            }
        };
        let abi_array = match abi["abi"].as_array() {
            Some(array) => array,
            None => {
                println!("No 'abi' field found in {}: {:?}", file_name, abi);
                continue;
            }
        };

        let mut test_body = String::new();
        for item in abi_array {
            let fn_name = item["name"].as_str().unwrap_or("");
            let fn_type = item["type"].as_str().unwrap_or("");
            let state_mutability = item["stateMutability"].as_str().unwrap_or("");
            let inputs = item["inputs"].as_array().unwrap_or(&vec![]).to_vec();
            let outputs = item["outputs"].as_array().unwrap_or(&vec![]).to_vec();

            if fn_type != "function" {
                continue;
            }

            // Generate test logic based on the function
            if fn_name == "set" && inputs.len() == 1 && inputs[0]["type"] == "uint256" {
                test_body.push_str(
                    r#"
            contract.set(U256::from(42))?;
"#,
                );
            } else if fn_name == "get"
                && outputs.len() == 1
                && outputs[0]["type"] == "uint256"
                && state_mutability == "view"
            {
                test_body.push_str(
                    r#"
            let retrieved = contract.get()?;
            println!("Retrieved value via get: {:?}", retrieved);
            assert_eq!(retrieved, U256::from(42));
"#,
                );
            }
        }

        // Only add the test if we generated some test body
        if !test_body.is_empty() {
            test_file_content.push_str(&format!(
                r#"
    #[test]
    fn test_{contract_struct_name}() -> anyhow::Result<()> {{
        #[cfg(feature = "abi-import")]
        {{
            import!("{out_dir}/Storage.sol/{file_name}");
            let contract_address = Address::from(zint::primitives::CONTRACT);
            println!("Contract address: {{contract_address:?}}");
            let contract = {contract_struct_name}::new(contract_address);
            {test_body}
            // Check storage directly
            let mut evm = contract.evm.clone();
            let storage_key = zink::storage::Mapping::<u8, U256>::storage_key(0);
            let stored_value = U256::from_be_bytes(
                evm.storage(*contract_address.as_bytes(), storage_key).unwrap_or([0u8; 32])
            );
            println!("Stored value in EVM: {{stored_value:?}}");
            assert_eq!(stored_value, U256::from(42));
            Ok(())
        }}
        #[cfg(not(feature = "abi-import"))]
        {{
            println!("Test skipped: abi-import feature not enabled");
            Ok(())
        }}
    }}
"#,
            ));
        } else {
            println!("No testable functions found in ABI for {}", file_name);
        }
    }

    test_file_content.push_str("}\n");

    // Write ztests/src/lib.rs
    fs::write(ztests_src_path.join("lib.rs"), test_file_content)
        .context("Failed to write ztests/src/lib.rs")?;

    println!("Created ztests crate at {}", ztests_path.display());
    Ok(())
}

pub fn run_ztests() -> Result<()> {
    // Find ztests directory
    let ztests_path = find_up("ztests/Cargo.toml")?
        .parent()
        .unwrap()
        .to_path_buf();

    // Deploy contracts before running tests
    let outputs = Contract::find_foundry_outputs()?;
    if outputs.is_empty() {
        println!("No Foundry outputs found");
        return Err(anyhow::anyhow!("No contracts to deploy"));
    }
    for (contract_name, _abi_path, bytecode) in outputs {
        println!(
            "Deploying contract {} with bytecode size {}",
            contract_name,
            bytecode.len()
        );
        let mut contract = Contract {
            wasm: bytecode,
            ..Default::default()
        };
        let evm = contract.deploy()?;
        evm.commit(true);
        println!(
            "Deployed contract {} at address {:?}",
            contract_name, contract.address
        );
    }

    let status = std::process::Command::new("cargo")
        .args(["nextest", "run", "--manifest-path", "ztests/Cargo.toml"])
        .current_dir(ztests_path.parent().unwrap())
        .status()
        .context("Failed to run cargo nextest")?;

    if !status.success() {
        anyhow::bail!("Tests failed");
    }

    Ok(())
}