//! Example: tests for the timing features in tor-rtcompat.

use tor_rtcompat::test_with_all_runtimes;
use tor_rtcompat::{SleepProvider, SleepProviderExt, Timeout, TimeoutError};

use tor_rtmock::time::MockSleepProvider;

use futures::channel::oneshot;
use futures::FutureExt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime};

#[test]
fn timeouts() {
    fn setup() -> (
        MockSleepProvider,
        oneshot::Sender<()>,
        Timeout<oneshot::Receiver<()>, tor_rtmock::time::Sleeping>,
    ) {
        let start = SystemTime::now();
        let (send, recv) = oneshot::channel::<()>();
        let mock_sp = MockSleepProvider::new(start);
        let ten_min = Duration::new(10 * 60, 0);
        let timeout_future = mock_sp.timeout(ten_min, recv);
        (mock_sp, send, timeout_future)
    }

    // The timeout occurs.
    test_with_all_runtimes!(|_| async {
        let (mock_sp, _send, timeout_future) = setup();
        mock_sp.advance(Duration::new(3600, 0)).await;
        assert!(matches!(timeout_future.await, Err(TimeoutError)));
    });
    // The data is ready immediately.
    test_with_all_runtimes!(|_| async {
        let (_, send, timeout_future) = setup();
        send.send(()).unwrap();
        assert_eq!(timeout_future.await, Ok(Ok(())));
    });
    // The data is ready after a little while
    test_with_all_runtimes!(|_| async {
        let (mock_sp, send, timeout_future) = setup();
        mock_sp.advance(Duration::new(10, 0)).await;
        send.send(()).unwrap();
        assert_eq!(timeout_future.await, Ok(Ok(())));
    });
    // The data is ready _and_ the timeout occurs.
    test_with_all_runtimes!(|_| async {
        let (mock_sp, send, timeout_future) = setup();
        send.send(()).unwrap();
        mock_sp.advance(Duration::new(3600, 0)).await;
        assert_eq!(timeout_future.await, Ok(Ok(())));
    });
    // Make sure that nothing happens too early.
    test_with_all_runtimes!(|_| async {
        let (mock_sp, _send, timeout_future) = setup();
        mock_sp.advance(Duration::new(300, 0)).await;
        assert_eq!(timeout_future.now_or_never(), None);
    });
}

fn start() -> SystemTime {
    // Yes this is a homestuck reference
    SystemTime::UNIX_EPOCH + Duration::from_secs(1239657180)
}
const ONE_DAY: Duration = Duration::from_secs(86400);

#[test]
fn wallclock_simple() {
    // Simple case: time goes by.
    test_with_all_runtimes!(|_| async {
        let mock_sp = MockSleepProvider::new(start());
        let b = AtomicBool::new(false);
        futures::join!(
            async {
                mock_sp.sleep_until_wallclock(start() + ONE_DAY).await;
                b.store(true, Ordering::SeqCst);
            },
            async {
                while mock_sp.wallclock() < start() + ONE_DAY {
                    assert!(!b.load(Ordering::SeqCst));
                    mock_sp.advance(Duration::new(413, 0)).await;
                }
            }
        );
        assert!(b.load(Ordering::SeqCst));
    });
}

#[test]
fn wallclock_early() {
    // Simple case 2: time goes by, but not enough of it.
    test_with_all_runtimes!(|_| async {
        let mock_sp = MockSleepProvider::new(start());
        let b = AtomicBool::new(false);
        let (send, mut recv) = oneshot::channel();
        futures::join!(
            async {
                let mut sleep = mock_sp.sleep_until_wallclock(start() + ONE_DAY).fuse();
                futures::select! {
                    _ = sleep => b.store(true, Ordering::SeqCst),
                    _ = recv => (),
                };
            },
            async {
                while mock_sp.wallclock() < start() + (ONE_DAY / 2) {
                    assert!(!b.load(Ordering::SeqCst));
                    mock_sp.advance(Duration::new(413, 0)).await;
                }
                send.send(()).unwrap();
            }
        );
        assert!(!b.load(Ordering::SeqCst));
    });
}

#[test]
fn wallclock_jump_forward() {
    // Clock jumps forward, so event triggers.
    test_with_all_runtimes!(|_| async {
        let mock_sp = MockSleepProvider::new(start());
        let b = AtomicBool::new(false);
        let i1 = mock_sp.now();
        futures::join!(
            async {
                mock_sp.sleep_until_wallclock(start() + ONE_DAY).await;
                b.store(true, Ordering::SeqCst);
            },
            async {
                mock_sp.jump_to(start() + ONE_DAY);
                mock_sp.advance(Duration::new(1000, 0)).await; // have to rest some.
            }
        );
        assert!(b.load(Ordering::SeqCst));
        let i2 = mock_sp.now();
        assert!(i2 - i1 < ONE_DAY);
    });
}

#[test]
fn wallclock_jump_backwards() {
    // Clock jumps backward, so event does not trigger early.
    test_with_all_runtimes!(|_| async {
        let mock_sp = MockSleepProvider::new(start());
        let b = AtomicBool::new(false);
        let (send, mut recv) = oneshot::channel();
        let i1 = mock_sp.now();
        futures::join!(
            async {
                let mut sleep = mock_sp.sleep_until_wallclock(start() + ONE_DAY).fuse();
                futures::select! {
                    _ = sleep => b.store(true, Ordering::SeqCst),
                    _ = recv => (),
                };
            },
            async {
                mock_sp.jump_to(start() - ONE_DAY);
                let mut elapsed = Duration::new(0, 0);
                while elapsed < (3 * ONE_DAY) / 2 {
                    assert!(!b.load(Ordering::SeqCst));
                    mock_sp.advance(Duration::new(413, 0)).await;
                    elapsed += Duration::new(413, 0);
                }
                send.send(()).unwrap();
            }
        );
        assert!(!b.load(Ordering::SeqCst));
        let i2 = mock_sp.now();
        assert!(i2 - i1 > ONE_DAY);
        assert!(mock_sp.wallclock() < start() + ONE_DAY);
    });
}
