//! DockerComposeSupervisor tests. A fake `docker` script records the exact
//! arguments it was invoked with and returns a controllable exit code, so we
//! assert the compose invocations + state transitions with no real Docker
//! daemon — the same mock-the-external-binary approach the steamcmd tests use.
#![cfg(unix)]

use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use corrosion_host_agent::config::InstanceConfig;
use corrosion_host_agent::docker_compose::{DockerComposeConfig, DockerComposeSupervisor};
use corrosion_host_agent::supervisor::{InstanceState, Supervisor};

/// Write a fake `docker` executable that appends its args (space-joined) to
/// `args_log` and exits with the integer in `exit_file` (0 if absent).
fn fake_docker(dir: &Path, args_log: &Path, exit_file: &Path) -> PathBuf {
    let script = dir.join("fakedocker");
    let body = format!(
        "#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit \"$(cat '{}' 2>/dev/null || echo 0)\"\n",
        args_log.display(),
        exit_file.display(),
    );
    std::fs::write(&script, body).unwrap();
    let mut perms = std::fs::metadata(&script).unwrap().permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&script, perms).unwrap();
    script
}

fn dune_instance(command: Vec<String>, service: Option<String>) -> InstanceConfig {
    InstanceConfig {
        id: "dune-main".to_string(),
        game: "dune".to_string(),
        root: PathBuf::from("/tmp"),
        label: None,
        executable: None,
        args: vec![],
        working_dir: None,
        rcon: None,
        steamcmd: None,
        docker_compose: Some(DockerComposeConfig {
            file: Some(PathBuf::from("docker-compose.yml")),
            project: Some("duneproj".to_string()),
            service,
            command: Some(command),
        }),
    }
}

#[tokio::test]
async fn start_runs_compose_up_detached_and_sets_running() {
    let dir = tempfile::tempdir().unwrap();
    let args_log = dir.path().join("args.log");
    let exit_file = dir.path().join("exit");
    let docker = fake_docker(dir.path(), &args_log, &exit_file);

    let sup = DockerComposeSupervisor::new(&dune_instance(
        vec![docker.to_string_lossy().into_owned()],
        None,
    ));
    assert_eq!(sup.state(), InstanceState::Stopped);

    sup.clone().start().await.expect("compose up should succeed");
    assert_eq!(sup.state(), InstanceState::Running);

    let logged = std::fs::read_to_string(&args_log).unwrap();
    assert!(logged.contains("up -d"), "expected `up -d`; got: {logged}");
    assert!(logged.contains("-p duneproj"), "expected project flag; got: {logged}");
    assert!(logged.contains("-f docker-compose.yml"), "expected file flag; got: {logged}");
}

#[tokio::test]
async fn stop_runs_compose_stop_and_sets_stopped() {
    let dir = tempfile::tempdir().unwrap();
    let args_log = dir.path().join("args.log");
    let exit_file = dir.path().join("exit");
    let docker = fake_docker(dir.path(), &args_log, &exit_file);

    let sup = DockerComposeSupervisor::new(&dune_instance(
        vec![docker.to_string_lossy().into_owned()],
        None,
    ));
    sup.clone().start().await.expect("up");
    sup.clone().stop().await.expect("compose stop should succeed");
    assert_eq!(sup.state(), InstanceState::Stopped);
    assert_eq!(sup.uptime_seconds().await, 0);

    let logged = std::fs::read_to_string(&args_log).unwrap();
    assert!(logged.lines().any(|l| l.contains("stop")), "expected a `stop` call; got: {logged}");
}

#[tokio::test]
async fn restart_runs_compose_restart() {
    let dir = tempfile::tempdir().unwrap();
    let args_log = dir.path().join("args.log");
    let exit_file = dir.path().join("exit");
    let docker = fake_docker(dir.path(), &args_log, &exit_file);

    let sup = DockerComposeSupervisor::new(&dune_instance(
        vec![docker.to_string_lossy().into_owned()],
        None,
    ));
    sup.clone().restart().await.expect("compose restart should succeed");
    assert_eq!(sup.state(), InstanceState::Running);

    let logged = std::fs::read_to_string(&args_log).unwrap();
    assert!(logged.contains("restart"), "expected `restart`; got: {logged}");
}

#[tokio::test]
async fn single_service_is_targeted() {
    let dir = tempfile::tempdir().unwrap();
    let args_log = dir.path().join("args.log");
    let exit_file = dir.path().join("exit");
    let docker = fake_docker(dir.path(), &args_log, &exit_file);

    let sup = DockerComposeSupervisor::new(&dune_instance(
        vec![docker.to_string_lossy().into_owned()],
        Some("gameserver".to_string()),
    ));
    sup.clone().start().await.expect("up");

    let logged = std::fs::read_to_string(&args_log).unwrap();
    assert!(
        logged.contains("up -d gameserver"),
        "service must be appended after `up -d`; got: {logged}"
    );
}

#[tokio::test]
async fn compose_failure_errors_and_reverts_state() {
    let dir = tempfile::tempdir().unwrap();
    let args_log = dir.path().join("args.log");
    let exit_file = dir.path().join("exit");
    std::fs::write(&exit_file, "1").unwrap(); // make the fake docker fail
    let docker = fake_docker(dir.path(), &args_log, &exit_file);

    let sup = DockerComposeSupervisor::new(&dune_instance(
        vec![docker.to_string_lossy().into_owned()],
        None,
    ));
    let err = sup.clone().start().await.expect_err("nonzero compose exit must fail");
    assert!(err.to_string().contains("compose up failed"), "got: {err}");
    assert_eq!(sup.state(), InstanceState::Stopped, "failed start must revert to Stopped");
}

#[tokio::test]
async fn missing_docker_binary_errors_cleanly() {
    let sup = DockerComposeSupervisor::new(&dune_instance(
        vec!["/nonexistent/docker-xyz".to_string()],
        None,
    ));
    let err = sup.clone().start().await.expect_err("missing docker must fail");
    assert!(err.to_string().contains("docker"), "error should mention docker: {err}");
    assert_eq!(sup.state(), InstanceState::Stopped);
}
