use std::mem;
use std::panic::catch_unwind;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use std::task::{Poll, Waker};

use async_executor::{Executor, Task};
use futures_lite::future;
use once_cell::sync::Lazy;

#[test]
fn executor_cancels_everything() {
    static DROP: AtomicUsize = AtomicUsize::new(0);
    static WAKER: Lazy<Mutex<Option<Waker>>> = Lazy::new(|| Default::default());

    let ex = Executor::new();

    let task = ex.spawn(async {
        let _guard = CallOnDrop(|| {
            DROP.fetch_add(1, Ordering::SeqCst);
        });

        future::poll_fn(|cx| {
            *WAKER.lock().unwrap() = Some(cx.waker().clone());
            Poll::Pending::<()>
        })
        .await;
    });

    future::block_on(ex.tick());
    assert!(WAKER.lock().unwrap().is_some());
    assert_eq!(DROP.load(Ordering::SeqCst), 0);

    drop(ex);
    assert_eq!(DROP.load(Ordering::SeqCst), 1);

    assert!(catch_unwind(|| future::block_on(task)).is_err());
    assert_eq!(DROP.load(Ordering::SeqCst), 1);
}

#[test]
fn leaked_executor_leaks_everything() {
    static DROP: AtomicUsize = AtomicUsize::new(0);
    static WAKER: Lazy<Mutex<Option<Waker>>> = Lazy::new(|| Default::default());

    let ex = Executor::new();

    let task = ex.spawn(async {
        let _guard = CallOnDrop(|| {
            DROP.fetch_add(1, Ordering::SeqCst);
        });

        future::poll_fn(|cx| {
            *WAKER.lock().unwrap() = Some(cx.waker().clone());
            Poll::Pending::<()>
        })
        .await;
    });

    future::block_on(ex.tick());
    assert!(WAKER.lock().unwrap().is_some());
    assert_eq!(DROP.load(Ordering::SeqCst), 0);

    mem::forget(ex);
    assert_eq!(DROP.load(Ordering::SeqCst), 0);

    assert!(future::block_on(future::poll_once(task)).is_none());
    assert_eq!(DROP.load(Ordering::SeqCst), 0);
}

#[test]
fn await_task_after_dropping_executor() {
    let s: String = "hello".into();

    let ex = Executor::new();
    let task: Task<&str> = ex.spawn(async { &*s });
    assert!(ex.try_tick());

    drop(ex);
    assert_eq!(future::block_on(task), "hello");
    drop(s);
}

#[test]
fn drop_executor_and_then_drop_finished_task() {
    static DROP: AtomicUsize = AtomicUsize::new(0);

    let ex = Executor::new();
    let task = ex.spawn(async {
        CallOnDrop(|| {
            DROP.fetch_add(1, Ordering::SeqCst);
        })
    });
    assert!(ex.try_tick());

    assert_eq!(DROP.load(Ordering::SeqCst), 0);
    drop(ex);
    assert_eq!(DROP.load(Ordering::SeqCst), 0);
    drop(task);
    assert_eq!(DROP.load(Ordering::SeqCst), 1);
}

#[test]
fn drop_finished_task_and_then_drop_executor() {
    static DROP: AtomicUsize = AtomicUsize::new(0);

    let ex = Executor::new();
    let task = ex.spawn(async {
        CallOnDrop(|| {
            DROP.fetch_add(1, Ordering::SeqCst);
        })
    });
    assert!(ex.try_tick());

    assert_eq!(DROP.load(Ordering::SeqCst), 0);
    drop(task);
    assert_eq!(DROP.load(Ordering::SeqCst), 1);
    drop(ex);
    assert_eq!(DROP.load(Ordering::SeqCst), 1);
}

struct CallOnDrop<F: Fn()>(F);

impl<F: Fn()> Drop for CallOnDrop<F> {
    fn drop(&mut self) {
        (self.0)();
    }
}
