// License: see LICENSE file at root directory of `master` branch

use std::{
    convert::TryFrom,
    io::{Error, ErrorKind},
    sync::{
        Arc,
        atomic::{self, AtomicUsize},
    },
    thread,
    time::{Duration, Instant},
};

use blackhole::{ActiveLimit, BlackHole, Job, OneTime, Result};

const ATOMIC_ORDERING: atomic::Ordering = atomic::Ordering::Relaxed;
const DELAY_AS_MILLIS: u64 = 500;

struct Task {
    index: usize,
    data: Arc<AtomicUsize>,
}

impl Job for Task {

    fn run(&mut self) -> Option<Box<dyn Job>> {
        if self.index == 0 {
            panic!("Test panicking...");
        }
        match self.index % 2 {
            0 => {
                println!("#{} -> {}", self.index, self.data.load(ATOMIC_ORDERING));
                None
            },
            _ => {
                let last = self.data.fetch_add(self.index * 2 - 1, ATOMIC_ORDERING);
                println!("{} + {} = {}", last, self.index, last + self.index);
                thread::sleep(Duration::from_millis(DELAY_AS_MILLIS));
                Some(Box::new(Task { index: self.index + 1, data: self.data.clone() }))
            }
        }
    }

}

#[test]
fn tests() -> Result<()> {
    const ACTIVE_LIMIT: usize = 4;
    const QUEUE_LIMIT: usize = 100;
    const COUNT: usize = ACTIVE_LIMIT * 2 + ACTIVE_LIMIT - 1;

    let black_hole = BlackHole::make(ActiveLimit::try_from(ACTIVE_LIMIT).unwrap(), QUEUE_LIMIT)?;
    let data = Arc::new(AtomicUsize::new(0));
    let start_time = Instant::now();
    for i in 0..=COUNT {
        let data = data.clone();
        black_hole.throw(Task { index: i, data })?;
    }
    black_hole.escape_on_idle()?;

    let expected_max_runtime = Duration::from_millis({
        let active_limit = u64::try_from(ACTIVE_LIMIT).unwrap();
        let left = u64::try_from(COUNT - ACTIVE_LIMIT).unwrap();
        DELAY_AS_MILLIS + left / active_limit * DELAY_AS_MILLIS + match left % active_limit {
            0 => DELAY_AS_MILLIS,
            _ => DELAY_AS_MILLIS * 2
        }
    });
    println!("Expected max runtime: {:?}", &expected_max_runtime);

    let runtime = Instant::now() - start_time;
    println!("Runtime: {:?}", &runtime);

    assert_eq!(data.load(ATOMIC_ORDERING), COUNT * (COUNT + 1) / 2);

    match runtime < expected_max_runtime {
        true => Ok(()),
        false => Err(Error::new(ErrorKind::Other, "::escape_on_idle() took too much time")),
    }
}

#[test]
fn test_get_online_processors() {
    let online_processors = unsafe {
        libc::sysconf(libc::_SC_NPROCESSORS_ONLN)
    };
    assert!(online_processors > 0);
    assert_eq!(unsafe { blackhole::get_online_processors!(None) }, u64::try_from(online_processors).unwrap());
}

#[test]
#[ignore]
fn stressing_black_hole() -> Result<()> {
    const QUEUE_LIMIT: usize = 100_000;
    static mut BYTES: [u8; QUEUE_LIMIT] = [0; QUEUE_LIMIT];

    let black_hole = BlackHole::make(ActiveLimit::try_from(unsafe { blackhole::get_online_processors!(None) }).unwrap(), QUEUE_LIMIT)?;
    for i in 0..QUEUE_LIMIT {
        assert!(black_hole.throw(OneTime::new(move || unsafe { BYTES[i] = 1; }))?.is_none());
    }
    black_hole.escape_on_idle()?;

    unsafe {
        for i in 0..BYTES.len() {
            assert_eq!(&BYTES[i], &1);
        }
    }

    Ok(())
}
