//! Pure validation logic for project settings — no side effects.
//!
//! All functions in this module are pure: given the same input, they always
//! return the same output, and they never perform any I/O.

use super::{Error, project::ProjectSettings};

/// Validate the incoming [`ProjectSettings`] before writing to disk.
///
/// # Errors
/// - [`Error::Validation`] if any field value is invalid.
pub fn validate_project_settings(s: &ProjectSettings) -> Result<(), Error> {
    match s.default_qa.as_str() {
        "server" | "agent" | "human" => {}
        other => {
            return Err(Error::Validation(format!(
                "Invalid default_qa value '{other}'. Must be one of: server, agent, human"
            )));
        }
    }
    Ok(())
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::ProjectConfig;
    use crate::service::settings::project::settings_from_config;

    fn make_settings(default_qa: &str) -> ProjectSettings {
        let cfg = ProjectConfig {
            default_qa: default_qa.to_string(),
            ..Default::default()
        };
        settings_from_config(&cfg)
    }

    // ── Valid cases ───────────────────────────────────────────────────────────

    #[test]
    fn accepts_server_qa_mode() {
        assert!(validate_project_settings(&make_settings("server")).is_ok());
    }

    #[test]
    fn accepts_agent_qa_mode() {
        assert!(validate_project_settings(&make_settings("agent")).is_ok());
    }

    #[test]
    fn accepts_human_qa_mode() {
        assert!(validate_project_settings(&make_settings("human")).is_ok());
    }

    #[test]
    fn accepts_all_qa_modes() {
        for mode in &["server", "agent", "human"] {
            let result = validate_project_settings(&make_settings(mode));
            assert!(result.is_ok(), "qa mode '{mode}' should be valid");
        }
    }

    // ── Invalid cases ─────────────────────────────────────────────────────────

    #[test]
    fn rejects_empty_qa_mode() {
        let err = validate_project_settings(&make_settings("")).unwrap_err();
        assert!(matches!(err, Error::Validation(_)));
    }

    #[test]
    fn rejects_unknown_qa_mode() {
        let err = validate_project_settings(&make_settings("robot")).unwrap_err();
        assert!(matches!(err, Error::Validation(ref msg) if msg.contains("robot")));
    }

    #[test]
    fn rejects_uppercase_qa_mode() {
        let err = validate_project_settings(&make_settings("Server")).unwrap_err();
        assert!(matches!(err, Error::Validation(_)));
    }

    #[test]
    fn rejects_partial_qa_mode() {
        let err = validate_project_settings(&make_settings("serv")).unwrap_err();
        assert!(matches!(err, Error::Validation(_)));
    }

    #[test]
    fn rejects_qa_mode_with_trailing_space() {
        let err = validate_project_settings(&make_settings("server ")).unwrap_err();
        assert!(matches!(err, Error::Validation(_)));
    }

    #[test]
    fn error_message_contains_invalid_value() {
        let err = validate_project_settings(&make_settings("bad_mode")).unwrap_err();
        if let Error::Validation(msg) = err {
            assert!(
                msg.contains("bad_mode"),
                "error message should include the bad value"
            );
            assert!(
                msg.contains("server") && msg.contains("agent") && msg.contains("human"),
                "error message should list valid values"
            );
        } else {
            panic!("expected ValidationError");
        }
    }

    // ── Settings with other fields set ───────────────────────────────────────

    #[test]
    fn valid_settings_with_all_optional_fields_set() {
        let s = ProjectSettings {
            default_qa: "agent".to_string(),
            default_coder_model: Some(crate::agents::AgentModel::Opus),
            max_coders: Some(4),
            max_retries: 5,
            base_branch: Some("main".to_string()),
            rate_limit_notifications: false,
            timezone: Some("UTC".to_string()),
            rendezvous: Some("ws://host:3001/crdt-sync".to_string()),
            watcher_sweep_interval_secs: 30,
            watcher_done_retention_secs: 3600,
        };
        assert!(validate_project_settings(&s).is_ok());
    }

    #[test]
    fn valid_settings_with_no_optional_fields() {
        let s = ProjectSettings {
            default_qa: "human".to_string(),
            default_coder_model: None,
            max_coders: None,
            max_retries: 2,
            base_branch: None,
            rate_limit_notifications: true,
            timezone: None,
            rendezvous: None,
            watcher_sweep_interval_secs: 60,
            watcher_done_retention_secs: 14400,
        };
        assert!(validate_project_settings(&s).is_ok());
    }
}
