duet 0.8.6

bi-directional synchronization
use std::path::PathBuf;

use color_eyre::eyre::Result;

use crate::profile::ProfileSource;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncOptions {
    pub interactive: bool,
    pub yes: bool,
    pub dry_run: bool,
    pub batch: bool,
    pub force: bool,
    pub verbose: bool,
    pub debug_info: bool,
    pub profile_performance: bool,
    pub profile_performance_json: Option<PathBuf>,
}

#[derive(Debug, PartialEq, Eq)]
pub enum Command {
    Help,
    Version,
    License,
    Server,
    Snapshot {
        profile: String,
        statefile: Option<PathBuf>,
    },
    Inspect {
        statefile: PathBuf,
    },
    Changes {
        profile: String,
        statefile: Option<PathBuf>,
    },
    Info {
        profile: String,
    },
    Walk {
        path: PathBuf,
    },
    Recover {
        statefile: PathBuf,
    },
    Sync {
        profile: ProfileSource,
        path: Option<PathBuf>,
        options: SyncOptions,
    },
}

pub fn parse_from_env() -> Result<Command> {
    parse(pico_args::Arguments::from_env())
}

fn parse(mut pargs: pico_args::Arguments) -> Result<Command> {
    if pargs.contains(["-h", "--help"]) {
        return Ok(Command::Help);
    }

    if pargs.contains("--version") {
        return Ok(Command::Version);
    }

    if pargs.contains("--license") {
        return Ok(Command::License);
    }

    if pargs.contains("--server") {
        return Ok(Command::Server);
    }

    let profile_file = pargs.opt_value_from_os_str("--profile-file", parse_path)?;
    let profile_performance_json =
        pargs.opt_value_from_os_str("--profile-performance-json", parse_path)?;

    let options = SyncOptions {
        interactive: pargs.contains(["-i", "--interactive"]),
        yes: pargs.contains(["-y", "--yes"]),
        dry_run: pargs.contains(["-n", "--dry-run"]),
        batch: pargs.contains(["-b", "--batch"]),
        force: pargs.contains(["-f", "--force"]),
        verbose: pargs.contains(["-v", "--verbose"]),
        debug_info: pargs.contains("--debug-info"),
        profile_performance: pargs.contains("--profile-performance"),
        profile_performance_json,
    };

    if let Some(profile_file) = profile_file {
        return Ok(Command::Sync {
            profile: ProfileSource::File(profile_file),
            path: pargs.opt_free_from_os_str(parse_path)?,
            options,
        });
    }

    let profile = match pargs.free_from_str::<String>() {
        Ok(profile) => profile,
        Err(_) => return Ok(Command::Help),
    };

    match profile.as_str() {
        "_snapshot" => Ok(Command::Snapshot {
            profile: pargs.free_from_str()?,
            statefile: pargs.opt_free_from_os_str(parse_path)?,
        }),
        "_inspect" => Ok(Command::Inspect {
            statefile: pargs.free_from_os_str(parse_path)?,
        }),
        "_changes" => Ok(Command::Changes {
            profile: pargs.free_from_str()?,
            statefile: pargs.opt_free_from_os_str(parse_path)?,
        }),
        "_info" => Ok(Command::Info {
            profile: pargs.free_from_str()?,
        }),
        "_walk" => Ok(Command::Walk {
            path: pargs.free_from_os_str(parse_path)?,
        }),
        "_recover" => Ok(Command::Recover {
            statefile: pargs.free_from_os_str(parse_path)?,
        }),
        _ => Ok(Command::Sync {
            profile: ProfileSource::Named(profile),
            path: pargs.opt_free_from_os_str(parse_path)?,
            options,
        }),
    }
}

fn parse_path(s: &std::ffi::OsStr) -> Result<PathBuf, &'static str> {
    Ok(s.into())
}

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

    fn parse_args(args: &[&str]) -> Command {
        parse(pico_args::Arguments::from_vec(
            args.iter().map(OsString::from).collect(),
        ))
        .unwrap()
    }

    #[test]
    fn parses_global_commands() {
        assert_eq!(parse_args(&["--help"]), Command::Help);
        assert_eq!(parse_args(&["--version"]), Command::Version);
        assert_eq!(parse_args(&["--license"]), Command::License);
        assert_eq!(parse_args(&["--server"]), Command::Server);
    }

    #[test]
    fn parses_sync_command_with_options() {
        assert_eq!(
            parse_args(&[
                "--interactive",
                "-y",
                "-n",
                "-b",
                "-f",
                "-v",
                "work",
                "docs",
            ]),
            Command::Sync {
                profile: ProfileSource::Named("work".to_string()),
                path: Some(PathBuf::from("docs")),
                options: SyncOptions {
                    interactive: true,
                    yes: true,
                    dry_run: true,
                    batch: true,
                    force: true,
                    verbose: true,
                    debug_info: false,
                    profile_performance: false,
                    profile_performance_json: None,
                },
            }
        );
    }

    #[test]
    fn parses_sync_command_with_debug_info() {
        assert_eq!(
            parse_args(&["--debug-info", "work"]),
            Command::Sync {
                profile: ProfileSource::Named("work".to_string()),
                path: None,
                options: SyncOptions {
                    interactive: false,
                    yes: false,
                    dry_run: false,
                    batch: false,
                    force: false,
                    verbose: false,
                    debug_info: true,
                    profile_performance: false,
                    profile_performance_json: None,
                },
            }
        );
    }

    #[test]
    fn parses_sync_command_with_performance_profile() {
        assert_eq!(
            parse_args(&[
                "--profile-performance",
                "--profile-performance-json",
                "profile.json",
                "work",
            ]),
            Command::Sync {
                profile: ProfileSource::Named("work".to_string()),
                path: None,
                options: SyncOptions {
                    interactive: false,
                    yes: false,
                    dry_run: false,
                    batch: false,
                    force: false,
                    verbose: false,
                    debug_info: false,
                    profile_performance: true,
                    profile_performance_json: Some(PathBuf::from("profile.json")),
                },
            }
        );
    }

    #[test]
    fn parses_sync_command_with_profile_file() {
        assert_eq!(
            parse_args(&["--profile-file", "profile.prf", "docs"]),
            Command::Sync {
                profile: ProfileSource::File(PathBuf::from("profile.prf")),
                path: Some(PathBuf::from("docs")),
                options: SyncOptions {
                    interactive: false,
                    yes: false,
                    dry_run: false,
                    batch: false,
                    force: false,
                    verbose: false,
                    debug_info: false,
                    profile_performance: false,
                    profile_performance_json: None,
                },
            }
        );
    }

    #[test]
    fn parses_hidden_commands() {
        assert_eq!(
            parse_args(&["_snapshot", "work", "state.bin"]),
            Command::Snapshot {
                profile: "work".to_string(),
                statefile: Some(PathBuf::from("state.bin")),
            }
        );
        assert_eq!(
            parse_args(&["_inspect", "state.bin"]),
            Command::Inspect {
                statefile: PathBuf::from("state.bin"),
            }
        );
        assert_eq!(
            parse_args(&["_changes", "work", "state.bin"]),
            Command::Changes {
                profile: "work".to_string(),
                statefile: Some(PathBuf::from("state.bin")),
            }
        );
        assert_eq!(
            parse_args(&["_info", "work"]),
            Command::Info {
                profile: "work".to_string(),
            }
        );
        assert_eq!(
            parse_args(&["_walk", "docs"]),
            Command::Walk {
                path: PathBuf::from("docs"),
            }
        );
        assert_eq!(
            parse_args(&["_recover", "state.bin"]),
            Command::Recover {
                statefile: PathBuf::from("state.bin"),
            }
        );
    }
}