mgt 0.0.2

Command line tool to analyze the WildFly management model.
//! Analysis source abstraction: either a WildFly version or a feature pack.

use crate::feature_pack::FeaturePack;
use wildfly_container_versions::WildFlyContainer;

/// An analysis source: either a WildFly version or a third-party feature pack.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Source {
    WildFly(WildFlyContainer),
    FeaturePack(FeaturePack),
}

impl Source {
    /// Parses a single identifier, trying feature pack first, then WildFly version.
    pub fn parse(input: &str) -> anyhow::Result<Source> {
        if let Ok(fp) = FeaturePack::parse(input) {
            return Ok(Source::FeaturePack(fp));
        }
        WildFlyContainer::version(input)
            .map(Source::WildFly)
            .map_err(|e| anyhow::anyhow!("{}", e))
    }

    /// Parses a comma-separated list or range (e.g. `26,28,34` or `20..29`).
    pub fn parse_list(input: &str) -> anyhow::Result<Vec<Source>> {
        if input.contains("..") {
            let containers =
                WildFlyContainer::enumeration(input).map_err(|e| anyhow::anyhow!("{}", e))?;
            return Ok(containers.into_iter().map(Source::WildFly).collect());
        }

        let mut sources = Vec::new();
        for part in input.split(',') {
            let part = part.trim();
            if !part.is_empty() {
                sources.push(Source::parse(part)?);
            }
        }
        Ok(sources)
    }

    /// Returns a human-readable name (e.g. `34.0` or `ai 0.9.0`).
    pub fn display_name(&self) -> String {
        match self {
            Source::WildFly(wc) => wc.display_version(),
            Source::FeaturePack(fp) => fp.display_name(),
        }
    }

    /// Returns the numeric port offset used for bolt/http port calculation.
    pub fn port_offset(&self) -> u16 {
        match self {
            Source::WildFly(wc) => wc.identifier,
            Source::FeaturePack(fp) => fp.port_offset(),
        }
    }

    /// Returns the identifier used in container and volume names (e.g. `340` or `ai-0.9.0`).
    pub fn container_id(&self) -> String {
        match self {
            Source::WildFly(wc) => wc.identifier.to_string(),
            Source::FeaturePack(fp) => fp.container_id(),
        }
    }

    /// Returns a human-readable label for the welcome page (e.g. `"WildFly 34.0"` or `"AI Feature Pack 0.9.0"`).
    pub fn welcome_label(&self) -> String {
        match self {
            Source::WildFly(wc) => format!("WildFly {}", wc.display_version()),
            Source::FeaturePack(fp) => {
                format!("{} Feature Pack {}", fp.name, fp.version)
            }
        }
    }

    /// Returns the source type label value (`"wildfly"` or `"feature-pack"`).
    pub fn source_type(&self) -> &'static str {
        match self {
            Source::WildFly(_) => "wildfly",
            Source::FeaturePack(_) => "feature-pack",
        }
    }

    /// Returns a parseable source name (e.g. `"34.0"` or `"ai:0.9.0"`).
    pub fn source_name(&self) -> String {
        match self {
            Source::WildFly(wc) => wc.display_version(),
            Source::FeaturePack(fp) => format!("{}:{}", fp.shortcut, fp.version),
        }
    }
}

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

    #[test]
    fn parse_wildfly_version() {
        let source = Source::parse("34").unwrap();
        assert!(matches!(source, Source::WildFly(_)));
        assert_eq!(source.display_name(), "34.0");
    }

    #[test]
    fn parse_wildfly_version_with_minor() {
        let source = Source::parse("26.1").unwrap();
        assert!(matches!(source, Source::WildFly(_)));
        assert_eq!(source.display_name(), "26.1");
    }

    #[test]
    fn parse_feature_pack() {
        let source = Source::parse("ai").unwrap();
        assert!(matches!(source, Source::FeaturePack(_)));
        assert_eq!(source.display_name(), "ai 0.9.0");
    }

    #[test]
    fn parse_versioned_feature_pack() {
        let source = Source::parse("ai:0.9.0").unwrap();
        assert!(matches!(source, Source::FeaturePack(_)));
        assert_eq!(source.display_name(), "ai 0.9.0");
    }

    #[test]
    fn parse_invalid_input() {
        assert!(Source::parse("invalid").is_err());
    }

    #[test]
    fn parse_list_wildfly_only() {
        let sources = Source::parse_list("26,28,34").unwrap();
        assert_eq!(sources.len(), 3);
        assert!(sources.iter().all(|s| matches!(s, Source::WildFly(_))));
    }

    #[test]
    fn parse_list_feature_packs_only() {
        let sources = Source::parse_list("ai,grpc").unwrap();
        assert_eq!(sources.len(), 2);
        assert!(sources.iter().all(|s| matches!(s, Source::FeaturePack(_))));
    }

    #[test]
    fn parse_list_mixed() {
        let sources = Source::parse_list("34,ai,26.1").unwrap();
        assert_eq!(sources.len(), 3);
        assert!(matches!(sources[0], Source::WildFly(_)));
        assert!(matches!(sources[1], Source::FeaturePack(_)));
        assert!(matches!(sources[2], Source::WildFly(_)));
    }

    #[test]
    fn parse_list_range() {
        let sources = Source::parse_list("26..29").unwrap();
        assert!(sources.len() >= 4);
        assert!(sources.iter().all(|s| matches!(s, Source::WildFly(_))));
    }

    #[test]
    fn port_offset_wildfly() {
        let source = Source::parse("34").unwrap();
        assert_eq!(source.port_offset(), 340);
    }

    #[test]
    fn port_offset_feature_pack() {
        let source = Source::parse("ai").unwrap();
        assert_eq!(source.port_offset(), 10_000);
    }

    #[test]
    fn port_offsets_no_overlap() {
        let wf_max = 990u16; // WildFly 99.0 = 99*10+0 = 990
        let fp_min = 10_000u16;
        assert!(
            wf_max < fp_min,
            "WildFly and feature pack port ranges overlap"
        );
    }

    #[test]
    fn container_id_wildfly() {
        let source = Source::parse("34").unwrap();
        assert_eq!(source.container_id(), "340");
    }

    #[test]
    fn container_id_feature_pack() {
        let source = Source::parse("ai").unwrap();
        assert_eq!(source.container_id(), "ai-0-9-0");
    }

    #[test]
    fn source_type_wildfly() {
        let source = Source::parse("34").unwrap();
        assert_eq!(source.source_type(), "wildfly");
    }

    #[test]
    fn source_type_feature_pack() {
        let source = Source::parse("ai").unwrap();
        assert_eq!(source.source_type(), "feature-pack");
    }

    #[test]
    fn source_name_wildfly() {
        let source = Source::parse("34").unwrap();
        assert_eq!(source.source_name(), "34.0");
    }

    #[test]
    fn source_name_feature_pack() {
        let source = Source::parse("ai").unwrap();
        assert_eq!(source.source_name(), "ai:0.9.0");
    }

    #[test]
    fn source_name_roundtrips() {
        for input in &["34", "26.1", "ai", "grpc"] {
            let source = Source::parse(input).unwrap();
            let reparsed = Source::parse(&source.source_name()).unwrap();
            assert_eq!(source, reparsed);
        }
    }
}