use std::{ops::Not, process::{Command, ExitStatus, Output}};

use anyhow::{Result, bail, Context};


/// Helper trait implemented on [Command] which allows running commands
/// with an error returned if a nonzero exit status is received.
pub trait CommandExitOk {
    /// Run the command like [`Command::output`] does, but return an error
    /// if a nonzero exit status is received.
    ///
    /// This method is modelled to be similar to [`Output::exit_ok`],
    /// but includes the command that completed into the error message.
    fn output_exit_ok(&mut self) -> Result<Output>;

    /// Run the command like [`Command::status`] does, but return an error
    /// if a nonzero exit status is received.
    ///
    /// This method is modelled to be similar to [`ExitStatus::exit_ok`],
    /// but includes the command that completed into the error message.
    fn status_exit_ok(&mut self) -> Result<ExitStatus>;
}
impl CommandExitOk for Command {
    fn output_exit_ok(&mut self) -> Result<Output> {
        let command_string = format_command(self);

        let output = self.output()
            .context(format!("error while running command `{command_string}`"))?;

        let formatted_output = format_command_output(&command_string, &output)?;

        if output.status.success().not() {
            bail!("Error while running command.\n{formatted_output}")
        }
        Ok(output)
    }

    fn status_exit_ok(&mut self) -> Result<ExitStatus> {
        let command_string = format_command(self);

        let status = self.status()
            .context(format!("error while running command `{command_string}`"))?;

        if status.success().not() {
            let formatted_status = format_command_status(&command_string, &status);
            bail!("Error while running command.\n{formatted_status}")
        }
        Ok(status)
    }
}


fn format_command(command: &Command) -> String {
    std::iter::once(command.get_program())
        .chain(command.get_args())
        .map(|arg| arg.to_string_lossy())
        .map(|arg| if arg.contains(' ') {
                format!("\"{arg}\"")
            } else {
                arg.to_string()
            }
        )
        .collect::<Vec<_>>()
        .join(" ")
}
fn format_command_status(command: &str, status: &ExitStatus) -> String {

    let mut message = format!("  Command: {command}");
    if let Some(status) = &status.code() {
        message += &format!("\n  Status Code: {status}");
    }
    message
}
fn format_command_output(command: &str, output: &Output) -> Result<String> {
    let mut message = format_command_status(command, &output.status);

    if output.stdout.is_empty().not() {
        message += &format!("\n  Stdout:\n{}", String::from_utf8(output.stdout.clone())?);
    }
    if output.stderr.is_empty().not() {
        message += &format!("\n  Stderr:\n{}", String::from_utf8(output.stderr.clone())?);
    }
    Ok(message)
}


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


    #[test]
    fn happy_flow() {
        let result = Command::new("true")
            .output_exit_ok();

        assert!(result.is_ok());
    }

    #[test]
    fn error() {
        let result = Command::new("false")
            .output_exit_ok();

        assert!(result.is_err());
    }

    #[test]
    fn should_format_command() {
        let mut command = Command::new("true");
        command.arg("--help");
        let result = format_command(&command);
        assert_eq!(result, "true --help");

        let mut command = Command::new("true");
        command.arg("hello world");
        let result = format_command(&command);
        assert_eq!(result, "true \"hello world\"");

        let command = Command::new("true false");
        let result = format_command(&command);
        assert_eq!(result, "\"true false\"");
    }
}
