//! CLI overrides for transport-level auth config.
//!
//! Why this exists: an operator running the same suite against staging and
//! production should not have to fork the YAML to swap a client cert or an
//! AWS region. The `mcptest run` flags here override (or populate) the
//! parsed [`AuthSchemes`] so CLI values win over YAML, the same precedence
//! the header and variable flags already use.
//!
//! Secret discipline: the `*-env` / `*-key-env` flags name an environment
//! variable, never a raw secret. This module only ever copies a flag's
//! string value (a path, region, URL, env-var name, or algorithm) into the
//! config; it never reads or logs the secret the env var holds.

use mcptest_config::auth_schemes::AuthSchemes;
use mcptest_core::auth::{
    MtlsConfig, SigV4Config, SigV4Credentials, SignatureAlgorithm, WebBotAuthConfig,
};

use super::RunArgs;

/// Failure applying a CLI auth override.
///
/// Kept typed so the run handler can convert it to an `anyhow` error at the
/// CLI boundary with a clear, actionable message rather than a debug dump.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum AuthOverrideError {
    /// `--webbot-algorithm` did not name a known algorithm.
    #[error("--webbot-algorithm `{0}` is not recognized; expected `ed25519` or `rsa-pss`")]
    UnknownAlgorithm(String),
}

/// True when the operator set at least one auth-override flag.
///
/// Lets the caller skip the YAML overlay entirely when no flag is present,
/// so a normal run never constructs a partial config.
pub fn any_auth_override(args: &RunArgs) -> bool {
    args.client_cert.is_some()
        || args.client_key.is_some()
        || args.client_key_env.is_some()
        || args.ca_bundle.is_some()
        || args.aws_sigv4_region.is_some()
        || args.service.is_some()
        || args.profile.is_some()
        || args.webbot_key.is_some()
        || args.webbot_key_env.is_some()
        || args.webbot_agent.is_some()
        || args.webbot_algorithm.is_some()
}

/// Apply every set auth-override flag onto `schemes`, populating a scheme
/// block when the YAML did not declare one.
///
/// CLI values win over YAML. A scheme stays `None` when neither the YAML
/// nor any flag touches it. `--profile` selects a named AWS profile as the
/// SigV4 credential source; the keys themselves resolve from
/// `~/.aws/credentials` at sign time, never from the command line.
pub fn apply_auth_overrides(
    schemes: &mut AuthSchemes,
    args: &RunArgs,
) -> Result<(), AuthOverrideError> {
    apply_mtls(schemes, args);
    apply_sigv4(schemes, args);
    apply_web_bot_auth(schemes, args)?;
    Ok(())
}

/// Overlay the mTLS flags. Populates an [`MtlsConfig`] when the YAML had
/// none and any mTLS flag is set.
fn apply_mtls(schemes: &mut AuthSchemes, args: &RunArgs) {
    let touches_mtls = args.client_cert.is_some()
        || args.client_key.is_some()
        || args.client_key_env.is_some()
        || args.ca_bundle.is_some();
    if !touches_mtls {
        return;
    }
    let mtls = schemes.mtls.get_or_insert_with(MtlsConfig::default);
    if let Some(cert) = &args.client_cert {
        mtls.client_cert_path = Some(cert.clone());
    }
    if let Some(key) = &args.client_key {
        mtls.client_key_path = Some(key.clone());
    }
    if let Some(env) = &args.client_key_env {
        mtls.client_key_env = Some(env.clone());
    }
    if let Some(ca) = &args.ca_bundle {
        mtls.ca_bundle_path = Some(ca.clone());
    }
}

/// Overlay the SigV4 flags. Populates a [`SigV4Config`] when the YAML had
/// none and a SigV4 flag is set. `--profile` selects the named-profile
/// credential source, resolved from `~/.aws/credentials` at sign time.
fn apply_sigv4(schemes: &mut AuthSchemes, args: &RunArgs) {
    let touches_sigv4 =
        args.aws_sigv4_region.is_some() || args.service.is_some() || args.profile.is_some();
    if !touches_sigv4 {
        return;
    }
    let sigv4 = schemes.sigv4.get_or_insert_with(|| SigV4Config {
        region: String::new(),
        service: String::new(),
        credentials: SigV4Credentials::FromEnvironment,
        signature_lifetime: None,
    });
    if let Some(region) = &args.aws_sigv4_region {
        sigv4.region = region.clone();
    }
    if let Some(service) = &args.service {
        sigv4.service = service.clone();
    }
    if let Some(name) = &args.profile {
        sigv4.credentials = SigV4Credentials::Profile { name: name.clone() };
    }
}

/// Overlay the Web Bot Auth flags. Populates a [`WebBotAuthConfig`] when the
/// YAML had none and a Web Bot Auth flag is set. Parses the algorithm name.
fn apply_web_bot_auth(schemes: &mut AuthSchemes, args: &RunArgs) -> Result<(), AuthOverrideError> {
    let algorithm = match &args.webbot_algorithm {
        Some(name) => Some(parse_algorithm(name)?),
        None => None,
    };
    let touches_wba = args.webbot_key.is_some()
        || args.webbot_key_env.is_some()
        || args.webbot_agent.is_some()
        || algorithm.is_some();
    if !touches_wba {
        return Ok(());
    }
    let wba = schemes
        .web_bot_auth
        .get_or_insert_with(default_web_bot_auth);
    if let Some(key) = &args.webbot_key {
        wba.key_path = Some(key.clone());
    }
    if let Some(env) = &args.webbot_key_env {
        wba.key_env = Some(env.clone());
    }
    if let Some(agent) = &args.webbot_agent {
        wba.signature_agent = agent.clone();
    }
    if let Some(alg) = algorithm {
        wba.algorithm = alg;
    }
    Ok(())
}

/// Construct the config-default Web Bot Auth block, used when CLI flags
/// populate a scheme the YAML did not declare. The spec-default components
/// and expiry come from the config type's serde defaults; we build an empty
/// shell here and let the override fill the fields, with validation catching
/// anything still missing (a required `directory_url`).
fn default_web_bot_auth() -> WebBotAuthConfig {
    serde_yaml::from_str("directory_url: ''\nsignature_agent: ''\n")
        .expect("the empty web_bot_auth shell always deserializes")
}

/// Map an algorithm flag value to the typed enum, mirroring the YAML
/// `kebab-case` spelling so `--webbot-algorithm rsa-pss` matches the file.
fn parse_algorithm(name: &str) -> Result<SignatureAlgorithm, AuthOverrideError> {
    match name {
        "ed25519" => Ok(SignatureAlgorithm::Ed25519),
        "rsa-pss" => Ok(SignatureAlgorithm::RsaPss),
        other => Err(AuthOverrideError::UnknownAlgorithm(other.to_string())),
    }
}

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

    fn args() -> RunArgs {
        RunArgs::default()
    }

    #[test]
    fn no_flags_means_no_override() {
        assert!(!any_auth_override(&args()));
        let mut schemes = AuthSchemes::default();
        apply_auth_overrides(&mut schemes, &args()).unwrap();
        assert!(schemes.is_empty());
    }

    #[test]
    fn mtls_flags_populate_an_absent_block() {
        let run = RunArgs {
            client_cert: Some("/tmp/client.pem".into()),
            client_key: Some("/tmp/client.key".into()),
            ca_bundle: Some("/tmp/ca.pem".into()),
            ..args()
        };
        let mut schemes = AuthSchemes::default();
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let mtls = schemes.mtls.expect("mtls populated");
        assert_eq!(
            mtls.client_cert_path.as_deref(),
            Some(std::path::Path::new("/tmp/client.pem"))
        );
        assert_eq!(
            mtls.ca_bundle_path.as_deref(),
            Some(std::path::Path::new("/tmp/ca.pem"))
        );
    }

    #[test]
    fn cli_cert_overrides_yaml_cert() {
        let mut schemes = AuthSchemes {
            mtls: Some(MtlsConfig {
                client_cert_path: Some("/etc/yaml.pem".into()),
                client_key_path: Some("/etc/yaml.key".into()),
                ..Default::default()
            }),
            ..Default::default()
        };
        let run = RunArgs {
            client_cert: Some("/cli/override.pem".into()),
            ..args()
        };
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let mtls = schemes.mtls.unwrap();
        assert_eq!(
            mtls.client_cert_path.as_deref(),
            Some(std::path::Path::new("/cli/override.pem"))
        );
        // The untouched key path stays from the YAML.
        assert_eq!(
            mtls.client_key_path.as_deref(),
            Some(std::path::Path::new("/etc/yaml.key"))
        );
    }

    #[test]
    fn client_key_env_names_the_var_not_the_secret() {
        let run = RunArgs {
            client_cert: Some("/tmp/client.pem".into()),
            client_key_env: Some("MCPTEST_CLIENT_KEY".into()),
            ..args()
        };
        let mut schemes = AuthSchemes::default();
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let mtls = schemes.mtls.unwrap();
        assert_eq!(mtls.client_key_env.as_deref(), Some("MCPTEST_CLIENT_KEY"));
    }

    #[test]
    fn sigv4_region_and_service_override() {
        let mut schemes = AuthSchemes {
            sigv4: Some(SigV4Config {
                region: "us-east-1".into(),
                service: "execute-api".into(),
                credentials: SigV4Credentials::FromEnvironment,
                signature_lifetime: None,
            }),
            ..Default::default()
        };
        let run = RunArgs {
            aws_sigv4_region: Some("eu-west-1".into()),
            ..args()
        };
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let sigv4 = schemes.sigv4.unwrap();
        assert_eq!(sigv4.region, "eu-west-1");
        // Service untouched.
        assert_eq!(sigv4.service, "execute-api");
    }

    #[test]
    fn sigv4_flags_populate_absent_block_with_env_credentials() {
        let run = RunArgs {
            aws_sigv4_region: Some("us-west-2".into()),
            service: Some("lambda".into()),
            ..args()
        };
        let mut schemes = AuthSchemes::default();
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let sigv4 = schemes.sigv4.unwrap();
        assert_eq!(sigv4.region, "us-west-2");
        assert_eq!(sigv4.service, "lambda");
        assert_eq!(sigv4.credentials, SigV4Credentials::FromEnvironment);
    }

    #[test]
    fn profile_flag_selects_the_named_profile_credential_source() {
        let run = RunArgs {
            profile: Some("prod".into()),
            aws_sigv4_region: Some("us-east-1".into()),
            service: Some("execute-api".into()),
            ..args()
        };
        let mut schemes = AuthSchemes::default();
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let sigv4 = schemes.sigv4.expect("sigv4 populated");
        assert_eq!(
            sigv4.credentials,
            SigV4Credentials::Profile {
                name: "prod".into()
            }
        );
        assert_eq!(sigv4.region, "us-east-1");
    }

    #[test]
    fn profile_flag_alone_populates_an_absent_block() {
        let run = RunArgs {
            profile: Some("staging".into()),
            ..args()
        };
        let mut schemes = AuthSchemes::default();
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let sigv4 = schemes.sigv4.expect("sigv4 populated by profile alone");
        assert_eq!(
            sigv4.credentials,
            SigV4Credentials::Profile {
                name: "staging".into()
            }
        );
    }

    #[test]
    fn web_bot_auth_flags_override() {
        let mut schemes = AuthSchemes {
            web_bot_auth: Some(
                serde_yaml::from_str(
                    "key_path: /etc/bot.key\ndirectory_url: https://yaml.example/dir\nsignature_agent: https://yaml.example\n",
                )
                .unwrap(),
            ),
            ..Default::default()
        };
        let run = RunArgs {
            webbot_agent: Some("https://cli.example".into()),
            webbot_algorithm: Some("ed25519".into()),
            ..args()
        };
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let wba = schemes.web_bot_auth.unwrap();
        assert_eq!(wba.signature_agent, "https://cli.example");
        assert_eq!(wba.algorithm, SignatureAlgorithm::Ed25519);
        // The directory_url the CLI did not touch stays from the YAML.
        assert_eq!(wba.directory_url, "https://yaml.example/dir");
    }

    #[test]
    fn web_bot_auth_unknown_algorithm_errors() {
        let run = RunArgs {
            webbot_algorithm: Some("nonsense".into()),
            ..args()
        };
        let mut schemes = AuthSchemes::default();
        let err = apply_auth_overrides(&mut schemes, &run).unwrap_err();
        assert_eq!(err, AuthOverrideError::UnknownAlgorithm("nonsense".into()));
    }

    #[test]
    fn webbot_key_env_names_the_var() {
        let run = RunArgs {
            webbot_key_env: Some("MCPTEST_BOT_KEY".into()),
            webbot_agent: Some("https://cli.example".into()),
            ..args()
        };
        let mut schemes = AuthSchemes::default();
        apply_auth_overrides(&mut schemes, &run).unwrap();
        let wba = schemes.web_bot_auth.unwrap();
        assert_eq!(wba.key_env.as_deref(), Some("MCPTEST_BOT_KEY"));
    }
}
