#![no_main]
#![allow(missing_docs)]
#![allow(clippy::unwrap_used)]

use std::sync::LazyLock;

use anyhow::Result;
use brush_parser::ast;
use libfuzzer_sys::fuzz_target;

static TOKIO_RT: LazyLock<tokio::runtime::Runtime> =
    LazyLock::new(|| tokio::runtime::Runtime::new().unwrap());

static SHELL_TEMPLATE: LazyLock<brush_core::Shell> = LazyLock::new(|| {
    TOKIO_RT
        .block_on(
            brush_core::Shell::builder()
                .profile(brush_core::ProfileLoadBehavior::Skip)
                .rc(brush_core::RcLoadBehavior::Skip)
                .build(),
        )
        .unwrap()
});

fn eval_arithmetic(mut shell: brush_core::Shell, input: &ast::ArithmeticExpr) -> Result<()> {
    const DEFAULT_TIMEOUT_IN_SECONDS: u64 = 15;

    //
    // Turn it back into a string so we can pass it in on the command-line.
    //
    let input_str = input.to_string();

    //
    // Instantiate a brush shell with defaults, then try to evaluate the expression.
    //
    let parsed_expr = brush_parser::arithmetic::parse(input_str.as_str()).ok();
    let our_eval_result = if let Some(parsed_expr) = parsed_expr {
        shell.eval_arithmetic(&parsed_expr).ok()
    } else {
        None
    };

    //
    // Now run it under 'bash'
    //
    let mut oracle_cmd = std::process::Command::new("bash");
    oracle_cmd
        .arg("--noprofile")
        .arg("--norc")
        .arg("-O")
        .arg("extglob")
        .arg("-t");

    let mut oracle_cmd = assert_cmd::Command::from_std(oracle_cmd);

    oracle_cmd.timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_IN_SECONDS));

    let input = std::format!("echo \"$(( {input_str} ))\"\n");
    oracle_cmd.write_stdin(input.as_bytes());

    let oracle_result = oracle_cmd.output()?;
    let oracle_eval_result = if oracle_result.status.success() {
        let oracle_output = std::str::from_utf8(&oracle_result.stdout)?;
        oracle_output.trim().parse::<i64>().ok()
    } else {
        None
    };

    //
    // Compare results.
    //
    if our_eval_result != oracle_eval_result {
        Err(anyhow::anyhow!(
            "Mismatched eval results: {oracle_eval_result:?} from oracle vs. {our_eval_result:?} from our test (expr: '{input_str}', oracle result: {oracle_result:?})"
        ))
    } else {
        Ok(())
    }
}

fuzz_target!(|input: ast::ArithmeticExpr| {
    let s = input.to_string();
    let s = s.trim();

    // For now, intentionally ignore known problematic cases without actually running them.
    if s.contains("+ 0")
        || s.is_empty()
        || s.contains(|c: char| c.is_ascii_control() || !c.is_ascii())
        || s.contains("$[")
    // old deprecated form of arithmetic expansion
    {
        return;
    }

    let shell = SHELL_TEMPLATE.clone();
    eval_arithmetic(shell, &input).unwrap();
});
