#![cfg(target_arch = "wasm32")]

wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

use futures_channel::oneshot;
use js_sys::Promise;
#[cfg(target_feature = "atomics")]
use std::future::Future;
use std::ops::FnMut;
#[cfg(target_feature = "atomics")]
use std::pin::Pin;
#[cfg(target_feature = "atomics")]
use std::task::{Context, Poll};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{future_to_promise, spawn_local, JsFuture};
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
async fn promise_resolve_is_ok_future() {
    let p = js_sys::Promise::resolve(&JsValue::from(42));
    let x = JsFuture::from(p).await.unwrap();
    assert_eq!(x, 42);
}

#[wasm_bindgen_test]
async fn promise_reject_is_error_future() {
    let p = js_sys::Promise::reject(&JsValue::from(42));
    let e = JsFuture::from(p).await.unwrap_err();
    assert_eq!(e, 42);
}

#[wasm_bindgen_test]
async fn ok_future_is_resolved_promise() {
    let p = future_to_promise(async { Ok(JsValue::from(42)) });
    let x = JsFuture::from(p).await.unwrap();
    assert_eq!(x, 42);
}

#[wasm_bindgen_test]
async fn error_future_is_rejected_promise() {
    let p = future_to_promise(async { Err(JsValue::from(42)) });
    let e = JsFuture::from(p).await.unwrap_err();
    assert_eq!(e, 42);
}

#[wasm_bindgen_test]
fn debug_jsfuture() {
    let p = js_sys::Promise::resolve(&JsValue::from(42));
    let f = JsFuture::from(p);
    assert_eq!(&format!("{:?}", f), "JsFuture { ... }");
}

#[wasm_bindgen]
extern "C" {
    fn setTimeout(c: &Closure<dyn FnMut()>);
}

#[cfg(target_feature = "atomics")]
#[wasm_bindgen(module = "/tests/wait_async_mock.js")]
extern "C" {
    #[wasm_bindgen(js_name = installNotifyOnlyWaitAsyncMock)]
    fn install_wait_async_mock() -> JsValue;

    #[wasm_bindgen(js_name = restoreWaitAsyncMock)]
    fn restore_wait_async_mock(original: &JsValue);
}

#[wasm_bindgen_test]
async fn oneshot_works() {
    let (tx, rx) = oneshot::channel::<u32>();
    let mut tx = Some(tx);
    let closure = Closure::wrap(Box::new(move || {
        drop(tx.take().unwrap());
    }) as Box<dyn FnMut()>);
    setTimeout(&closure);
    closure.forget();
    rx.await.unwrap_err();
}

#[wasm_bindgen_test]
async fn spawn_local_runs() {
    let (tx, rx) = oneshot::channel::<u32>();
    spawn_local(async {
        tx.send(42).unwrap();
    });
    assert_eq!(rx.await.unwrap(), 42);
}

#[wasm_bindgen_test]
async fn spawn_local_nested() {
    let (ta, mut ra) = oneshot::channel::<u32>();
    let (ts, rs) = oneshot::channel::<u32>();
    let (tx, rx) = oneshot::channel::<u32>();
    // The order in which the various promises and tasks run is important!
    // We want, on different ticks each, the following things to happen
    // 1. A promise resolves, off of which we can spawn our inbetween assertion
    // 2. The outer task runs, spawns in the inner task, and the inbetween promise, then yields
    // 3. The inbetween promise runs and asserts that the inner task hasn't run
    // 4. The inner task runs
    // This depends crucially on two facts:
    // - JsFuture schedules on ticks independently from tasks
    // - The order of ticks is the same as the code flow
    let promise = Promise::resolve(&JsValue::null());

    spawn_local(async move {
        // Create a closure that runs in between the two ticks and
        // assert that the inner task hasn't run yet
        let inbetween = Closure::wrap(Box::new(move |_| {
            assert_eq!(
                ra.try_recv().unwrap(),
                None,
                "Nested task should not have run yet"
            );
        }) as Box<dyn FnMut(JsValue)>);
        let inbetween = promise.then(&inbetween);
        spawn_local(async {
            ta.send(0xdead).unwrap();
            ts.send(0xbeaf).unwrap();
        });
        JsFuture::from(inbetween).await.unwrap();
        assert_eq!(
            rs.await.unwrap(),
            0xbeaf,
            "Nested task should run eventually"
        );
        tx.send(42).unwrap();
    });

    assert_eq!(rx.await.unwrap(), 42);
}

#[wasm_bindgen_test]
async fn spawn_local_err_no_exception() {
    let (tx, rx) = oneshot::channel::<u32>();
    spawn_local(async {});
    spawn_local(async {
        tx.send(42).unwrap();
    });
    let val = rx.await.unwrap();
    assert_eq!(val, 42);
}

#[wasm_bindgen_test]
async fn can_create_multiple_futures_from_same_promise() {
    let promise = js_sys::Promise::resolve(&JsValue::null());
    let a = JsFuture::from(promise.clone());
    let b = JsFuture::from(promise);

    a.await.unwrap();
    b.await.unwrap();
}

#[cfg(target_feature = "atomics")]
struct WaitAsyncMockGuard {
    original: JsValue,
}

#[cfg(target_feature = "atomics")]
impl Drop for WaitAsyncMockGuard {
    fn drop(&mut self) {
        restore_wait_async_mock(&self.original);
    }
}

#[cfg(target_feature = "atomics")]
#[derive(Default)]
struct PendingThenReady {
    polled: bool,
}

#[cfg(target_feature = "atomics")]
impl Future for PendingThenReady {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.polled {
            Poll::Ready(())
        } else {
            self.polled = true;
            Poll::Pending
        }
    }
}

// Reproduces a race condition where the waitAsync
// promise resolves without first transitioning the task state back to AWAKE.
// Without the fix this panics inside `Task::run` because it observes the
// `SLEEPING` state on entry.
#[cfg(target_feature = "atomics")]
#[wasm_bindgen_test(async)]
async fn wait_async_promise_callback_runs_without_wake() {
    let _guard = WaitAsyncMockGuard {
        original: install_wait_async_mock(),
    };

    let (done_tx, done_rx) = oneshot::channel::<()>();
    spawn_local(async move {
        PendingThenReady::default().await;
        done_tx.send(()).ok();
    });

    done_rx.await.expect("task finished");
}

#[cfg(feature = "futures-core-03-stream")]
#[wasm_bindgen_test]
async fn can_use_an_async_iterable_as_stream() {
    use futures_lite::stream::StreamExt;
    use wasm_bindgen_futures::stream::JsStream;

    let async_iter = js_sys::Function::new_no_args(
        "return async function*() { 
            yield 42;
            yield 24;
        }()",
    )
    .call0(&JsValue::undefined())
    .unwrap()
    .unchecked_into::<js_sys::AsyncIterator>();

    let mut stream = JsStream::from(async_iter);
    assert_eq!(stream.next().await, Some(Ok(JsValue::from(42))));
    assert_eq!(stream.next().await, Some(Ok(JsValue::from(24))));
    assert_eq!(stream.next().await, None);
}

#[wasm_bindgen_test]
#[should_panic]
async fn should_panic() {
    panic!()
}

#[wasm_bindgen_test]
#[should_panic = "error message"]
async fn should_panic_string() {
    panic!("error message")
}

#[wasm_bindgen_test]
#[should_panic(expected = "error message")]
async fn should_panic_expected() {
    panic!("error message")
}

#[wasm_bindgen_test]
#[ignore]
async fn ignore() {
    panic!("this test should have been ignored")
}

#[wasm_bindgen_test]
#[ignore = "reason"]
async fn ignore_reason() {
    panic!("this test should have been ignored")
}
