use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
use tokio::process::{Child, ChildStderr, ChildStdout};
use tokio::sync::Mutex as AsyncMutex;

pub async fn collect_process_output<F>(
    child: Arc<AsyncMutex<Child>>,
    stdout: ChildStdout,
    stderr: ChildStderr,
    mut on_stdout_line: F,
) -> Result<(String, String, std::process::ExitStatus), String>
where
    F: FnMut(&str) -> Result<(), String>,
{
    let stderr_task = tokio::spawn(async move {
        let mut stderr_reader = BufReader::new(stderr);
        let mut stderr_output = String::new();
        stderr_reader
            .read_to_string(&mut stderr_output)
            .await
            .map_err(|e| format!("Failed to read stderr: {}", e))?;
        Ok::<String, String>(stderr_output)
    });

    let mut reader = BufReader::new(stdout).lines();
    let mut output = String::new();
    while let Ok(Some(line)) = reader.next_line().await {
        on_stdout_line(&line)?;
        output.push_str(&line);
        output.push('\n');
    }

    let status = {
        let mut child_guard = child.lock().await;
        child_guard
            .wait()
            .await
            .map_err(|e| format!("Failed to wait for process: {}", e))?
    };

    let stderr_output = stderr_task
        .await
        .map_err(|e| format!("Failed to join stderr reader: {}", e))??;

    Ok((output, stderr_output, status))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::process::Stdio;
    use tokio::process::Command;

    #[tokio::test]
    async fn test_collect_process_output_reads_stdout_and_stderr() {
        let mut child = Command::new("sh")
            .args([
                "-c",
                "printf 'out-1\\n'; printf 'err-1\\n' >&2; printf 'out-2\\n'; printf 'err-2\\n' >&2",
            ])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .expect("process should spawn");

        let stdout = child.stdout.take().expect("stdout must be captured");
        let stderr = child.stderr.take().expect("stderr must be captured");
        let child = Arc::new(AsyncMutex::new(child));

        let mut lines = Vec::new();
        let (stdout_output, stderr_output, status) =
            collect_process_output(child, stdout, stderr, |line| {
                lines.push(line.to_string());
                Ok(())
            })
            .await
            .expect("collection should succeed");

        assert!(status.success());
        assert_eq!(lines, vec!["out-1".to_string(), "out-2".to_string()]);
        assert_eq!(stdout_output, "out-1\nout-2\n");
        assert_eq!(stderr_output, "err-1\nerr-2\n");
    }
}
