use rand::{rngs::StdRng, SeedableRng};
use shuttle::rand::Rng;
use shuttle::scheduler::{DfsScheduler, Scheduler};
use shuttle::sync::{Arc, Mutex};
use shuttle::{check_uncontrolled_nondeterminism, thread};
use test_log::test;

fn check_uncontrolled_nondeterminism_custom_scheduler_and_config<F, S>(f: F, scheduler: S)
where
    F: Fn() + Send + Sync + 'static,
    S: Scheduler + 'static,
{
    use shuttle::scheduler::UncontrolledNondeterminismCheckScheduler;
    use shuttle::Config;

    let config = Config::default();

    let scheduler = UncontrolledNondeterminismCheckScheduler::new(scheduler);

    let runner = shuttle::Runner::new(scheduler, config);
    runner.run(f);
}

fn have_n_threads_acquire_mutex<R: rand::RngCore, F: (Fn() -> R) + Send + Sync>(
    thread_rng: &'static F,
    num_threads: u64,
    modulus: u64,
) {
    let lock = Arc::new(Mutex::new(0u64));
    let threads: Vec<_> = (0..num_threads)
        .map(|_| {
            let my_lock = lock.clone();

            thread::spawn(move || {
                let x = thread_rng().gen::<u64>();

                if x % modulus == 0 {
                    let mut num = my_lock.lock().unwrap();
                    *num += 1;
                }
            })
        })
        .collect();

    threads.into_iter().for_each(|t| t.join().expect("Failed"));
}

#[test]
fn randomly_acquire_lock_shuttle_rand() {
    check_uncontrolled_nondeterminism(
        || have_n_threads_acquire_mutex(&shuttle::rand::thread_rng, 10, 10),
        1000,
    );
}

#[test]
#[should_panic = "possible nondeterminism"]
fn randomly_acquire_lock_regular_rand() {
    check_uncontrolled_nondeterminism(|| have_n_threads_acquire_mutex(&rand::thread_rng, 10, 10), 1000);
}

fn spawn_random_amount_of_threads<R: rand::RngCore, F: (Fn() -> R) + Send + Sync>(
    thread_rng: &'static F,
    max_threads: u64,
) {
    let num_threads: u64 = thread_rng().gen::<u64>() % max_threads;
    have_n_threads_acquire_mutex(&rand::thread_rng, num_threads, 1);
}

#[test]
fn spawn_random_amount_of_threads_shuttle_rand() {
    check_uncontrolled_nondeterminism(|| spawn_random_amount_of_threads(&shuttle::rand::thread_rng, 10), 1000);
}

#[test]
#[should_panic = "possible nondeterminism"]
fn spawn_random_amount_of_threads_regular_rand() {
    check_uncontrolled_nondeterminism(|| spawn_random_amount_of_threads(&rand::thread_rng, 10), 1000);
}

#[test]
fn spawn_random_amount_of_threads_dfs_shuttle_rand() {
    let scheduler = DfsScheduler::new(None, true);
    check_uncontrolled_nondeterminism_custom_scheduler_and_config(
        || spawn_random_amount_of_threads(&shuttle::rand::thread_rng, 2),
        scheduler,
    );
}

#[test]
#[should_panic]
fn spawn_random_amount_of_threads_dfs_regular_rand() {
    for _ in 0..10 {
        let scheduler = DfsScheduler::new(None, true);
        check_uncontrolled_nondeterminism_custom_scheduler_and_config(
            || spawn_random_amount_of_threads(&rand::thread_rng, 10),
            scheduler,
        );
    }
}

fn spawn_random_amount_of_threads_mutex_rng(rng: &Mutex<StdRng>, max_threads: u64) {
    let num_threads = rng.lock().unwrap().gen::<u64>() % max_threads;
    have_n_threads_acquire_mutex(&rand::thread_rng, num_threads, 1);
}

#[test]
#[should_panic = "possible nondeterminism: current execution should have ended"]
fn panic_should_have_ended() {
    let scheduler = DfsScheduler::new(None, true);
    let rng = Mutex::new(StdRng::seed_from_u64(12345));
    check_uncontrolled_nondeterminism_custom_scheduler_and_config(
        move || spawn_random_amount_of_threads_mutex_rng(&rng, 2),
        scheduler,
    );
}

#[test]
#[should_panic = "possible nondeterminism: current execution ended earlier than expected"]
fn panic_ended_earlier() {
    let scheduler = DfsScheduler::new(None, true);
    let rng = Mutex::new(StdRng::seed_from_u64(123));
    check_uncontrolled_nondeterminism_custom_scheduler_and_config(
        move || spawn_random_amount_of_threads_mutex_rng(&rng, 3),
        scheduler,
    );
}

#[test]
#[should_panic = "possible nondeterminism: set of runnable tasks is different than expected"]
fn panic_set_of_runnable() {
    let scheduler = DfsScheduler::new(None, true);
    let rng = Mutex::new(StdRng::seed_from_u64(123));
    check_uncontrolled_nondeterminism_custom_scheduler_and_config(
        move || spawn_random_amount_of_threads_mutex_rng(&rng, 15),
        scheduler,
    );
}

fn make_random_numbers() {
    for _ in 0..10 {
        shuttle::rand::thread_rng().gen::<u64>();
    }
}

#[test]
#[should_panic = "possible nondeterminism: next step was context switch, but recording expected random number generation"]
fn panic_context_switch_when_expecting_rng() {
    let scheduler = DfsScheduler::new(None, true);
    let rng = Mutex::new(StdRng::seed_from_u64(123));
    let modulo = 3;
    check_uncontrolled_nondeterminism_custom_scheduler_and_config(
        move || {
            let test = rng.lock().unwrap().gen::<u64>() % modulo == 0;
            if test {
                have_n_threads_acquire_mutex(&rand::thread_rng, 10, 1);
            } else {
                make_random_numbers();
            }
        },
        scheduler,
    );
}

#[test]
#[should_panic = "possible nondeterminism: next step was random number generation, but recording expected context switch"]
fn panic_rng_when_expecting_context_switch() {
    let scheduler = DfsScheduler::new(None, true);
    let rng = Mutex::new(StdRng::seed_from_u64(123));
    let modulo = 5;
    check_uncontrolled_nondeterminism_custom_scheduler_and_config(
        move || {
            let test = rng.lock().unwrap().gen::<u64>() % modulo == 0;
            if test {
                have_n_threads_acquire_mutex(&rand::thread_rng, 10, 1);
            } else {
                make_random_numbers();
            }
        },
        scheduler,
    );
}

fn have_n_threads_yield(num_threads: u64) {
    let threads: Vec<_> = (0..num_threads)
        .map(|_| {
            thread::spawn(move || {
                thread::yield_now();
            })
        })
        .collect();

    threads.into_iter().for_each(|t| t.join().expect("Failed"));
}

#[test]
#[should_panic = "possible nondeterminism: `next_task` was called with `is_yielding`"]
fn panic_is_yielding() {
    let scheduler = DfsScheduler::new(None, true);
    let rng = Mutex::new(StdRng::seed_from_u64(123));
    let modulo = 5;
    check_uncontrolled_nondeterminism_custom_scheduler_and_config(
        move || {
            let test = rng.lock().unwrap().gen::<u64>() % modulo == 0;
            if test {
                have_n_threads_acquire_mutex(&rand::thread_rng, 10, 1);
            } else {
                have_n_threads_yield(10);
            }
        },
        scheduler,
    );
}

fn iterate_over_hash_set(num_entries: u64) {
    use std::collections::HashSet;
    let hash_set: HashSet<u64> = HashSet::from_iter(0..num_entries);
    for e in hash_set {
        if e % 2 == 0 {
            let _ = shuttle::rand::thread_rng().gen::<u64>();
        } else {
            let _ = thread::spawn(|| {}).join();
        }
    }
}

#[test]
#[should_panic = "possible nondeterminism: next step was"]
fn hashset_without_set_seed() {
    check_uncontrolled_nondeterminism(|| iterate_over_hash_set(10), 1000);
}
