use std::{env, fmt, process};

const VERSION: &str = env!("CARGO_PKG_VERSION");

fn main() {
    let args = env::args().skip(1).collect();

    match run(args) {
        Ok(output) => println!("{output}"),
        Err(error) => {
            eprintln!("{error}");
            process::exit(2);
        }
    }
}

fn run(args: Vec<String>) -> Result<String, CliError> {
    match command_from(&args)? {
        Command::Hello => Ok("hello from elios".to_owned()),
        Command::Version => Ok(format!("elios-cli {VERSION}")),
        Command::Add(left, right) => Ok(left.add(right).to_string()),
    }
}

fn command_from(args: &[String]) -> Result<Command, CliError> {
    match args {
        [] => Ok(Command::Hello),
        [command] if command == "hello" => Ok(Command::Hello),
        [command] if command == "--version" || command == "version" => Ok(Command::Version),
        [command, left, right] if command == "add" => {
            Ok(Command::Add(Number::parse(left)?, Number::parse(right)?))
        }
        _ => Err(CliError::Usage),
    }
}

enum Command {
    Hello,
    Version,
    Add(Number, Number),
}

#[derive(Copy, Clone, Eq, PartialEq)]
struct Number(i64);

impl Number {
    fn parse(input: &str) -> Result<Self, CliError> {
        let value = input
            .parse()
            .map_err(|_| CliError::BadNumber(input.to_owned()))?;

        Ok(Self(value))
    }

    fn add(self, other: Self) -> Sum {
        Sum(self.0 + other.0)
    }
}

struct Sum(i64);

impl fmt::Display for Sum {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

#[derive(Debug, Eq, PartialEq)]
enum CliError {
    Usage,
    BadNumber(String),
}

impl fmt::Display for CliError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Usage => write!(formatter, "usage: elios [hello|version|--version|add A B]"),
            Self::BadNumber(value) => write!(formatter, "invalid number: {value}"),
        }
    }
}

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

    #[test]
    fn default_command_says_hello() {
        assert_eq!(run(words(&[])).unwrap(), "hello from elios");
    }

    #[test]
    fn add_command_prints_sum() {
        assert_eq!(run(words(&["add", "2", "3"])).unwrap(), "5");
    }

    #[test]
    fn version_command_prints_package_version() {
        assert_eq!(run(words(&["--version"])).unwrap(), "elios-cli 0.0.1");
    }

    #[test]
    fn add_command_rejects_bad_number() {
        assert_eq!(
            run(words(&["add", "x", "3"])).unwrap_err(),
            CliError::BadNumber("x".to_owned())
        );
    }

    fn words(input: &[&str]) -> Vec<String> {
        input.iter().map(|word| word.to_string()).collect()
    }
}
