// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! Test some trivial aspects of the timelimit crate.

#![expect(clippy::panic_in_result_fn, reason = "integration test")]
#![expect(clippy::tests_outside_test_module, reason = "integration test")]

use std::str::FromStr as _;
use std::time::Instant;

use anyhow::{Context as _, Error as AnyError, Result, anyhow};
use smol::io::AsyncReadExt as _;
use smol::process::{Command, Stdio};

use timelimit::exitcode::{ExitCodeReport, WrappedReport};
use timelimit::limit_smol::Limit;
use timelimit::{Config, Signal};

fn run_sleep(
    cfg: Config,
    cmd: Command,
    exitcode: u8,
    signalled: bool,
    thresh_low: u128,
    thresh_high: u128,
) {
    let before = Instant::now();
    let res = smol::block_on(Limit::new(cfg).run(cmd));
    let millis = before.elapsed().as_millis();

    if signalled {
        assert!(matches!(res.report(), WrappedReport::Signal(_)), "{res:?}");
    } else {
        assert!(
            matches!(res.report(), WrappedReport::Exact(rc) if *rc == exitcode),
            "{res:?}"
        );
    }
    assert_eq!(res.exitcode(), exitcode, "{res:?}");

    assert!(millis >= thresh_low, "{millis}");
    assert!(millis < thresh_high, "{millis}");
}

fn def_cmd() -> Command {
    let mut cmd = Command::new("sleep");
    cmd.arg("9");
    cmd
}

#[test]
fn limit_finish_bad() {
    let cmd = {
        let mut cmd = Command::new("sleep");
        cmd.arg("-1");
        cmd
    };
    run_sleep(
        Config::builder().warntime(1_f32).killtime(1_f32).build(),
        cmd,
        1_u8,
        false,
        0_u128,
        1500_u128,
    );
}

#[test]
fn limit_finish_immediately() {
    let cmd = {
        let mut cmd = Command::new("cat");
        cmd.stdin(Stdio::null());
        cmd
    };
    run_sleep(
        Config::builder().warntime(3_f32).killtime(3_f32).build(),
        cmd,
        0_u8,
        false,
        0_u128,
        1500_u128,
    );
}

#[test]
fn limit_finish() {
    run_sleep(
        Config::builder().warntime(12_f32).killtime(3_f32).build(),
        def_cmd(),
        0_u8,
        false,
        7500_u128,
        10500_u128,
    );
}

#[test]
fn limit_warn() {
    run_sleep(
        Config::builder().warntime(3_f32).killtime(6_f32).build(),
        def_cmd(),
        143_u8,
        true,
        1500_u128,
        4500_u128,
    );
}

#[test]
fn limit_kill() -> Result<()> {
    run_sleep(
        Config::builder()
            .warntime(3_f32)
            .warnsig(
                Signal::from_str("SIGWINCH").context("Could not recognize the SIGWINCH signal")?,
            )
            .killtime(3_f32)
            .build(),
        def_cmd(),
        137_u8,
        true,
        4500_u128,
        7500_u128,
    );
    Ok(())
}

#[test]
fn limit_with_io() -> Result<()> {
    let cfg = Config::builder().warntime(7.5_f32).killtime(3_f32).build();
    let cmd = {
        let mut cmd = Command::new("stdbuf");
        cmd.args([
            "-oL",
            "--",
            "sh",
            "-c",
            "echo 0; sleep 3; echo 1; sleep 3; echo 2; sleep 3; echo 3",
        ])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit());
        cmd
    };
    let (res, output) = smol::block_on(async move {
        let (tx, rx) = oneshot::channel();
        let res = Limit::new(cfg).run_with_io(cmd, Some(tx)).await;
        let mut child_io = rx
            .into_future()
            .await
            .with_context(|| format!("No child IO struct received, res {res:?}"))?;
        let mut output = Vec::new();
        child_io
            .take_stdout()
            .ok_or_else(|| anyhow!("No stdout in the child IO struct received, res {res:?}"))?
            .read_to_end(&mut output)
            .await
            .context("Could not read from the child's standard output stream")?;
        Ok::<(ExitCodeReport, Vec<u8>), AnyError>((res, output))
    })?;
    assert!(
        matches!(res.report(), WrappedReport::Signal(_)),
        "{res:?} {output:?}"
    );
    assert_eq!(res.exitcode(), 143_u8, "{res:?} {output:?}");
    assert_eq!(output, b"0\n1\n2\n", "{output:?}");
    Ok(())
}
