//! Configuration for how tests get run.

use std::ops::RangeInclusive;
use std::sync::LazyLock;
use std::{env, fmt, str};

use crate::generate::random::{SEED, SEED_ENV};
use crate::{BaseName, Group, Identifier, Ty, test_log};

/// The environment variable indicating which extensive tests should be run.
pub const EXTENSIVE_ENV: &str = "LIBM_EXTENSIVE_TESTS";

/// Specify the number of iterations via this environment variable, rather than using the default.
pub const EXTENSIVE_ITER_ENV: &str = "LIBM_EXTENSIVE_ITERATIONS";

/// The override value, if set by the above environment.
static EXTENSIVE_ITER_OVERRIDE: LazyLock<Option<u64>> = LazyLock::new(|| {
    env::var(EXTENSIVE_ITER_ENV)
        .map(|v| v.parse().expect("failed to parse iteration count"))
        .ok()
});

/// Specific tests that need to have a reduced amount of iterations to complete in a reasonable
/// amount of time.
const EXTREMELY_SLOW_TESTS: &[SlowTest] = &[
    SlowTest {
        ident: Identifier::Fmodf128,
        gen_kind: GeneratorKind::Spaced,
        extensive: false,
        reduce_factor: 50,
    },
    SlowTest {
        ident: Identifier::Fmodf128,
        gen_kind: GeneratorKind::Spaced,
        extensive: true,
        reduce_factor: 50,
    },
];

/// A pattern to match a `CheckCtx`, plus a factor to reduce by.
struct SlowTest {
    ident: Identifier,
    gen_kind: GeneratorKind,
    extensive: bool,
    reduce_factor: u64,
}

impl SlowTest {
    /// True if the test in `CheckCtx` should be reduced by `reduce_factor`.
    fn matches_ctx(&self, ctx: &CheckCtx) -> bool {
        self.ident == ctx.fn_ident
            && self.gen_kind == ctx.gen_kind
            && self.extensive == ctx.extensive
    }
}

/// Maximum number of iterations to run for a single routine.
///
/// The default value of one greater than `u32::MAX` allows testing single-argument `f32` routines
/// and single- or double-argument `f16` routines exhaustively. `f64` and `f128` can't feasibly
/// be tested exhaustively; however, [`EXTENSIVE_ITER_ENV`] can be set to run tests for multiple
/// hours.
pub fn extensive_max_iterations() -> u64 {
    let default = 1 << 32; // default value
    EXTENSIVE_ITER_OVERRIDE.unwrap_or(default)
}

/// Context passed to [`CheckOutput`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckCtx {
    /// Allowed ULP deviation
    pub ulp: Option<u32>,
    pub fn_ident: Identifier,
    pub base_name: BaseName,
    /// Function name.
    pub fn_name: &'static str,
    /// Return the unsuffixed version of the function name.
    pub base_name_str: &'static str,
    /// Source of truth for tests.
    pub basis: CheckBasis,
    pub gen_kind: GeneratorKind,
    pub extensive: bool,
    /// If specified, this value will override the value returned by [`iteration_count`].
    pub override_iterations: Option<u64>,
}

impl CheckCtx {
    /// Create a new check context, using the default ULP for the function.
    pub fn new(fn_ident: Identifier, basis: CheckBasis, gen_kind: GeneratorKind) -> Self {
        let mut ret = Self {
            ulp: None,
            fn_ident,
            fn_name: fn_ident.as_str(),
            base_name: fn_ident.base_name(),
            base_name_str: fn_ident.base_name().as_str(),
            basis,
            gen_kind,
            extensive: false,
            override_iterations: None,
        };
        ret.ulp = crate::default_ulp(&ret);
        ret
    }

    /// Configure that this is an extensive test.
    pub fn extensive(mut self, extensive: bool) -> Self {
        self.extensive = extensive;
        self
    }

    /// The number of input arguments for this function.
    pub fn input_count(&self) -> usize {
        self.fn_ident.math_op().rust_sig.args.len()
    }

    pub fn override_iterations(&mut self, count: u64) {
        self.override_iterations = Some(count)
    }
}

/// Possible items to test against
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CheckBasis {
    /// Check against Musl's math sources.
    Musl,
    /// Check against infinite precision (MPFR).
    Mpfr,
    /// Benchmarks or other times when this is not relevant.
    None,
}

/// The different kinds of generators that provide test input, which account for input pattern
/// and quantity.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GeneratorKind {
    /// Extremes, zeros, nonstandard numbers, etc.
    EdgeCases,
    /// Spaced by logarithm (floats) or linear (integers).
    Spaced,
    /// Test inputs from an RNG.
    Random,
    /// A provided test case list.
    List,
}

/// A list of all functions that should get extensive tests, as configured by environment variable.
///
/// This also supports the special test name `all` to run all tests, as well as `all_f16`,
/// `all_f32`, `all_f64`, and `all_f128` to run all tests for a specific float type.
static EXTENSIVE: LazyLock<Vec<Identifier>> = LazyLock::new(|| {
    let var = env::var(EXTENSIVE_ENV).unwrap_or_default();
    let list = var.split(",").filter(|s| !s.is_empty()).collect::<Vec<_>>();
    let mut ret = Vec::new();

    let append_ty_ops = |ret: &mut Vec<_>, group: Group| {
        let iter = Identifier::ALL
            .iter()
            .filter(move |id| id.math_op().group == group)
            .copied();
        ret.extend(iter);
    };

    for item in list {
        match item {
            "all" => ret = Identifier::ALL.to_owned(),
            "all_f16" => append_ty_ops(&mut ret, Group::F16),
            "all_f32" => append_ty_ops(&mut ret, Group::F32),
            "all_f64" => append_ty_ops(&mut ret, Group::F64),
            "all_f128" => append_ty_ops(&mut ret, Group::F128),
            "all_int" => append_ty_ops(&mut ret, Group::Integer),
            s => {
                let id = Identifier::from_str(s)
                    .unwrap_or_else(|| panic!("unrecognized test name `{s}`"));
                ret.push(id);
            }
        }
    }

    ret
});

/// Most ops are somewhere on the order or 10^7 iterations per second when running exhaustive
/// tests. Assuming about four hours to run, this is log2 of the max number of inputs that coul
/// be tested.
const MAX_REASONABLE_EXHAUSTIVE_BITS: u32 = 36;

/// Information about the function to be tested.
#[derive(Debug)]
struct TestEnv {
    /// Tests should be reduced because the platform is slow. E.g. 32-bit or emulated.
    slow_platform: bool,
    /// How many bits of input there are for this function.
    total_input_bits: u32,
    /// Env indicates that an extensive test should be run.
    should_run_extensive: bool,
    /// Multiprecision tests will be run.
    mp_tests_enabled: bool,
    /// The number of inputs to the function.
    input_count: usize,
}

impl TestEnv {
    fn from_env(ctx: &CheckCtx) -> Self {
        let id = ctx.fn_ident;
        let op = id.math_op();

        let will_run_mp = cfg!(feature = "build-mpfr");

        let mut total_input_bits = 0;
        for ty in op.rust_sig.args {
            total_input_bits += ty.effective_bits();
        }

        let will_run_extensive = EXTENSIVE.contains(&id);

        let input_count = op.rust_sig.args.len();

        Self {
            slow_platform: slow_platform(),
            total_input_bits,
            should_run_extensive: will_run_extensive,
            mp_tests_enabled: will_run_mp,
            input_count,
        }
    }
}

/// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run in QEMU. Start
/// with a reduced number on these platforms.
fn slow_platform() -> bool {
    let slow_on_ci = crate::emulated()
        || usize::BITS < 64
        || cfg!(all(target_arch = "x86_64", target_vendor = "apple"));

    // If not running in CI, there is no need to reduce iteration count.
    slow_on_ci && crate::ci()
}

/// The number of iterations to run for a given test.
pub fn iteration_count(ctx: &CheckCtx, argnum: usize) -> u64 {
    let t_env = TestEnv::from_env(ctx);

    // Ideally run 5M tests
    let mut domain_iter_count: u64 = 4_000_000;

    // Start with a reduced number of tests on slow platforms.
    if t_env.slow_platform {
        domain_iter_count = 100_000;
    }

    // If we will be running tests against MPFR, we don't need to test as much against musl.
    // However, there are some platforms where we have to test against musl since MPFR can't be
    // built.
    if t_env.mp_tests_enabled && ctx.basis == CheckBasis::Musl {
        domain_iter_count /= 100;
    }

    // Run fewer random tests than domain tests.
    let random_iter_count = domain_iter_count / 100;

    let mut total_iterations = match ctx.gen_kind {
        GeneratorKind::Spaced if ctx.extensive => extensive_max_iterations(),
        GeneratorKind::Spaced => domain_iter_count,
        GeneratorKind::Random => random_iter_count,
        GeneratorKind::EdgeCases | GeneratorKind::List => {
            unimplemented!("shoudn't need `iteration_count` for {:?}", ctx.gen_kind)
        }
    };

    // This signature has too many possible inputs to test exhaustively, so increase input count
    // on all other kinds of tests to get better coverage.
    if t_env.total_input_bits > MAX_REASONABLE_EXHAUSTIVE_BITS {
        if ctx.extensive {
            // Extensive already has a pretty high test count.
            total_iterations *= 2;
        } else {
            total_iterations *= 4;
        }
    }

    // Functions with more arguments get more iterations.
    let arg_multiplier = 1 << (t_env.input_count - 1);
    total_iterations *= arg_multiplier;

    // FMA has a huge domain but is reasonably fast to run, so increase another 1.5x.
    if ctx.base_name == BaseName::Fma {
        total_iterations = 3 * total_iterations / 2;
    }

    // Some tests are significantly slower than others and need to be further reduced.
    if let Some(slow) = EXTREMELY_SLOW_TESTS
        .iter()
        .find(|slow| slow.matches_ctx(ctx))
    {
        // However, do not override if the extensive iteration count has been manually set.
        if !(ctx.extensive && EXTENSIVE_ITER_OVERRIDE.is_some()) {
            total_iterations /= slow.reduce_factor;
        }
    }

    if cfg!(optimizations_enabled) {
        // Always run at least 10,000 tests.
        total_iterations = total_iterations.max(10_000);
    } else {
        // Without optimizations, just run a quick check regardless of other parameters.
        total_iterations = 800;
    }

    let mut overridden = false;
    if let Some(count) = ctx.override_iterations {
        total_iterations = count;
        overridden = true;
    }

    // Adjust for the number of inputs
    let ntests = match t_env.input_count {
        1 => total_iterations,
        2 => (total_iterations as f64).sqrt().ceil() as u64,
        3 => (total_iterations as f64).cbrt().ceil() as u64,
        _ => panic!("test has more than three arguments"),
    };

    let total = ntests.pow(t_env.input_count.try_into().unwrap());

    let seed_msg = match ctx.gen_kind {
        GeneratorKind::Spaced => String::new(),
        GeneratorKind::Random => {
            format!(
                " using `{SEED_ENV}={}`",
                str::from_utf8(SEED.as_slice()).unwrap()
            )
        }
        GeneratorKind::EdgeCases | GeneratorKind::List => unimplemented!(),
    };

    test_log(&format!(
        "{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {ntests} iterations \
         ({total} total){seed_msg}{omsg}",
        gen_kind = ctx.gen_kind,
        basis = ctx.basis,
        fn_ident = ctx.fn_ident,
        arg = argnum + 1,
        args = t_env.input_count,
        omsg = if overridden { " (overridden)" } else { "" }
    ));

    ntests
}

/// Some tests require that an integer be kept within reasonable limits; if that is needed, retun
/// a limited range.
pub fn int_range<I>(ctx: &CheckCtx, argnum: usize) -> Option<RangeInclusive<I>>
where
    I: TryFrom<i32, Error: fmt::Debug>,
{
    let t_env = TestEnv::from_env(ctx);

    let argcount = ctx.fn_ident.math_op().rust_sig.args.len();
    assert!(
        argnum < argcount,
        "requested argnum {argnum} of only {argcount} args"
    );

    // Shift operations can have UB if the shift value exceeds their range
    if matches!(
        ctx.base_name,
        BaseName::Ashl | BaseName::Ashr | BaseName::Lshr
    ) && argnum == 1
    {
        let max = match ctx.fn_ident.math_op().rust_sig.args[0] {
            Ty::U32 | Ty::I32 => 31,
            Ty::U64 | Ty::I64 => 63,
            Ty::U128 | Ty::I128 => 127,
            ty => panic!("unexpected type {ty}"),
        };

        return Some(map_range(0..=max));
    }

    // Use the whole range for most functions.
    if !matches!(ctx.base_name, BaseName::Jn | BaseName::Yn) {
        return None;
    }

    assert_eq!(
        argnum, 0,
        "For `jn`/`yn`, only the first argument takes an integer"
    );

    // The integer argument to `jn` is an iteration count. Limit this to ensure tests can be
    // completed in a reasonable amount of time.
    let non_extensive_range = if t_env.slow_platform || !cfg!(optimizations_enabled) {
        (-0xf)..=0xff
    } else {
        (-0xff)..=0xffff
    };

    let extensive_range = (-0xfff)..=0xfffff;

    let ret = match ctx.gen_kind {
        _ if ctx.extensive => extensive_range,
        GeneratorKind::Spaced | GeneratorKind::Random => non_extensive_range,
        GeneratorKind::EdgeCases => extensive_range,
        GeneratorKind::List => unimplemented!("shoudn't need range for {:?}", ctx.gen_kind),
    };

    Some(map_range(ret))
}

fn map_range<I>(r: RangeInclusive<i32>) -> RangeInclusive<I>
where
    I: TryFrom<i32, Error: fmt::Debug>,
{
    I::try_from(*r.start()).unwrap()..=I::try_from(*r.end()).unwrap()
}

/// For domain tests, limit how many asymptotes or specified check points we test.
pub fn check_point_count(ctx: &CheckCtx) -> usize {
    assert_eq!(
        ctx.gen_kind,
        GeneratorKind::EdgeCases,
        "check_point_count is intended for edge case tests"
    );
    let t_env = TestEnv::from_env(ctx);
    if t_env.slow_platform || !cfg!(optimizations_enabled) {
        4
    } else {
        10
    }
}

/// When validating points of interest (e.g. asymptotes, inflection points, extremes), also check
/// this many surrounding values.
pub fn check_near_count(ctx: &CheckCtx) -> u64 {
    assert_eq!(
        ctx.gen_kind,
        GeneratorKind::EdgeCases,
        "check_near_count is intended for edge case tests"
    );
    if cfg!(optimizations_enabled) {
        // Taper based on the number of inputs.
        match ctx.input_count() {
            1 | 2 => 100,
            3 => 50,
            x => panic!("unexpected argument count {x}"),
        }
    } else {
        8
    }
}

/// Check whether extensive actions should be run or skipped.
pub fn skip_extensive_test(ctx: &CheckCtx) -> bool {
    let t_env = TestEnv::from_env(ctx);
    !t_env.should_run_extensive
}

/// The number of iterations to run for `u256` fuzz tests.
pub fn bigint_fuzz_iteration_count() -> u64 {
    if !cfg!(optimizations_enabled) {
        return 1000;
    }

    if slow_platform() { 100_000 } else { 5_000_000 }
}
