use serde::{Deserialize, Serialize};

/// SSH authentication method.
///
/// Supports password authentication and private key authentication. Choose one.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AuthMethod {
    /// Password authentication
    Password {
        /// Login password
        #[serde(skip_serializing)]
        password: String,
    },
    /// Private key authentication
    PrivateKey {
        /// Private key file path
        private_key_path: String,
        /// Private key passphrase (if the key file is encrypted)
        #[serde(skip_serializing)]
        passphrase: Option<String>,
    },
}

/// Host key verification policy.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum HostKeyPolicy {
    /// Strict mode: host key must be found in known_hosts (default).
    #[default]
    Strict,
    /// Accept new hosts: automatically trust and add unknown host keys.
    AcceptNew,
    /// Disable verification: accept any host key (not recommended, testing only).
    Disable,
}

/// SSH connection configuration.
///
/// This is the fundamental connection parameter type for this library, suitable for all SSH operations.
///
/// Common usage:
///
/// ```ignore
/// use sshr::ssh::{SshConfig, AuthMethod, HostKeyPolicy};
///
/// // Password authentication
/// let cfg = SshConfig {
///     host: "127.0.0.1".to_string(),
///     port: 22,
///     user: "root".to_string(),
///     auth: AuthMethod::Password { password: "secret".to_string() },
///     timeout_ms: 8_000,
///     host_key_policy: HostKeyPolicy::Strict,
///     known_hosts_path: None,
/// };
/// ```
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SshConfig {
    /// Target server address or hostname.
    pub host: String,
    /// SSH port, typically `22`.
    pub port: u16,
    /// Login username.
    pub user: String,
    /// Authentication method (password or private key).
    pub auth: AuthMethod,
    /// Timeout for connection and command execution, in milliseconds.
    pub timeout_ms: u64,
    /// Host key verification policy, defaults to `Strict`.
    #[serde(default)]
    pub host_key_policy: HostKeyPolicy,
    /// Custom known_hosts file path.
    /// Uses `~/.ssh/known_hosts` when `None`.
    #[serde(default)]
    pub known_hosts_path: Option<String>,
}

impl SshConfig {
    /// Create a config with minimal parameters, using sensible defaults for the rest.
    ///
    /// - `port`: 22
    /// - `timeout_ms`: 8000
    /// - `host_key_policy`: Strict
    /// - `known_hosts_path`: None
    pub fn new(host: impl Into<String>, user: impl Into<String>, auth: AuthMethod) -> Self {
        Self {
            host: host.into(),
            port: 22,
            user: user.into(),
            auth,
            timeout_ms: 8_000,
            host_key_policy: HostKeyPolicy::Strict,
            known_hosts_path: None,
        }
    }
}

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

    // ===== AuthMethod tests =====

    #[test]
    fn auth_method_password_serialization() {
        let auth = AuthMethod::Password {
            password: "secret".to_string(),
        };
        let json = serde_json::to_string(&auth).unwrap();
        // password should be skipped by skip_serializing
        assert!(
            !json.contains("secret"),
            "password should not appear in serialization output: {}",
            json
        );
        assert!(json.contains("password"));
    }

    #[test]
    fn auth_method_private_key_serialization() {
        let auth = AuthMethod::PrivateKey {
            private_key_path: "/root/.ssh/id_rsa".to_string(),
            passphrase: Some("mypass".to_string()),
        };
        let json = serde_json::to_string(&auth).unwrap();
        assert!(
            !json.contains("mypass"),
            "passphrase should not appear in serialization output: {}",
            json
        );
        assert!(json.contains("id_rsa"));
    }

    // ===== HostKeyPolicy tests =====

    #[test]
    fn host_key_policy_default_is_strict() {
        let policy = HostKeyPolicy::default();
        assert!(matches!(policy, HostKeyPolicy::Strict));
    }

    #[test]
    fn host_key_policy_serialization() {
        assert_eq!(
            serde_json::to_string(&HostKeyPolicy::Strict).unwrap(),
            r#"{"type":"strict"}"#
        );
        assert_eq!(
            serde_json::to_string(&HostKeyPolicy::AcceptNew).unwrap(),
            r#"{"type":"accept_new"}"#
        );
        assert_eq!(
            serde_json::to_string(&HostKeyPolicy::Disable).unwrap(),
            r#"{"type":"disable"}"#
        );
    }

    #[test]
    fn host_key_policy_deserialization() {
        let policy: HostKeyPolicy = serde_json::from_str(r#"{"type":"accept_new"}"#).unwrap();
        assert!(matches!(policy, HostKeyPolicy::AcceptNew));
    }

    // ===== SshConfig tests =====

    #[test]
    fn ssh_config_serialization_roundtrip() {
        let cfg = SshConfig {
            host: "example.com".to_string(),
            port: 2222,
            user: "admin".to_string(),
            auth: AuthMethod::Password {
                password: "secret".to_string(),
            },
            timeout_ms: 5000,
            host_key_policy: HostKeyPolicy::AcceptNew,
            known_hosts_path: Some("/custom/known_hosts".to_string()),
        };
        let json = serde_json::to_string(&cfg).unwrap();
        assert!(
            !json.contains("secret"),
            "password should not appear in JSON"
        );

        // Deserialization requires the password field (skip_serializing only affects serialization)
        let mut json_val: serde_json::Value = serde_json::from_str(&json).unwrap();
        json_val["auth"]["password"] = serde_json::Value::String("secret".to_string());
        let json_with_pass = serde_json::to_string(&json_val).unwrap();

        let deserialized: SshConfig = serde_json::from_str(&json_with_pass).unwrap();
        assert_eq!(deserialized.host, "example.com");
        assert_eq!(deserialized.port, 2222);
        assert_eq!(deserialized.user, "admin");
        assert_eq!(deserialized.timeout_ms, 5000);
        assert!(matches!(
            deserialized.host_key_policy,
            HostKeyPolicy::AcceptNew
        ));
        assert_eq!(
            deserialized.known_hosts_path,
            Some("/custom/known_hosts".to_string())
        );
    }

    #[test]
    fn ssh_config_deserialize_missing_optional_fields() {
        let json = r#"{"host":"h","port":22,"user":"u","auth":{"type":"password","password":"p"},"timeout_ms":1000}"#;
        let cfg: SshConfig = serde_json::from_str(json).unwrap();
        assert!(
            matches!(cfg.host_key_policy, HostKeyPolicy::Strict),
            "should default to Strict when missing"
        );
        assert_eq!(
            cfg.known_hosts_path, None,
            "should default to None when missing"
        );
    }

    #[test]
    fn ssh_config_clone() {
        let cfg = SshConfig {
            host: "h".to_string(),
            port: 22,
            user: "u".to_string(),
            auth: AuthMethod::Password {
                password: "p".to_string(),
            },
            timeout_ms: 1000,
            host_key_policy: Default::default(),
            known_hosts_path: None,
        };
        let cloned = cfg.clone();
        assert_eq!(cfg.host, cloned.host);
        assert_eq!(cfg.port, cloned.port);
    }
}
