#![cfg(not(debug_assertions))]

use eth2_keystore::{Keystore, KeystoreBuilder, PlainText};
use std::fs::{self, File};
use std::path::Path;
use tempfile::{tempdir, TempDir};
use types::{test_utils::generate_deterministic_keypair, EthSpec, Keypair, MainnetEthSpec};
use validator_dir::{
    Builder, BuilderError, ValidatorDir, ETH1_DEPOSIT_DATA_FILE, ETH1_DEPOSIT_TX_HASH_FILE,
    VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE,
};

/// A very weak password with which to encrypt the keystores.
pub const INSECURE_PASSWORD: &[u8] = &[50; 51];

/// Helper struct for configuring tests.
struct BuildConfig {
    random_voting_keystore: bool,
    random_withdrawal_keystore: bool,
    deposit_amount: Option<u64>,
    store_withdrawal_keystore: bool,
}

impl Default for BuildConfig {
    fn default() -> Self {
        Self {
            random_voting_keystore: true,
            random_withdrawal_keystore: true,
            deposit_amount: None,
            store_withdrawal_keystore: true,
        }
    }
}

/// Check that a keystore exists and can be decrypted with a password in password_dir
fn check_keystore<P: AsRef<Path>>(path: P, password_dir: P) -> Keypair {
    let mut file = File::open(path).unwrap();
    let keystore = Keystore::from_json_reader(&mut file).unwrap();
    let pubkey = keystore.pubkey();
    let password_path = password_dir.as_ref().join(format!("0x{}", pubkey));
    let password = fs::read(password_path).unwrap();
    keystore.decrypt_keypair(&password).unwrap()
}

/// Creates a keystore using `generate_deterministic_keypair`.
pub fn generate_deterministic_keystore(i: usize) -> Result<(Keystore, PlainText), String> {
    let keypair = generate_deterministic_keypair(i);

    let keystore = KeystoreBuilder::new(&keypair, INSECURE_PASSWORD, "".into())
        .map_err(|e| format!("Unable to create keystore builder: {:?}", e))?
        .build()
        .map_err(|e| format!("Unable to build keystore: {:?}", e))?;

    Ok((keystore, INSECURE_PASSWORD.to_vec().into()))
}

/// A testing harness for generating validator directories.
struct Harness {
    validators_dir: TempDir,
    password_dir: TempDir,
}

impl Harness {
    /// Create a new harness using temporary directories.
    pub fn new() -> Self {
        Self {
            validators_dir: tempdir().unwrap(),
            password_dir: tempdir().unwrap(),
        }
    }

    /// Create a `ValidatorDir` from the `config`, then assert that the `ValidatorDir` was generated
    /// correctly with respect to the `config`.
    pub fn create_and_test(&self, config: &BuildConfig) -> ValidatorDir {
        let spec = MainnetEthSpec::default_spec();

        /*
         * Build the `ValidatorDir`.
         */

        let builder = Builder::new(self.validators_dir.path().into())
            .password_dir(self.password_dir.path())
            // Note: setting the withdrawal keystore here ensure that it can get replaced by
            // further calls to `random_withdrawal_keystore`.
            .store_withdrawal_keystore(config.store_withdrawal_keystore);

        let builder = if config.random_voting_keystore {
            builder.random_voting_keystore().unwrap()
        } else {
            let (keystore, password) = generate_deterministic_keystore(0).unwrap();
            builder.voting_keystore(keystore, password.as_bytes())
        };

        let builder = if config.random_withdrawal_keystore {
            builder.random_withdrawal_keystore().unwrap()
        } else {
            let (keystore, password) = generate_deterministic_keystore(1).unwrap();
            builder.withdrawal_keystore(keystore, password.as_bytes())
        };

        let builder = if let Some(amount) = config.deposit_amount {
            builder.create_eth1_tx_data(amount, &spec)
        } else {
            builder
        };

        let mut validator = builder.build().unwrap();

        /*
         * Assert that the dir is consistent with the config.
         */

        let withdrawal_keystore_path = validator.dir().join(WITHDRAWAL_KEYSTORE_FILE);
        let password_dir = self.password_dir.path().into();

        // Ensure the voting keypair exists and can be decrypted.
        let voting_keypair =
            check_keystore(&validator.dir().join(VOTING_KEYSTORE_FILE), &password_dir);

        if !config.random_voting_keystore {
            assert_eq!(voting_keypair.pk, generate_deterministic_keypair(0).pk)
        }

        // Use OR here instead of AND so we *always* check for the withdrawal keystores if random
        // keystores were generated.
        if config.random_withdrawal_keystore || config.store_withdrawal_keystore {
            // Ensure the withdrawal keypair exists and can be decrypted.
            let withdrawal_keypair = check_keystore(&withdrawal_keystore_path, &password_dir);

            if !config.random_withdrawal_keystore {
                assert_eq!(withdrawal_keypair.pk, generate_deterministic_keypair(1).pk)
            }

            // The withdrawal keys should be distinct from the voting keypairs.
            assert_ne!(withdrawal_keypair.pk, voting_keypair.pk);
        }

        if !config.store_withdrawal_keystore && !config.random_withdrawal_keystore {
            assert!(!withdrawal_keystore_path.exists())
        }

        if let Some(amount) = config.deposit_amount {
            // Check that the deposit data can be decoded.
            let data = validator.eth1_deposit_data().unwrap().unwrap();

            // Ensure the amount is consistent.
            assert_eq!(data.deposit_data.amount, amount);

            // Check that the eth1 deposit data file is a valid format.
            let hex =
                String::from_utf8(fs::read(validator.dir().join(ETH1_DEPOSIT_DATA_FILE)).unwrap())
                    .unwrap();
            assert!(hex.starts_with("0x"), "deposit data should have 0x prefix");
            hex::decode(&hex[2..]).unwrap();
        } else {
            // If there was no deposit then we should return `Ok(None)`.
            assert!(validator.eth1_deposit_data().unwrap().is_none());
        }

        let tx_hash_path = validator.dir().join(ETH1_DEPOSIT_TX_HASH_FILE);

        // The eth1 deposit file should not exist, yet.
        assert!(!tx_hash_path.exists());

        let tx = "junk data";

        // Save a tx hash.
        validator.save_eth1_deposit_tx_hash(tx).unwrap();

        // Ensure the saved tx hash is correct.
        assert_eq!(fs::read(tx_hash_path).unwrap(), tx.as_bytes().to_vec());

        // Saving a second tx hash should fail.
        validator.save_eth1_deposit_tx_hash(tx).unwrap_err();

        validator
    }
}

#[test]
fn concurrency() {
    let harness = Harness::new();

    let val_dir = harness.create_and_test(&BuildConfig::default());
    let path = val_dir.dir().to_owned();

    // Should not re-open whilst opened after build.
    ValidatorDir::open(&path).unwrap_err();

    drop(val_dir);

    // Should re-open after drop.
    let val_dir = ValidatorDir::open(&path).unwrap();

    // Should not re-open when opened via ValidatorDir.
    ValidatorDir::open(&path).unwrap_err();

    drop(val_dir);

    // Should re-open again.
    ValidatorDir::open(&path).unwrap();
}

#[test]
fn without_voting_keystore() {
    let harness = Harness::new();

    assert!(matches!(
        Builder::new(harness.validators_dir.path().into(),)
            .password_dir(harness.password_dir.path())
            .random_withdrawal_keystore()
            .unwrap()
            .build(),
        Err(BuilderError::UninitializedVotingKeystore)
    ))
}

#[test]
fn without_withdrawal_keystore() {
    let harness = Harness::new();
    let spec = &MainnetEthSpec::default_spec();

    // Should build without withdrawal keystore if not storing the it or creating eth1 data.
    Builder::new(harness.validators_dir.path().into())
        .password_dir(harness.password_dir.path())
        .random_voting_keystore()
        .unwrap()
        .store_withdrawal_keystore(false)
        .build()
        .unwrap();

    assert!(
        matches!(
            Builder::new(harness.validators_dir.path().into(),)
                .password_dir(harness.password_dir.path())
                .random_voting_keystore()
                .unwrap()
                .store_withdrawal_keystore(true)
                .build(),
            Err(BuilderError::UninitializedWithdrawalKeystore)
        ),
        "storing the keystore requires keystore"
    );

    assert!(
        matches!(
            Builder::new(harness.validators_dir.path().into(),)
                .password_dir(harness.password_dir.path())
                .random_voting_keystore()
                .unwrap()
                .create_eth1_tx_data(42, spec)
                .build(),
            Err(BuilderError::UninitializedWithdrawalKeystore)
        ),
        "storing the keystore requires keystore"
    );
}

#[test]
fn deterministic_voting_keystore() {
    let harness = Harness::new();

    let config = BuildConfig {
        random_voting_keystore: false,
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn deterministic_withdrawal_keystore_without_saving() {
    let harness = Harness::new();

    let config = BuildConfig {
        random_withdrawal_keystore: false,
        store_withdrawal_keystore: false,
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn deterministic_withdrawal_keystore_with_saving() {
    let harness = Harness::new();

    let config = BuildConfig {
        random_withdrawal_keystore: false,
        store_withdrawal_keystore: true,
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn both_keystores_deterministic_without_saving() {
    let harness = Harness::new();

    let config = BuildConfig {
        random_voting_keystore: false,
        random_withdrawal_keystore: false,
        store_withdrawal_keystore: false,
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn both_keystores_random_without_saving() {
    let harness = Harness::new();

    let config = BuildConfig {
        random_voting_keystore: true,
        random_withdrawal_keystore: true,
        store_withdrawal_keystore: false,
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn both_keystores_deterministic_with_saving() {
    let harness = Harness::new();

    let config = BuildConfig {
        random_voting_keystore: false,
        random_withdrawal_keystore: false,
        store_withdrawal_keystore: true,
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn eth1_data() {
    let harness = Harness::new();

    let config = BuildConfig {
        deposit_amount: Some(123456),
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn store_withdrawal_keystore_without_eth1_data() {
    let harness = Harness::new();

    let config = BuildConfig {
        store_withdrawal_keystore: false,
        random_withdrawal_keystore: true,
        deposit_amount: None,
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}

#[test]
fn store_withdrawal_keystore_with_eth1_data() {
    let harness = Harness::new();

    let config = BuildConfig {
        store_withdrawal_keystore: false,
        random_withdrawal_keystore: true,
        deposit_amount: Some(32000000000),
        ..BuildConfig::default()
    };

    harness.create_and_test(&config);
}