mod cli;
mod git;
mod mass;
mod remotes;
#[cfg(test)]
mod testing;

use std::env;

use anyhow::Result;
use clap::Parser;
use cli::{Args, Commands, RemoteArgs};
use console::style;
use tokio::fs;

pub async fn run() -> Result<()> {
    let args = Args::parse();
    run_with_args(args).await
}

async fn run_with_args(args: Args) -> Result<()> {
    let dir = args.dir.unwrap_or_else(|| env::current_dir().unwrap());

    eprintln!(
        "{}",
        style(format!("📁 Working directory: {}", dir.display())).dim()
    );

    match args.command {
        Commands::Clone { file, remote } => {
            let repos = resolve_repositories(file, remote).await?;

            mass::clone(&dir, repos).await?;
        }
        Commands::Repos { remote } => {
            let repos = resolve_repositories(None, remote).await?;
            for repo in repos {
                println!("{repo}");
            }
        }
        Commands::Status { dirty } => {
            mass::status(&dir, dirty).await?;
        }
        Commands::CheckoutMain => {
            eprintln!(
                "{}",
                style("➜ Checking out default branch (main/master)").cyan()
            );
            mass::checkout_default(&dir).await?;
        }
        Commands::Exec { command } => {
            if !command.is_empty() {
                eprintln!(
                    "{}",
                    style(format!("➜ Executing: {}", command.join(" "))).cyan()
                );
            }
            mass::exec(&dir, &command).await?;
        }
        Commands::External(command) => {
            let command = std::iter::once("git".to_string())
                .chain(command)
                .collect::<Vec<_>>();
            eprintln!(
                "{}",
                style(format!("➜ Executing: {}", command.join(" "))).cyan()
            );
            mass::exec(&dir, &command).await?;
        }
        Commands::NoGit => {
            mass::nogit(&dir).await?;
        }
    }

    Ok(())
}

async fn resolve_repositories(
    file: Option<std::path::PathBuf>,
    remote: RemoteArgs,
) -> Result<remotes::Repositories> {
    let remote_spec = remote.into_spec()?;

    match (remote_spec, file) {
        (Some(spec), _) => {
            show_remote_auth_usage(&spec);
            remotes::repositories(spec).await
        }
        (_, Some(fp)) => Ok(fs::read_to_string(fp)
            .await?
            .lines()
            .map(String::from)
            .collect()),
        _ => Ok(Vec::new()),
    }
}

fn show_remote_auth_usage(spec: &remotes::RemoteSpec) {
    if let Some(env_var) = spec.auth_env_var_if_present() {
        eprintln!(
            "{}",
            style(format!(
                "🔐 Using {env_var} from environment for remote discovery"
            ))
            .dim()
        );
    }
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};
    use std::process::Command;

    use tempfile::{NamedTempFile, tempdir};
    use tokio::fs;

    use super::{resolve_repositories, run_with_args, show_remote_auth_usage};
    use crate::cli::{Args, Commands, RemoteArgs};
    use crate::testing::{env_lock, remove_env_var, set_env_var};

    fn empty_remote_args() -> RemoteArgs {
        RemoteArgs {
            github: None,
            gitlab: None,
            forgejo: None,
        }
    }

    fn run_git(dir: &Path, args: &[&str]) {
        let output = Command::new("git")
            .args(args)
            .current_dir(dir)
            .output()
            .unwrap();
        assert!(output.status.success());
    }

    fn init_repo(dir: &Path) {
        run_git(dir, &["init", "-b", "main"]);
        std::fs::write(dir.join("README.md"), "madgit\n").unwrap();
        run_git(dir, &["add", "README.md"]);
        run_git(
            dir,
            &[
                "-c",
                "user.name=Test User",
                "-c",
                "user.email=test@example.com",
                "commit",
                "-m",
                "initial",
            ],
        );
    }

    fn init_parent_with_repo() -> (tempfile::TempDir, PathBuf) {
        let temp_dir = tempdir().unwrap();
        let repo_dir = temp_dir.path().join("repo-a");
        std::fs::create_dir(&repo_dir).unwrap();
        init_repo(&repo_dir);
        (temp_dir, repo_dir)
    }

    #[tokio::test]
    async fn resolve_repositories_reads_repositories_from_file() {
        let file = NamedTempFile::new().unwrap();
        fs::write(
            file.path(),
            "https://example.com/a.git\nhttps://example.com/b.git\n",
        )
        .await
        .unwrap();

        let repositories =
            resolve_repositories(Some(file.path().to_path_buf()), empty_remote_args())
                .await
                .unwrap();

        assert_eq!(
            repositories,
            vec![
                "https://example.com/a.git".to_string(),
                "https://example.com/b.git".to_string()
            ]
        );
    }

    #[tokio::test]
    async fn resolve_repositories_returns_empty_when_no_input_is_provided() {
        let repositories = resolve_repositories(None, empty_remote_args())
            .await
            .unwrap();

        assert!(repositories.is_empty());
    }

    #[tokio::test]
    async fn resolve_repositories_returns_error_for_invalid_remote_spec() {
        let error = resolve_repositories(
            None,
            RemoteArgs {
                github: None,
                gitlab: Some(" ".to_string()),
                forgejo: None,
            },
        )
        .await
        .unwrap_err();

        assert!(
            error
                .to_string()
                .contains("remote provider argument cannot be empty")
        );
    }

    #[tokio::test]
    async fn resolve_repositories_returns_error_for_missing_file() {
        let error = resolve_repositories(
            Some(PathBuf::from("/definitely/missing/repos.txt")),
            empty_remote_args(),
        )
        .await
        .unwrap_err();

        assert!(
            error.to_string().contains("No such file") || error.to_string().contains("os error")
        );
    }

    #[test]
    fn show_remote_auth_usage_runs_when_token_is_present() {
        let _guard = env_lock().lock().unwrap();
        set_env_var("GITHUB_TOKEN", "token");

        let spec = crate::remotes::RemoteSpec::github("wykwit").unwrap();
        show_remote_auth_usage(&spec);

        remove_env_var("GITHUB_TOKEN");
    }

    #[tokio::test]
    async fn run_with_args_handles_empty_exec_command() {
        let (temp_dir, _) = init_parent_with_repo();

        let error = run_with_args(Args {
            command: Commands::Exec {
                command: Vec::new(),
            },
            dir: Some(temp_dir.path().to_path_buf()),
        })
        .await
        .unwrap_err();

        assert_eq!(error.to_string(), "Some operations failed");
    }

    #[tokio::test]
    async fn run_with_args_handles_no_git_command() {
        let temp_dir = tempdir().unwrap();
        std::fs::create_dir(temp_dir.path().join("notes")).unwrap();

        run_with_args(Args {
            command: Commands::NoGit,
            dir: Some(temp_dir.path().to_path_buf()),
        })
        .await
        .unwrap();
    }
}
