//! Exhaustive tests for `f16` and `f32`, high-iteration for `f64` and `f128`.

use std::fmt;
use std::io::{self, IsTerminal};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;

use indicatif::{ProgressBar, ProgressStyle};
use libm_test::generate::spaced;
use libm_test::mpfloat::MpOp;
use libm_test::{
    CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TestResult, TupleCall,
    skip_extensive_test,
};
use libtest_mimic::{Arguments, Trial};
use rayon::prelude::*;
use spaced::SpacedInput;

const BASIS: CheckBasis = CheckBasis::Mpfr;

/// Run the extensive test suite.
pub fn run() {
    let mut args = Arguments::from_args();
    // Prevent multiple tests from running in parallel, each test gets parallized internally.
    args.test_threads = Some(1);
    let tests = register_all_tests();

    // With default parallelism, the CPU doesn't saturate. We don't need to be nice to
    // other processes, so do 1.5x to make sure we use all available resources.
    let threads = std::thread::available_parallelism()
        .map(Into::into)
        .unwrap_or(0)
        * 3
        / 2;
    rayon::ThreadPoolBuilder::new()
        .num_threads(threads)
        .build_global()
        .unwrap();

    libtest_mimic::run(&args, tests).exit();
}

macro_rules! mp_extensive_tests {
    (
        fn_name: $fn_name:ident,
        attrs: [$($attr:meta),*],
        extra: [$push_to:ident],
    ) => {
        $(#[$attr])*
        register_single_test::<libm_test::op::$fn_name::Routine>(&mut $push_to);
    };
}

/// Create a list of tests for consumption by `libtest_mimic`.
fn register_all_tests() -> Vec<Trial> {
    let mut all_tests = Vec::new();

    libm_macros::for_each_function! {
        callback: mp_extensive_tests,
        extra: [all_tests],
        skip: [
            // FIXME: test needed, see
            // https://github.com/rust-lang/libm/pull/311#discussion_r1818273392
            nextafter,
            nextafterf,
        ],
    }

    all_tests
}

/// Add a single test to the list.
fn register_single_test<Op>(all: &mut Vec<Trial>)
where
    Op: MathOp + MpOp,
    Op::RustArgs: SpacedInput<Op> + Send,
{
    let test_name = format!("mp_extensive_{}", Op::NAME);
    let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::Spaced).extensive(true);
    let skip = skip_extensive_test(&ctx);

    let runner = move || {
        if !cfg!(optimizations_enabled) {
            panic!("extensive tests should be run with --release");
        }

        let res = run_single_test::<Op>(&ctx);
        let e = match res {
            Ok(()) => return Ok(()),
            Err(e) => e,
        };

        // Format with the `Debug` implementation so we get the error cause chain, and print it
        // here so we see the result immediately (rather than waiting for all tests to conclude).
        let e = format!("{e:?}");
        eprintln!("failure testing {}:{e}\n", Op::IDENTIFIER);

        Err(e.into())
    };

    all.push(Trial::test(test_name, runner).with_ignored_flag(skip));
}

/// Test runner for a signle routine.
fn run_single_test<Op>(ctx: &CheckCtx) -> TestResult
where
    Op: MathOp + MpOp,
    Op::RustArgs: SpacedInput<Op> + Send,
{
    // Small delay before printing anything so other output from the runner has a chance to flush.
    std::thread::sleep(Duration::from_millis(500));
    eprintln!();

    let completed = AtomicU64::new(0);
    let (ref mut cases, total) = spaced::get_test_cases::<Op>(ctx);
    let pb = Progress::new(Op::NAME, total);

    let test_single_chunk = |mp_vals: &mut Op::MpTy, input_vec: Vec<Op::RustArgs>| -> TestResult {
        for input in input_vec {
            // Test the input.
            let mp_res = Op::run(mp_vals, input);
            let crate_res = input.call_intercept_panics(Op::ROUTINE);
            crate_res.validate(mp_res, input, ctx)?;

            let completed = completed.fetch_add(1, Ordering::Relaxed) + 1;
            pb.update(completed, input);
        }

        Ok(())
    };

    // Chunk the cases so Rayon doesn't switch threads between each iterator item. 50k seems near
    // a performance sweet spot. Ideally we would reuse these allocations rather than discarding,
    // but that is difficult with Rayon's API.
    let chunk_size = 50_000;
    let chunks = std::iter::from_fn(move || {
        let mut v = Vec::with_capacity(chunk_size);
        v.extend(cases.take(chunk_size));
        (!v.is_empty()).then_some(v)
    });

    // Run the actual tests
    let res = chunks
        .par_bridge()
        .try_for_each_init(Op::new_mp, test_single_chunk);

    let real_total = completed.load(Ordering::Relaxed);
    pb.complete(real_total);

    if res.is_ok() && real_total != total {
        // Provide a warning if our estimate needs to be updated.
        panic!("total run {real_total} does not match expected {total}");
    }

    res
}

/// Wrapper around a `ProgressBar` that handles styles and non-TTY messages.
struct Progress {
    pb: ProgressBar,
    name_padded: String,
    final_style: ProgressStyle,
    is_tty: bool,
}

impl Progress {
    const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \
        {human_pos:>13}/{human_len:13} {per_sec:18} eta {eta:8} {msg}";
    const PB_TEMPLATE_FINAL: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \
        {human_pos:>13}/{human_len:13} {per_sec:18} done in {elapsed_precise}";

    fn new(name: &str, total: u64) -> Self {
        eprintln!("starting extensive tests for `{name}`");
        let name_padded = format!("{name:9}");
        let is_tty = io::stderr().is_terminal();

        let initial_style =
            ProgressStyle::with_template(&Self::PB_TEMPLATE.replace("NAME", &name_padded))
                .unwrap()
                .progress_chars("##-");

        let final_style =
            ProgressStyle::with_template(&Self::PB_TEMPLATE_FINAL.replace("NAME", &name_padded))
                .unwrap()
                .progress_chars("##-");

        let pb = ProgressBar::new(total);
        pb.set_style(initial_style);

        Self {
            pb,
            final_style,
            name_padded,
            is_tty,
        }
    }

    fn update(&self, completed: u64, input: impl fmt::Debug) {
        // Infrequently update the progress bar.
        if completed.is_multiple_of(20_000) {
            self.pb.set_position(completed);
        }

        if completed.is_multiple_of(500_000) {
            self.pb.set_message(format!("input: {input:<24?}"));
        }

        if !self.is_tty && completed.is_multiple_of(5_000_000) {
            let len = self.pb.length().unwrap_or_default();
            eprintln!(
                "[{elapsed:3?}s {percent:3.0}%] {name} \
                {human_pos:>10}/{human_len:<10} {per_sec:14.2}/s eta {eta:4}s {input:<24?}",
                elapsed = self.pb.elapsed().as_secs(),
                percent = completed as f32 * 100.0 / len as f32,
                name = self.name_padded,
                human_pos = completed,
                human_len = len,
                per_sec = self.pb.per_sec(),
                eta = self.pb.eta().as_secs()
            );
        }
    }

    fn complete(self, real_total: u64) {
        self.pb.set_style(self.final_style);
        self.pb.set_position(real_total);
        self.pb.abandon();

        if !self.is_tty {
            let len = self.pb.length().unwrap_or_default();
            eprintln!(
                "[{elapsed:3}s {percent:3.0}%] {name} \
                {human_pos:>10}/{human_len:<10} {per_sec:14.2}/s done in {elapsed_precise}",
                elapsed = self.pb.elapsed().as_secs(),
                percent = real_total as f32 * 100.0 / len as f32,
                name = self.name_padded,
                human_pos = real_total,
                human_len = len,
                per_sec = self.pb.per_sec(),
                elapsed_precise = self.pb.elapsed().as_secs(),
            );
        }

        eprintln!();
    }
}
