//! Sub-command to repeatedly run test suite for the specified
//! project.

use std::{
    fs::{create_dir, remove_dir_all, write, OpenOptions},
    io::Write,
    path::{Path, PathBuf},
    time::SystemTime,
};

use clap::Parser;
use log::{debug, info, trace};
use tempfile::tempdir;
use walkdir::WalkDir;

use radicle::git::Oid;

use crate::{
    report::Report, runlog::RunLog, spec::Spec, timestamp, Args, FAILURE, STATS_TXT, SUCCESS,
};

/// Run tests for the project specified in the SPEC file.
#[derive(Debug, Parser)]
pub struct Run {
    /// How many repetitions? Default is no limit.
    #[clap(long)]
    repeats: Option<usize>,

    /// Directory where to put HTML log files and report.
    #[clap(long)]
    logs: PathBuf,

    /// Maximum time a run of the test suite may take.
    #[clap(long, default_value = "600")]
    timeout: usize,

    /// Check if the test suite leaves temporary files behind.
    #[clap(long)]
    check_tempdir: bool,

    spec: PathBuf,
}

impl Run {
    /// Execute the `run` sub-command.
    pub fn run(&self, args: &Args) -> anyhow::Result<()> {
        let spec = Spec::from_file(&self.spec)?;

        debug!("{args:#?}");
        debug!("{spec:#?}");

        assert!(self.logs.exists());
        let stats_txt = self.logs.join(STATS_TXT);

        let tmp = tempdir()?;
        let working_dir = tmp.path().join("srcdir");

        let report_html = self.logs.join("counts.html");
        let mut i = 0;
        loop {
            i += 1;
            if let Some(repeats) = &self.repeats {
                if i > *repeats {
                    break;
                }
            }
            info!("repetition {i}");

            let run_id = timestamp(&SystemTime::now());
            info!("run {run_id}");

            let mut run_log = RunLog::default();
            run_log.url(&spec.repository_url);
            run_log.git_ref(&spec.git_ref);

            spec.versions(&mut run_log)?;

            if working_dir.exists() {
                spec.git_remote_update(&working_dir, &mut run_log)?;
                debug!("git pulled");
            } else {
                spec.git_clone(&working_dir, &mut run_log)?;
                debug!("git cloned");
            }
            spec.git_checkout(&working_dir, &spec.git_ref, &mut run_log)?;
            debug!("git checked out");

            let commit = spec.git_head(&working_dir, &mut run_log)?;
            if let Ok(oid) = Oid::try_from(commit.as_str()) {
                run_log.git_commit(oid);
            }

            let test_tmpdir = tmp.path().join("tmp");
            create_dir(&test_tmpdir)?;
            debug!("created temporary directory {}", test_tmpdir.display());

            let (mut log, mut success) =
                spec.run_test_suite(&working_dir, self.timeout, &test_tmpdir, &mut run_log)?;
            info!("ran test suite: {success}");

            if self.check_tempdir && !dir_is_empty(&test_tmpdir) {
                log.push_str("\n\n\n# Temporary files left behind\n");
                success = false;
                info!("test failed to clean up its temporary files: test failed");
            }

            remove_dir_all(&test_tmpdir)?;

            if success {
                record(&stats_txt, &commit, SUCCESS)?;
            } else {
                record(&stats_txt, &commit, FAILURE)?;
            }
            assert!(stats_txt.exists());

            let log_subdir = commit_dir(&commit);
            let log_dir = self.logs.join(&log_subdir);
            if !log_dir.exists() {
                create_dir(&log_dir)?;
            }
            let log_filename = log_dir.join(format!(
                "log-{run_id}.{i}.{}.html",
                if success { "success" } else { "fail" }
            ));
            write(&log_filename, run_log.as_html().to_string())?;

            let report = Report::new(&spec.description, &stats_txt)?;
            write(
                &report_html,
                report.as_html(&spec, &working_dir).to_string(),
            )?;
        }

        Ok(())
    }
}

/// Return sub-directory for log files for a commit.
pub fn commit_dir(commit: &str) -> String {
    format!("log-{commit}")
}

fn dir_is_empty(dirname: &Path) -> bool {
    WalkDir::new(dirname)
        .min_depth(1)
        .into_iter()
        .next()
        .is_none()
}

fn record(filename: &Path, commit: &str, result: &str) -> anyhow::Result<()> {
    trace!("writing {result} on {commit} to {}", filename.display());
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(filename)?;
    file.write_all(format!("{commit} {result}\n").as_bytes())?;
    Ok(())
}
