// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use rstest::rstest;

use uucore::display::Quotable;
// spell-checker:ignore dont SIGBUS SIGSEGV sigsegv sigbus infd
use uutests::new_ucmd;

#[cfg(unix)]
use nix::sys::signal::Signal::{SIGBUS, SIGSEGV};
use std::io::ErrorKind;
use std::time::{Duration, Instant};

#[test]
fn test_invalid_time_interval() {
    new_ucmd!()
        .arg("xyz")
        .fails()
        .usage_error("invalid time interval 'xyz'");
    new_ucmd!()
        .args(&["--", "-1"])
        .fails()
        .usage_error("invalid time interval '-1'");
}

#[test]
fn test_sleep_no_suffix() {
    let millis_100 = Duration::from_millis(100);
    let before_test = Instant::now();

    new_ucmd!().args(&["0.1"]).succeeds().stdout_only("");

    let duration = before_test.elapsed();
    assert!(duration >= millis_100);
}

#[test]
fn test_sleep_s_suffix() {
    let millis_100 = Duration::from_millis(100);
    let before_test = Instant::now();

    new_ucmd!().args(&["0.1s"]).succeeds().stdout_only("");

    let duration = before_test.elapsed();
    assert!(duration >= millis_100);
}

#[test]
fn test_sleep_m_suffix() {
    let millis_600 = Duration::from_millis(600);
    let before_test = Instant::now();

    new_ucmd!().args(&["0.01m"]).succeeds().stdout_only("");

    let duration = before_test.elapsed();
    assert!(duration >= millis_600);
}

#[test]
fn test_sleep_h_suffix() {
    let millis_360 = Duration::from_millis(360);
    let before_test = Instant::now();

    new_ucmd!().args(&["0.0001h"]).succeeds().stdout_only("");

    let duration = before_test.elapsed();
    assert!(duration >= millis_360);
}

#[test]
fn test_sleep_negative_duration() {
    new_ucmd!().args(&["-1"]).fails();
    new_ucmd!().args(&["-1s"]).fails();
    new_ucmd!().args(&["-1m"]).fails();
    new_ucmd!().args(&["-1h"]).fails();
    new_ucmd!().args(&["-1d"]).fails();
}

#[test]
fn test_sleep_zero_duration() {
    new_ucmd!().args(&["0"]).succeeds().stdout_only("");
    new_ucmd!().args(&["0s"]).succeeds().stdout_only("");
    new_ucmd!().args(&["0m"]).succeeds().stdout_only("");
    new_ucmd!().args(&["0h"]).succeeds().stdout_only("");
    new_ucmd!().args(&["0d"]).succeeds().stdout_only("");
}

#[test]
fn test_sleep_no_argument() {
    new_ucmd!().fails().usage_error("missing operand");
}

#[test]
fn test_sleep_sum_duration_same_suffix() {
    let millis_200 = Duration::from_millis(100 + 100);
    let before_test = Instant::now();

    new_ucmd!()
        .args(&["0.1s", "0.1s"])
        .succeeds()
        .stdout_only("");

    let duration = before_test.elapsed();
    assert!(duration >= millis_200);
}

#[test]
fn test_sleep_sum_duration_different_suffix() {
    let millis_700 = Duration::from_millis(100 + 600);
    let before_test = Instant::now();

    new_ucmd!()
        .args(&["0.1s", "0.01m"])
        .succeeds()
        .stdout_only("");

    let duration = before_test.elapsed();
    assert!(duration >= millis_700);
}

#[test]
fn test_sleep_sum_duration_many() {
    let millis_900 = Duration::from_millis(100 + 100 + 300 + 400);
    let before_test = Instant::now();

    new_ucmd!()
        .args(&["0.1s", "0.1s", "0.3s", "0.4s"])
        .succeeds()
        .stdout_only("");

    let duration = before_test.elapsed();
    assert!(duration >= millis_900);
}

#[test]
fn test_sleep_wrong_time() {
    new_ucmd!().args(&["0.1s", "abc"]).fails();
}

#[test]
#[cfg(unix)]
fn test_sleep_stops_after_sigsegv() {
    let mut child = new_ucmd!()
        .arg("100")
        .timeout(Duration::from_secs(10))
        .run_no_wait();

    child
        .delay(100)
        .kill_with_custom_signal(SIGSEGV)
        .make_assertion()
        .with_current_output()
        .signal_is(SIGSEGV as i32) // make sure it was us who terminated the process
        .no_output();
}

#[test]
#[cfg(unix)]
fn test_sleep_stops_after_sigbus() {
    let mut child = new_ucmd!()
        .arg("100")
        .timeout(Duration::from_secs(10))
        .run_no_wait();

    child
        .delay(100)
        .kill_with_custom_signal(SIGBUS)
        .make_assertion()
        .with_current_output()
        .signal_is(SIGBUS as i32) // make sure it was us who terminated the process
        .no_output();
}

#[test]
fn test_sleep_when_single_input_exceeds_max_duration_then_no_error() {
    let mut child = new_ucmd!()
        .arg(format!("{}", u128::from(u64::MAX) + 1))
        .timeout(Duration::from_secs(10))
        .run_no_wait();

    #[cfg(unix)]
    child
        .delay(100)
        .kill()
        .make_assertion()
        .with_current_output()
        .signal_is(9) // make sure it was us who terminated the process
        .no_output();
    #[cfg(windows)]
    child
        .delay(100)
        .kill()
        .make_assertion()
        .with_current_output()
        .failure()
        .no_output();
}

#[test]
fn test_sleep_when_multiple_inputs_exceed_max_duration_then_no_error() {
    let mut child = new_ucmd!()
        .arg(format!("{}", u64::MAX))
        .arg("1")
        .timeout(Duration::from_secs(10))
        .run_no_wait();

    #[cfg(unix)]
    child
        .delay(100)
        .kill()
        .make_assertion()
        .with_current_output()
        .signal_is(9) // make sure it was us who terminated the process
        .no_output();
    #[cfg(windows)]
    child
        .delay(100)
        .kill()
        .make_assertion()
        .with_current_output()
        .failure()
        .no_output();
}

#[rstest]
#[case::whitespace_prefix(" 0.1s")]
#[case::multiple_whitespace_prefix("   0.1s")]
fn test_sleep_when_input_has_leading_whitespace_then_no_error(#[case] input: &str) {
    new_ucmd!()
        .arg(input)
        .timeout(Duration::from_secs(10))
        .succeeds()
        .no_output();
}

#[rstest]
#[case::whitespace_suffix("0.1s ")]
#[case::mixed_newlines_spaces_tabs("\n\t0.1s \n ")]
fn test_sleep_when_input_has_trailing_whitespace_then_error(#[case] input: &str) {
    new_ucmd!()
        .arg(input)
        .timeout(Duration::from_secs(10))
        .fails()
        .usage_error(format!("invalid time interval {}", input.quote()));
}

#[rstest]
#[case::only_space(" ")]
#[case::only_tab("\t")]
#[case::only_newline("\n")]
fn test_sleep_when_input_has_only_whitespace_then_error(#[case] input: &str) {
    new_ucmd!()
        .arg(input)
        .timeout(Duration::from_secs(10))
        .fails()
        .usage_error(format!("invalid time interval {}", input.quote()));
}

#[test]
fn test_sleep_when_multiple_input_some_with_error_then_shows_all_errors() {
    let expected = "invalid time interval 'abc'\n\
                    sleep: invalid time interval '1years'\n\
                    sleep: invalid time interval ' '";

    // Even if one of the arguments is valid, but the rest isn't, we should still fail and exit early.
    // So, the timeout of 10 seconds ensures we haven't executed `thread::sleep` with the only valid
    // interval of `100000.0` seconds.
    new_ucmd!()
        .args(&["abc", "100000.0", "1years", " "])
        .timeout(Duration::from_secs(10))
        .fails()
        .usage_error(expected);
}

#[test]
fn test_negative_interval() {
    new_ucmd!()
        .args(&["--", "-1"])
        .fails()
        .usage_error("invalid time interval '-1'");
}

#[rstest]
#[case::int("0x0")]
#[case::negative_zero("-0x0")]
#[case::int_suffix("0x0s")]
#[case::int_suffix("0x0h")]
#[case::frac("0x0.1")]
#[case::frac_suffix("0x0.1s")]
#[case::frac_suffix("0x0.001h")]
#[case::scientific("0x1.0p-3")]
#[case::scientific_suffix("0x1.0p-4s")]
fn test_valid_hex_duration(#[case] input: &str) {
    new_ucmd!().args(&["--", input]).succeeds().no_output();
}

#[rstest]
#[case::negative("-0x1")]
#[case::negative_suffix("-0x1s")]
#[case::negative_frac_suffix("-0x0.1s")]
#[case::wrong_capitalization("infD")]
#[case::wrong_capitalization("INFD")]
#[case::wrong_capitalization("iNfD")]
#[case::single_quote("'1")]
fn test_invalid_duration(#[case] input: &str) {
    new_ucmd!()
        .args(&["--", input])
        .fails()
        .usage_error(format!("invalid time interval {}", input.quote()));
}

#[cfg(unix)]
#[test]
#[should_panic = "Program must be run first or has not finished"]
fn test_cmd_result_signal_when_still_running_then_panic() {
    let mut child = new_ucmd!().arg("60").run_no_wait();

    child
        .make_assertion()
        .is_alive()
        .with_current_output()
        .signal();
}

#[cfg(unix)]
#[test]
fn test_cmd_result_signal_when_kill_then_signal() {
    let mut child = new_ucmd!().arg("60").run_no_wait();

    child.kill();
    child
        .make_assertion()
        .is_not_alive()
        .with_current_output()
        .signal_is(9)
        .signal_name_is("SIGKILL")
        .signal_name_is("KILL")
        .signal_name_is("9")
        .signal()
        .expect("Signal was none");

    child
        .wait()
        .unwrap()
        .signal_is(9)
        .signal_name_is("SIGKILL")
        .signal_name_is("KILL")
        .signal_name_is("9")
        .signal()
        .expect("Signal was none");
}

#[cfg(unix)]
#[rstest]
#[case::signal_only_part_of_name("IGKILL")] // spell-checker: disable-line
#[case::signal_just_sig("SIG")]
#[case::signal_value_too_high("100")]
#[case::signal_value_negative("-1")]
#[should_panic = "Invalid signal name or value"]
fn test_cmd_result_signal_when_invalid_signal_name_then_panic(#[case] signal_name: &str) {
    let mut child = new_ucmd!().arg("60").run_no_wait();

    child.kill();
    child.wait().unwrap().signal_name_is(signal_name);
}

#[test]
#[cfg(unix)]
fn test_cmd_result_signal_name_is_accepts_lowercase() {
    let mut child = new_ucmd!().arg("60").run_no_wait();
    child.kill();
    let result = child.wait().unwrap();
    result.signal_name_is("sigkill");
    result.signal_name_is("kill");
}

#[test]
fn test_uchild_when_wait_and_timeout_is_reached_then_timeout_error() {
    let child = new_ucmd!()
        .timeout(Duration::from_secs(1))
        .arg("10.0")
        .run_no_wait();

    match child.wait() {
        Err(error) if error.kind() == ErrorKind::Other => {
            std::assert_eq!(error.to_string(), "wait: Timeout of '1s' reached");
        }
        Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"),
        Ok(_) => panic!("Assertion failed: Expected timeout of `wait`."),
    }
}

#[rstest]
#[timeout(Duration::from_secs(5))]
fn test_uchild_when_kill_and_timeout_higher_than_kill_time_then_no_panic() {
    let mut child = new_ucmd!()
        .timeout(Duration::from_secs(60))
        .arg("20.0")
        .run_no_wait();

    child.kill().make_assertion().is_not_alive();
}

#[test]
fn test_uchild_when_try_kill_and_timeout_is_reached_then_error() {
    let mut child = new_ucmd!()
        .timeout(Duration::ZERO)
        .arg("10.0")
        .run_no_wait();

    match child.try_kill() {
        Err(error) if error.kind() == ErrorKind::Other => {
            std::assert_eq!(error.to_string(), "kill: Timeout of '0s' reached");
        }
        Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"),
        Ok(()) => panic!("Assertion failed: Expected timeout of `try_kill`."),
    }
}

#[test]
#[should_panic = "kill: Timeout of '0s' reached"]
fn test_uchild_when_kill_with_timeout_and_timeout_is_reached_then_panic() {
    let mut child = new_ucmd!()
        .timeout(Duration::ZERO)
        .arg("10.0")
        .run_no_wait();

    child.kill();
    panic!("Assertion failed: Expected timeout of `kill`.");
}

#[test]
#[should_panic(expected = "wait: Timeout of '1.1s' reached")]
fn test_ucommand_when_run_with_timeout_and_timeout_is_reached_then_panic() {
    new_ucmd!()
        .timeout(Duration::from_millis(1100))
        .arg("10.0")
        .run();

    panic!("Assertion failed: Expected timeout of `run`.")
}

#[rstest]
#[timeout(Duration::from_secs(10))]
fn test_ucommand_when_run_with_timeout_higher_then_execution_time_then_no_panic() {
    new_ucmd!()
        .timeout(Duration::from_secs(60))
        .arg("1.0")
        .run();
}
