//! Tests for task instrumentation.
//!
//! These tests ensure that the instrumentation for task spawning and task
//! lifecycles is correct.
#![warn(rust_2018_idioms)]
#![cfg(all(tokio_unstable, feature = "tracing", target_has_atomic = "64"))]

use std::{mem, time::Duration};

use tokio::task;
use tracing_mock::{expect, span::NewSpan, subscriber};

#[tokio::test]
async fn task_spawn_creates_span() {
    let task_span = expect::span()
        .named("runtime.spawn")
        .with_target("tokio::task");

    let (subscriber, handle) = subscriber::mock()
        .new_span(&task_span)
        .enter(&task_span)
        .exit(&task_span)
        // The task span is entered once more when it gets dropped
        .enter(&task_span)
        .exit(&task_span)
        .drop_span(task_span)
        .run_with_handle();

    {
        let _guard = tracing::subscriber::set_default(subscriber);
        tokio::spawn(futures::future::ready(()))
            .await
            .expect("failed to await join handle");
    }

    handle.assert_finished();
}

#[tokio::test]
async fn task_spawn_loc_file_recorded() {
    let task_span = expect::span()
        .named("runtime.spawn")
        .with_target("tokio::task")
        .with_fields(expect::field("loc.file").with_value(&file!()));

    let (subscriber, handle) = subscriber::mock().new_span(task_span).run_with_handle();

    {
        let _guard = tracing::subscriber::set_default(subscriber);

        tokio::spawn(futures::future::ready(()))
            .await
            .expect("failed to await join handle");
    }

    handle.assert_finished();
}

#[tokio::test]
async fn task_builder_name_recorded() {
    let task_span = expect_task_named("test-task");

    let (subscriber, handle) = subscriber::mock().new_span(task_span).run_with_handle();

    {
        let _guard = tracing::subscriber::set_default(subscriber);
        task::Builder::new()
            .name("test-task")
            .spawn(futures::future::ready(()))
            .unwrap()
            .await
            .expect("failed to await join handle");
    }

    handle.assert_finished();
}

#[tokio::test]
async fn task_builder_loc_file_recorded() {
    let task_span = expect::span()
        .named("runtime.spawn")
        .with_target("tokio::task")
        .with_fields(expect::field("loc.file").with_value(&file!()));

    let (subscriber, handle) = subscriber::mock().new_span(task_span).run_with_handle();

    {
        let _guard = tracing::subscriber::set_default(subscriber);

        task::Builder::new()
            .spawn(futures::future::ready(()))
            .unwrap()
            .await
            .expect("failed to await join handle");
    }

    handle.assert_finished();
}

#[tokio::test]
async fn task_spawn_sizes_recorded() {
    let future = futures::future::ready(());
    let size = mem::size_of_val(&future) as u64;

    let task_span = expect::span()
        .named("runtime.spawn")
        .with_target("tokio::task")
        // TODO(hds): check that original_size.bytes is NOT recorded when this can be done in
        // tracing-mock without listing every other field.
        .with_fields(expect::field("size.bytes").with_value(&size));

    let (subscriber, handle) = subscriber::mock().new_span(task_span).run_with_handle();

    {
        let _guard = tracing::subscriber::set_default(subscriber);

        task::Builder::new()
            .spawn(future)
            .unwrap()
            .await
            .expect("failed to await join handle");
    }

    handle.assert_finished();
}

#[tokio::test]
async fn task_big_spawn_sizes_recorded() {
    let future = {
        async fn big<const N: usize>() {
            let mut a = [0_u8; N];
            for (idx, item) in a.iter_mut().enumerate() {
                *item = (idx % 256) as u8;
            }
            tokio::time::sleep(Duration::from_millis(10)).await;
            for (idx, item) in a.iter_mut().enumerate() {
                assert_eq!(*item, (idx % 256) as u8);
            }
        }

        // This is larger than the release auto-boxing threshold
        big::<20_000>()
    };

    fn boxed_size<T>(_: &T) -> usize {
        mem::size_of::<Box<T>>()
    }
    let size = mem::size_of_val(&future) as u64;
    let boxed_size = boxed_size(&future);

    let task_span = expect::span()
        .named("runtime.spawn")
        .with_target("tokio::task")
        .with_fields(
            expect::field("size.bytes")
                .with_value(&boxed_size)
                .and(expect::field("original_size.bytes").with_value(&size)),
        );

    let (subscriber, handle) = subscriber::mock().new_span(task_span).run_with_handle();

    {
        let _guard = tracing::subscriber::set_default(subscriber);

        task::Builder::new()
            .spawn(future)
            .unwrap()
            .await
            .expect("failed to await join handle");
    }

    handle.assert_finished();
}

/// Expect a task with name
///
/// This is a convenience function to create the expectation for a new task
/// with the `task.name` field set to the provided name.
fn expect_task_named(name: &str) -> NewSpan {
    expect::span()
        .named("runtime.spawn")
        .with_target("tokio::task")
        .with_fields(
            expect::field("task.name").with_value(&tracing::field::debug(format_args!("{name}"))),
        )
}
