use beacon_node_fallback::{ApiTopic, beacon_node_health::BeaconNodeSyncDistanceTiers};

use crate::exec::CommandLineTestExec;
use bls::{Keypair, PublicKeyBytes};
use eth2::types::GraffitiPolicy;
use initialized_validators::DEFAULT_WEB3SIGNER_KEEP_ALIVE;
use sensitive_url::SensitiveUrl;
use std::fs::File;
use std::io::Write;
use std::net::IpAddr;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use std::string::ToString;
use std::time::Duration;
use tempfile::TempDir;
use types::{Address, Slot};
use validator_client::Config;

/// Returns the `lighthouse validator_client` command.
fn base_cmd() -> Command {
    let lighthouse_bin = env!("CARGO_BIN_EXE_lighthouse");
    let path = lighthouse_bin
        .parse::<PathBuf>()
        .expect("should parse CARGO_TARGET_DIR");

    let mut cmd = Command::new(path);
    cmd.arg("validator_client");
    cmd
}

// Wrapper around `Command` for easier Command Line Testing.
struct CommandLineTest {
    cmd: Command,
}
impl CommandLineTest {
    fn new() -> CommandLineTest {
        let base_cmd = base_cmd();
        CommandLineTest { cmd: base_cmd }
    }
}

impl CommandLineTestExec for CommandLineTest {
    type Config = Config;

    fn cmd_mut(&mut self) -> &mut Command {
        &mut self.cmd
    }
}

#[test]
fn datadir_flag() {
    CommandLineTest::new()
        .run()
        .with_config_and_dir(|config, dir| {
            assert_eq!(config.validator_dir, dir.path().join("validators"));
            assert_eq!(config.secrets_dir, dir.path().join("secrets"));
        });
}

#[test]
fn validators_and_secrets_dir_flags() {
    let dir = TempDir::new().expect("Unable to create temporary directory");
    CommandLineTest::new()
        .flag("validators-dir", dir.path().join("validators").to_str())
        .flag("secrets-dir", dir.path().join("secrets").to_str())
        .run_with_no_datadir()
        .with_config(|config| {
            assert_eq!(config.validator_dir, dir.path().join("validators"));
            assert_eq!(config.secrets_dir, dir.path().join("secrets"));
        });
}

#[test]
fn datadir_and_secrets_dir_flags() {
    let dir = TempDir::new().expect("Unable to create temporary directory");
    CommandLineTest::new()
        .flag("datadir", dir.path().join("data").to_str())
        .flag("secrets-dir", dir.path().join("secrets").to_str())
        .run_with_no_datadir()
        .with_config(|config| {
            assert_eq!(
                config.validator_dir,
                dir.path().join("data").join("validators")
            );
            assert_eq!(config.secrets_dir, dir.path().join("secrets"));
        });
}

#[test]
fn validators_dir_alias_flags() {
    let dir = TempDir::new().expect("Unable to create temporary directory");
    CommandLineTest::new()
        .flag("validator-dir", dir.path().join("validators").to_str())
        .flag("secrets-dir", dir.path().join("secrets").to_str())
        .run_with_no_datadir()
        .with_config(|config| {
            assert_eq!(config.validator_dir, dir.path().join("validators"));
            assert_eq!(config.secrets_dir, dir.path().join("secrets"));
        });
}

#[test]
fn beacon_nodes_flag() {
    CommandLineTest::new()
        .flag(
            "beacon-nodes",
            Some("http://localhost:1001,https://project:secret@infura.io/"),
        )
        .run()
        .with_config(|config| {
            assert_eq!(
                config.beacon_nodes[0].expose_full().to_string(),
                "http://localhost:1001/"
            );
            assert_eq!(config.beacon_nodes[0].to_string(), "http://localhost:1001/");
            assert_eq!(
                config.beacon_nodes[1].expose_full().to_string(),
                "https://project:secret@infura.io/"
            );
            assert_eq!(config.beacon_nodes[1].to_string(), "https://infura.io/");
        });
}

#[test]
fn disable_auto_discover_flag() {
    CommandLineTest::new()
        .flag("disable-auto-discover", None)
        .run()
        .with_config(|config| assert!(config.disable_auto_discover));
}

#[test]
fn init_slashing_protections_flag() {
    CommandLineTest::new()
        .flag("init-slashing-protection", None)
        .run()
        .with_config(|config| assert!(config.init_slashing_protection));
}

#[test]
fn use_long_timeouts_flag() {
    CommandLineTest::new()
        .flag("use-long-timeouts", None)
        .run()
        .with_config(|config| assert!(config.use_long_timeouts));
}

#[test]
fn long_timeouts_multiplier_flag_default() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert_eq!(config.long_timeouts_multiplier, 1));
}

#[test]
fn long_timeouts_multiplier_flag() {
    CommandLineTest::new()
        .flag("use-long-timeouts", None)
        .flag("long-timeouts-multiplier", Some("10"))
        .run()
        .with_config(|config| assert_eq!(config.long_timeouts_multiplier, 10));
}

#[test]
fn beacon_nodes_tls_certs_flag() {
    let dir = TempDir::new().expect("Unable to create temporary directory");
    CommandLineTest::new()
        .flag(
            "beacon-nodes-tls-certs",
            Some(
                [
                    dir.path().join("certificate.crt").to_str().unwrap(),
                    dir.path().join("certificate2.crt").to_str().unwrap(),
                ]
                .join(",")
                .as_str(),
            ),
        )
        .run()
        .with_config(|config| {
            assert_eq!(
                config.beacon_nodes_tls_certs,
                Some(vec![
                    dir.path().join("certificate.crt"),
                    dir.path().join("certificate2.crt")
                ])
            )
        });
}

// Tests for Graffiti flags.
#[test]
fn graffiti_flag() {
    CommandLineTest::new()
        .flag("graffiti", Some("nice-graffiti"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.graffiti.unwrap().to_string(),
                "0x6e6963652d677261666669746900000000000000000000000000000000000000"
            )
        });
}
#[test]
fn graffiti_file_flag() {
    let dir = TempDir::new().expect("Unable to create temporary directory");
    let mut file = File::create(dir.path().join("graffiti.txt")).expect("Unable to create file");
    let new_key = Keypair::random();
    let pubkeybytes = PublicKeyBytes::from(new_key.pk);
    let contents = "default:nice-graffiti";
    file.write_all(contents.as_bytes())
        .expect("Unable to write to file");
    CommandLineTest::new()
        .flag(
            "graffiti-file",
            dir.path().join("graffiti.txt").as_os_str().to_str(),
        )
        .run()
        .with_config(|config| {
            // Public key not present so load default.
            assert_eq!(
                config
                    .graffiti_file
                    .clone()
                    .unwrap()
                    .load_graffiti(&pubkeybytes)
                    .unwrap()
                    .unwrap()
                    .to_string(),
                "0x6e6963652d677261666669746900000000000000000000000000000000000000"
            )
        });
}
#[test]
fn graffiti_file_with_pk_flag() {
    let dir = TempDir::new().expect("Unable to create temporary directory");
    let mut file = File::create(dir.path().join("graffiti.txt")).expect("Unable to create file");
    let new_key = Keypair::random();
    let pubkeybytes = PublicKeyBytes::from(new_key.pk);
    let contents = format!("{}:nice-graffiti", pubkeybytes);
    file.write_all(contents.as_bytes())
        .expect("Unable to write to file");
    CommandLineTest::new()
        .flag(
            "graffiti-file",
            dir.path().join("graffiti.txt").as_os_str().to_str(),
        )
        .run()
        .with_config(|config| {
            assert_eq!(
                config
                    .graffiti_file
                    .clone()
                    .unwrap()
                    .load_graffiti(&pubkeybytes)
                    .unwrap()
                    .unwrap()
                    .to_string(),
                "0x6e6963652d677261666669746900000000000000000000000000000000000000"
            )
        });
}

// Tests for graffiti-append flag
#[test]
fn graffiti_append_default() {
    CommandLineTest::new().run().with_config(|config| {
        assert_eq!(
            config.graffiti_policy,
            Some(GraffitiPolicy::AppendClientVersions)
        );
    });
}

#[test]
fn graffiti_append_true_flag() {
    CommandLineTest::new()
        .flag("graffiti-append", Some("true"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.graffiti_policy,
                Some(GraffitiPolicy::AppendClientVersions)
            );
        });
}

#[test]
fn graffiti_append_false_flag() {
    CommandLineTest::new()
        .flag("graffiti-append", Some("false"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.graffiti_policy,
                Some(GraffitiPolicy::PreserveUserGraffiti)
            );
        });
}

// Retain previous behaviour: `--graffiti-append` with no value is the same as
// `--graffiti-append true`.
#[test]
fn graffiti_append_no_value() {
    CommandLineTest::new()
        .flag("graffiti-append", None)
        .run()
        .with_config(|config| {
            assert_eq!(
                config.graffiti_policy,
                Some(GraffitiPolicy::AppendClientVersions)
            );
        });
}

// Tests for suggested-fee-recipient flags.
#[test]
fn fee_recipient_flag() {
    CommandLineTest::new()
        .flag(
            "suggested-fee-recipient",
            Some("0x00000000219ab540356cbb839cbe05303d7705fa"),
        )
        .run()
        .with_config(|config| {
            assert_eq!(
                config.validator_store.fee_recipient,
                Some(Address::from_str("0x00000000219ab540356cbb839cbe05303d7705fa").unwrap())
            )
        });
}

// Tests for HTTP flags.
#[test]
fn http_flag() {
    CommandLineTest::new()
        .flag("http", None)
        .run()
        .with_config(|config| assert!(config.http_api.enabled));
}
#[test]
fn http_address_flag() {
    let addr = "127.0.0.99".parse::<IpAddr>().unwrap();
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-address", Some("127.0.0.99"))
        .flag("unencrypted-http-transport", None)
        .run()
        .with_config(|config| assert_eq!(config.http_api.listen_addr, addr));
}
#[test]
fn http_address_ipv6_flag() {
    let addr = "::1".parse::<IpAddr>().unwrap();
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-address", Some("::1"))
        .flag("unencrypted-http-transport", None)
        .run()
        .with_config(|config| assert_eq!(config.http_api.listen_addr, addr));
}
#[test]
#[should_panic]
fn missing_unencrypted_http_transport_flag() {
    let addr = "127.0.0.99".parse::<IpAddr>().unwrap();
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-address", Some("127.0.0.99"))
        .run()
        .with_config(|config| assert_eq!(config.http_api.listen_addr, addr));
}
#[test]
#[should_panic]
fn missing_http_http_port_flag() {
    CommandLineTest::new()
        .flag("http-port", Some("9090"))
        .run()
        .with_config(|config| assert_eq!(config.http_api.listen_port, 9090));
}
#[test]
fn http_port_flag() {
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-port", Some("9090"))
        .run()
        .with_config(|config| assert_eq!(config.http_api.listen_port, 9090));
}
#[test]
fn http_allow_origin_flag() {
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-allow-origin", Some("http://localhost:9009"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.http_api.allow_origin,
                Some("http://localhost:9009".to_string())
            );
        });
}
#[test]
fn http_allow_origin_all_flag() {
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-allow-origin", Some("*"))
        .run()
        .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string())));
}
#[test]
fn http_allow_keystore_export_default() {
    CommandLineTest::new()
        .flag("http", None)
        .run()
        .with_config(|config| assert!(!config.http_api.allow_keystore_export));
}
#[test]
fn http_allow_keystore_export_present() {
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-allow-keystore-export", None)
        .run()
        .with_config(|config| assert!(config.http_api.allow_keystore_export));
}
#[test]
fn http_store_keystore_passwords_in_secrets_dir_default() {
    CommandLineTest::new()
        .flag("http", None)
        .run()
        .with_config(|config| assert!(!config.http_api.store_passwords_in_secrets_dir));
}
#[test]
fn http_store_keystore_passwords_in_secrets_dir_present() {
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-store-passwords-in-secrets-dir", None)
        .run()
        .with_config(|config| assert!(config.http_api.store_passwords_in_secrets_dir));
}

#[test]
fn http_token_path_flag_present() {
    let dir = TempDir::new().expect("Unable to create temporary directory");
    CommandLineTest::new()
        .flag("http", None)
        .flag("http-token-path", dir.path().join("api-token.txt").to_str())
        .run()
        .with_config(|config| {
            assert_eq!(
                config.http_api.http_token_path,
                dir.path().join("api-token.txt")
            );
        });
}

#[test]
fn http_token_path_default() {
    CommandLineTest::new()
        .flag("http", None)
        .run()
        .with_config(|config| {
            assert_eq!(
                config.http_api.http_token_path,
                config.validator_dir.join("api-token.txt")
            );
        });
}

// Tests for Metrics flags.
#[test]
fn metrics_flag() {
    CommandLineTest::new()
        .flag("metrics", None)
        .run()
        .with_config(|config| assert!(config.http_metrics.enabled));
}
#[test]
fn metrics_address_flag() {
    let addr = "127.0.0.99".parse::<IpAddr>().unwrap();
    CommandLineTest::new()
        .flag("metrics", None)
        .flag("metrics-address", Some("127.0.0.99"))
        .run()
        .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr));
}
#[test]
fn metrics_address_ipv6_flag() {
    let addr = "::1".parse::<IpAddr>().unwrap();
    CommandLineTest::new()
        .flag("metrics", None)
        .flag("metrics-address", Some("::1"))
        .run()
        .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr));
}
#[test]
fn metrics_port_flag() {
    CommandLineTest::new()
        .flag("metrics", None)
        .flag("metrics-port", Some("9090"))
        .run()
        .with_config(|config| assert_eq!(config.http_metrics.listen_port, 9090));
}
#[test]
fn metrics_port_flag_default() {
    CommandLineTest::new()
        .flag("metrics", None)
        .run()
        .with_config(|config| assert_eq!(config.http_metrics.listen_port, 5064));
}
#[test]
fn metrics_allow_origin_flag() {
    CommandLineTest::new()
        .flag("metrics", None)
        .flag("metrics-allow-origin", Some("http://localhost:9009"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.http_metrics.allow_origin,
                Some("http://localhost:9009".to_string())
            );
        });
}
#[test]
fn metrics_allow_origin_all_flag() {
    CommandLineTest::new()
        .flag("metrics", None)
        .flag("metrics-allow-origin", Some("*"))
        .run()
        .with_config(|config| assert_eq!(config.http_metrics.allow_origin, Some("*".to_string())));
}
#[test]
pub fn malloc_tuning_flag() {
    CommandLineTest::new()
        .flag("disable-malloc-tuning", None)
        .run()
        .with_config(|config| assert!(!config.http_metrics.allocator_metrics_enabled));
}
#[test]
pub fn malloc_tuning_default() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert!(config.http_metrics.allocator_metrics_enabled));
}
#[test]
fn doppelganger_protection_flag() {
    CommandLineTest::new()
        .flag("enable-doppelganger-protection", None)
        .run()
        .with_config(|config| assert!(config.enable_doppelganger_protection));
}
#[test]
fn no_doppelganger_protection_flag() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert!(!config.enable_doppelganger_protection));
}

#[test]
fn no_gas_limit_flag() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert!(config.validator_store.gas_limit == Some(60_000_000)));
}
#[test]
fn gas_limit_flag() {
    CommandLineTest::new()
        .flag("gas-limit", Some("600"))
        .flag("builder-proposals", None)
        .run()
        .with_config(|config| assert_eq!(config.validator_store.gas_limit, Some(600)));
}
#[test]
fn no_builder_proposals_flag() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert!(!config.validator_store.builder_proposals));
}
#[test]
fn builder_proposals_flag() {
    CommandLineTest::new()
        .flag("builder-proposals", None)
        .run()
        .with_config(|config| assert!(config.validator_store.builder_proposals));
}
#[test]
fn builder_boost_factor_flag() {
    CommandLineTest::new()
        .flag("builder-boost-factor", Some("150"))
        .run()
        .with_config(|config| assert_eq!(config.validator_store.builder_boost_factor, Some(150)));
}
#[test]
fn no_builder_boost_factor_flag() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert_eq!(config.validator_store.builder_boost_factor, None));
}
#[test]
fn prefer_builder_proposals_flag() {
    CommandLineTest::new()
        .flag("prefer-builder-proposals", None)
        .run()
        .with_config(|config| assert!(config.validator_store.prefer_builder_proposals));
}
#[test]
fn no_prefer_builder_proposals_flag() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert!(!config.validator_store.prefer_builder_proposals));
}
#[test]
fn no_builder_registration_timestamp_override_flag() {
    CommandLineTest::new()
        .run()
        .with_config(|config| assert!(config.builder_registration_timestamp_override.is_none()));
}
#[test]
fn builder_registration_timestamp_override_flag() {
    CommandLineTest::new()
        .flag("builder-registration-timestamp-override", Some("100"))
        .run()
        .with_config(|config| {
            assert_eq!(config.builder_registration_timestamp_override, Some(100))
        });
}
#[test]
fn monitoring_endpoint() {
    CommandLineTest::new()
        .flag("monitoring-endpoint", Some("http://example:8000"))
        .flag("monitoring-endpoint-period", Some("30"))
        .run()
        .with_config(|config| {
            let api_conf = config.monitoring_api.as_ref().unwrap();
            assert_eq!(api_conf.monitoring_endpoint.as_str(), "http://example:8000");
            assert_eq!(api_conf.update_period_secs, Some(30));
        });
}

#[test]
fn no_broadcast_flag() {
    CommandLineTest::new().run().with_config(|config| {
        assert_eq!(config.broadcast_topics, vec![ApiTopic::Subscriptions]);
    });
}

#[test]
fn broadcast_flag() {
    // "none" variant
    CommandLineTest::new()
        .flag("broadcast", Some("none"))
        .run()
        .with_config(|config| {
            assert_eq!(config.broadcast_topics, vec![]);
        });
    // "none" with other values is ignored
    CommandLineTest::new()
        .flag("broadcast", Some("none,sync-committee"))
        .run()
        .with_config(|config| {
            assert_eq!(config.broadcast_topics, vec![ApiTopic::SyncCommittee]);
        });
    // Other valid variants
    CommandLineTest::new()
        .flag("broadcast", Some("blocks,subscriptions"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.broadcast_topics,
                vec![ApiTopic::Blocks, ApiTopic::Subscriptions],
            );
        });
    // Omitted "subscription" overrides default
    CommandLineTest::new()
        .flag("broadcast", Some("attestations"))
        .run()
        .with_config(|config| {
            assert_eq!(config.broadcast_topics, vec![ApiTopic::Attestations]);
        });
}

/// Tests for validator fallback flags.
#[test]
fn beacon_nodes_sync_tolerances_flag_default() {
    CommandLineTest::new().run().with_config(|config| {
        assert_eq!(
            config.beacon_node_fallback.sync_tolerances,
            BeaconNodeSyncDistanceTiers::default()
        )
    });
}
#[test]
fn beacon_nodes_sync_tolerances_flag() {
    CommandLineTest::new()
        .flag("beacon-nodes-sync-tolerances", Some("4,4,4"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.beacon_node_fallback.sync_tolerances,
                BeaconNodeSyncDistanceTiers {
                    synced: Slot::new(4),
                    small: Slot::new(8),
                    medium: Slot::new(12),
                }
            );
        });
}

#[test]
#[should_panic(expected = "invalid value")]
fn wrong_broadcast_flag() {
    CommandLineTest::new()
        .flag("broadcast", Some("foo, subscriptions"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.broadcast_topics,
                vec![ApiTopic::Blocks, ApiTopic::Subscriptions],
            );
        });
}

#[test]
fn disable_latency_measurement_service() {
    CommandLineTest::new()
        .flag("disable-latency-measurement-service", None)
        .run()
        .with_config(|config| {
            assert!(!config.enable_latency_measurement_service);
        });
}

#[test]
fn validator_registration_batch_size() {
    CommandLineTest::new().run().with_config(|config| {
        assert_eq!(config.validator_registration_batch_size, 500);
    });
    CommandLineTest::new()
        .flag("validator-registration-batch-size", Some("100"))
        .run()
        .with_config(|config| {
            assert_eq!(config.validator_registration_batch_size, 100);
        });
}

#[test]
#[should_panic]
fn validator_registration_batch_size_zero_value() {
    CommandLineTest::new()
        .flag("validator-registration-batch-size", Some("0"))
        .run();
}

#[test]
fn validator_disable_web3_signer_slashing_protection_default() {
    CommandLineTest::new().run().with_config(|config| {
        assert!(config.validator_store.enable_web3signer_slashing_protection);
    });
}

#[test]
fn validator_disable_web3_signer_slashing_protection() {
    CommandLineTest::new()
        .flag("disable-slashing-protection-web3signer", None)
        .run()
        .with_config(|config| {
            assert!(!config.validator_store.enable_web3signer_slashing_protection);
        });
}

#[test]
fn validator_web3_signer_keep_alive_default() {
    CommandLineTest::new().run().with_config(|config| {
        assert_eq!(
            config.initialized_validators.web3_signer_keep_alive_timeout,
            DEFAULT_WEB3SIGNER_KEEP_ALIVE
        );
    });
}

#[test]
fn validator_web3_signer_keep_alive_override() {
    CommandLineTest::new()
        .flag("web3-signer-keep-alive-timeout", Some("1000"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.initialized_validators.web3_signer_keep_alive_timeout,
                Some(Duration::from_secs(1))
            );
        });
}

#[test]
fn validator_proposer_nodes_default_empty() {
    CommandLineTest::new().run().with_config(|config| {
        assert_eq!(config.proposer_nodes, vec![]);
    });
}

#[test]
fn validator_proposer_nodes() {
    CommandLineTest::new()
        .flag("proposer-nodes", Some("http://bn-1:5052,http://bn-2:5052"))
        .run()
        .with_config(|config| {
            assert_eq!(
                config.proposer_nodes,
                vec![
                    SensitiveUrl::parse("http://bn-1:5052").unwrap(),
                    SensitiveUrl::parse("http://bn-2:5052").unwrap()
                ]
            );
        });
}

// Head monitor is enabled by default.
#[test]
fn head_monitor_default() {
    CommandLineTest::new().run().with_config(|config| {
        assert!(config.enable_beacon_head_monitor);
    });
}

#[test]
fn head_monitor_disabled() {
    CommandLineTest::new()
        .flag("disable-beacon-head-monitor", None)
        .run()
        .with_config(|config| {
            assert!(!config.enable_beacon_head_monitor);
        });
}
