//! (Integration) tests _via_ doctests so that each one runs within its own isolated process,
//! thereby preventing misinterations between the different logger global states.
#![allow(dead_code, unused_imports, clippy::all, deprecated)]

use apply as test_isolated;

#[rustfmt::skip]
macro_rules! set_doctest_prelude {
    (
        [$_:tt] $($prelude:tt)*
    ) => (
        $($prelude)*

        macro_rules! doctest {(
            $_(# $attr:tt)*
        $_($_(@$if_async:tt)?
            async
        )?
            fn $fname:ident() {
                $_($body:tt)*
            }
        ) => (
            $_(#$attr)*
            /// ```rust
            #[doc = stringify! {
                $($prelude)*

            $_($_($if_async)?
                let fut = async
            )?
                { $_($body)* }
            $_($_($if_async)?
                ;
                ::futures::executor::block_on(fut);
            )?
            }]
            /// ```
            mod $fname {}
        )}

        #[allow(unused_macros)]
        macro_rules! doctest_ignore {(
            $_(# $attr:tt)*
        $_($_(@$if_async:tt)?
            async
        )?
            fn $fname:ident() {
                $_($body:tt)*
            }
        ) => (
            $_(#$attr)*
            /// ```ignore
            #[doc = stringify! {
                $($prelude)*

            $_($_($if_async)?
                let fut = async
            )?
                { $_($body)* }
            $_($_($if_async)?
                ;
                ::futures::executor::block_on(fut);
            )?
            }]
            /// ```
            mod $fname {}
        )}
    );

    (
        $($prelude:tt)*
    ) => (
        set_doctest_prelude! { [$] $($prelude)* }
    );
}

set_doctest_prelude! {
    use dittolive_ditto::{
        error::{CoreApiErrorKind, ErrorKind},
        prelude::*,
    };

    /// Returns a "dummy" ditto instance with no license.
    fn init_ditto() -> Ditto {
        let ditto = Ditto::builder()
            .with_temp_dir()
            .with_identity(identity::OfflinePlayground::random).unwrap()
            .build().unwrap();
        ditto
    }

    fn init_ditto_in(path: &::std::path::Path) -> Ditto {
        let ditto = Ditto::builder()
            .with_root(::std::sync::Arc::new(PersistentRoot::new(path).unwrap()))
            .with_identity(identity::OfflinePlayground::random).unwrap()
            .build().unwrap();
        ditto
    }

    fn tempdir() -> ::tempfile::TempDir {
        ::tempfile::TempDir::new().expect("tempdir() to succeed")
    }

    fn read_gz_contents(path: impl AsRef<::std::path::Path>) -> ::std::io::Result<String> {
        let mut gz = ::flate2::read::GzDecoder::new(::std::fs::File::open(path)?);
        let mut s = String::new();
        ::std::io::Read::read_to_string(&mut gz, &mut s)?;
        Ok(s)
    }
}

/// Basic smoke test for log exports.
///
/// This test injects two log statements into Ditto's core logs, exports the
/// core logs into a compressed file, and validates that our injected logs are
/// present.
#[test_isolated(doctest)]
async fn export() {
    use uuid::Uuid;

    let _d = init_ditto();
    let tempdir = &tempdir();
    let path = &tempdir.path().join("exported-logs.jsonl.gz");

    // Generate unique entries which we'll inject into the logs. We don't
    // have control over the contents of the system logs since they're not a
    // stable API and are constantly evolving as we add/remove/tweak logs
    // internally. By using distinct needles to search our logging haystack
    // we remain resilient to other changes in the underlying logs.
    let needle_1 = Uuid::new_v4().to_string();
    let needle_2 = Uuid::new_v4().to_string();

    DittoLogger::__log_error(needle_1.as_str());
    DittoLogger::__log_error(needle_2.as_str());

    let _ = DittoLogger::export_to_file(path).await.unwrap();
    let data = read_gz_contents(path).unwrap();
    let lines = data.lines().collect::<Vec<_>>();

    // Asserting on lines is safe given that our log file are inherently
    // json lines. Any other test (e.g. number of bytes in the file) would be
    // to brittle and subject to future failure under different compression
    // circumstances, changes to underlying log messages, etc.
    // We must also test for 2 or more lines, rather than exactly 2. We are not
    // the only source of logs, and a system log may be written even at error
    // level.
    assert!(
        lines.len() >= 2,
        "exported logs should have contained at least our 2 log lines"
    );
    assert!(lines.iter().any(|line| line.contains(needle_1.as_str())));
    assert!(lines.iter().any(|line| line.contains(needle_2.as_str())));
}

#[test_isolated(doctest_ignore)]
async fn export_writes_empty_gzipped_file_if_no_logs_exist() {
    panic!("No practical way to test this at the moment");
}

#[test_isolated(doctest)]
async fn export_writes_all_logs_of_all_ditto_instances_since_the_last_instance_has_been_created() {
    use uuid::Uuid;

    let blah_a = format!("Blah A {}", Uuid::new_v4());

    let _ditto_a = init_ditto();
    DittoLogger::__log_error(&blah_a);

    let _ditto_b = init_ditto();
    let blah_b = format!("Blah B {}", Uuid::new_v4());
    DittoLogger::__log_error(&blah_b);

    let _ditto_c = init_ditto();
    let blah_c = format!("Blah C {}", Uuid::new_v4());
    DittoLogger::__log_error(&blah_c);

    let blub_a = format!("Blub A {}", Uuid::new_v4());
    let blub_b = format!("Blub B {}", Uuid::new_v4());
    let blub_c = format!("Blub C {}", Uuid::new_v4());
    DittoLogger::__log_error(&blub_a);
    DittoLogger::__log_error(&blub_b);
    DittoLogger::__log_error(&blub_c);

    let tempdir = tempdir();
    let path = tempdir.path().join("exported-logs.jsonl.gz");

    let number_of_bytes_written = DittoLogger::export_to_file(&path).await.unwrap() as usize;
    let raw_data = std::fs::read(&path).unwrap();
    assert_eq!(number_of_bytes_written, raw_data.len());

    let data = read_gz_contents(&path).unwrap();
    assert!(data.contains(&blah_c));
    assert!(data.contains(&blub_a));
    assert!(data.contains(&blub_b));
    assert!(data.contains(&blub_c));
}

#[test_isolated(doctest)]
async fn export_writes_all_logs_of_all_ditto_instances_even_if_the_last_instance_has_been_closed() {
    use uuid::Uuid;

    // NOTE: Due to current limitations, the logger keeps writing to the
    // persistence directory of the last Ditto instance, even if that has
    // been closed or went out of scope. This is a special test covering
    // this "suboptimal" behavior, remove it once this behavior has been
    // fixed.

    // We have to manually handle the persistence directories so they outlive
    // the ditto instances (by default `init_ditto()` auto-cleans-up that dir as
    // soon as ditto dies).
    let [a, b, c] = &::core::array::from_fn(|_| tempdir());
    let ditto_a = init_ditto_in(a.path());
    let blah_a = format!("Blah A {}", Uuid::new_v4());
    DittoLogger::__log_error(&blah_a);

    let ditto_b = init_ditto_in(b.path());
    let blah_b = format!("Blah B {}", Uuid::new_v4());
    DittoLogger::__log_error(&blah_b);

    let ditto_c = init_ditto_in(c.path());
    let blah_c = format!("Blah C {}", Uuid::new_v4());
    DittoLogger::__log_error(&blah_c);

    let blub_c = format!("Blub C {}", Uuid::new_v4());
    DittoLogger::__log_error(&blub_c);
    ditto_c.close();

    let blub_b = format!("Blub B {}", Uuid::new_v4());
    DittoLogger::__log_error(&blub_b);
    ditto_b.close();

    let blub_a = format!("Blub A {}", Uuid::new_v4());
    DittoLogger::__log_error(&blub_a);
    ditto_a.close();

    let tempdir = tempdir();
    let path = tempdir.path().join("exported-logs.jsonl.gz");

    let number_of_bytes_written = DittoLogger::export_to_file(&path).await.unwrap() as usize;
    let raw_data = std::fs::read(&path).unwrap();
    assert_eq!(number_of_bytes_written, raw_data.len());

    let data = read_gz_contents(path).unwrap();
    let log_lines = data.lines().collect::<Vec<_>>();
    assert!(data.contains(&blah_c));
    assert!(data.contains(&blub_c));
    assert!(data.contains(&blub_b));
    assert!(data.contains(&blub_a));
}

#[test_isolated(doctest)]
async fn export_throws_io_not_found_when_last_ditto_instance_created_is_closed_again_and_persistence_directory_removed_but_others_exist(
) {
    // NOTE: Due to current limitations, the logger keeps writing to the
    // persistence directory of the last Ditto instance, even if that has
    // been closed or went out of scope. This is a special test covering
    // this "suboptimal" behavior, remove it once this behavior has been
    // fixed.

    // We have to manually handle the persistence directories so they outlive
    // the ditto instances (by default `init_ditto()` auto-cleans-up that dir as
    // soon as ditto dies).
    let [a, b] = &::core::array::from_fn(|_| tempdir());
    let ditto_a = init_ditto_in(a.path());
    DittoLogger::__log_error("Blah A");

    let ditto_b = init_ditto_in(b.path());
    DittoLogger::__log_error("Blah B");

    // persistence directory auto-destroys when `.close()`d!
    let ditto_c = init_ditto();
    DittoLogger::__log_error("Blah C");

    DittoLogger::__log_error("Blub C");
    ditto_c.close(); // destroys persistence dir.

    DittoLogger::__log_error("Blub B");
    ditto_b.close();

    DittoLogger::__log_error("Blub A");
    ditto_a.close();

    let tempdir = &tempdir();
    let path = &tempdir.path().join("exported-logs.jsonl.gz");

    let result = DittoLogger::export_to_file(path).await;
    assert!(
        matches!(
            &result,
            Err(err) if err.kind() == ErrorKind::CoreApi(CoreApiErrorKind::IoNotFound)
        ),
        "expected `CoreApiErrorKind::IoAlreadyExists`, got {result:?}",
    );
}

#[test_isolated(doctest)]
async fn export_throws_io_error_already_exists() {
    let _d = init_ditto();
    let tempdir = &tempdir();
    let path = &tempdir.path().join("exported-logs.jsonl.gz");

    DittoLogger::export_to_file(path).await.unwrap();
    let result = DittoLogger::export_to_file(path).await;
    assert!(
        matches!(
            &result,
            Err(err) if err.kind() == ErrorKind::CoreApi(CoreApiErrorKind::IoAlreadyExists)
        ),
        "expected `CoreApiErrorKind::IoAlreadyExists`, got {result:?}",
    );
}

#[test_isolated(doctest)]
async fn export_throws_io_error_not_found() {
    let _d = init_ditto();
    let tempdir = &tempdir();
    let path = &tempdir
        .path()
        .join("travolta-pulp-fiction")
        .join("exported-logs.jsonl.gz");

    let result = DittoLogger::export_to_file(path).await;
    assert!(
        matches!(
            &result,
            Err(err) if err.kind() == ErrorKind::CoreApi(CoreApiErrorKind::IoNotFound)
        ),
        "expected `CoreApiErrorKind::IoNotFound`, got {result:?}",
    );
}

#[cfg(unix)]
#[test_isolated(doctest)]
async fn export_throws_io_error_permission_denied() {
    use std::{fs::Permissions, os::unix::fs::PermissionsExt};

    let _d = init_ditto();
    let tempdir = &tempdir();

    ::std::fs::set_permissions(tempdir.path(), Permissions::from_mode(0o000))
        .expect("chmod to succeed");

    let path = &tempdir
        .path()
        .join("travolta-pulp-fiction")
        .join("exported-logs.jsonl.gz");

    let result = DittoLogger::export_to_file(path).await;
    assert!(
        matches!(
            &result,
            Err(err) if err.kind() == ErrorKind::CoreApi(CoreApiErrorKind::IoPermissionDenied)
        ),
        "expected `CoreApiErrorKind::IoPermissionDenied`, got {result:?}",
    );
}

#[test_isolated(doctest)]
async fn export_throws_io_error_operation_failed_if_directory_along_path_is_a_file() {
    let _d = init_ditto();
    let tempdir = &tempdir();
    let path = &tempdir.path().join("exported-logs.jsonl.gz");

    DittoLogger::export_to_file(path).await.unwrap();
    let result = DittoLogger::export_to_file(&path.join("exported-logs.jsonl.gz")).await;
    assert!(
        matches!(
            &result,
            Err(err) if err.kind() == ErrorKind::CoreApi(CoreApiErrorKind::IoOperationFailed)
        ),
        "expected `CoreApiErrorKind::IoOperationFailed`, got {result:?}",
    );
}

#[test_isolated(doctest)]
async fn export_works_with_deepish_directory_paths_and_longish_file_names() {
    let _d = init_ditto();
    let tempdir = &tempdir();

    DittoLogger::__log_error("Blah");
    DittoLogger::__log_error("Blub");

    let mut path = tempdir.path().to_owned();

    let long_directory_name = &format!("deep-recursive-directory-{}", "x".repeat(28));
    (0..12).for_each(|_| path.push(long_directory_name));
    ::std::fs::create_dir_all(&path).expect("`mkdir -p` to succeed");

    let long_file_name = format!("exported-logs-{}.jsonl.gz", "x".repeat(28));
    path.push(long_file_name);

    let number_of_bytes_written = DittoLogger::export_to_file(&path).await.unwrap() as usize;
    let raw_data = std::fs::read(&path).unwrap();
    assert_eq!(number_of_bytes_written, raw_data.len());

    let data = read_gz_contents(path).unwrap();
    assert!(data.contains("Blah"));
    assert!(data.contains("Blub"));
}

#[test_isolated(doctest)]
async fn export_throws_io_error_operation_failed_if_file_name_is_too_long() {
    let _d = init_ditto();
    let tempdir = &tempdir();

    let mut path = tempdir.path().to_owned();

    let long_directory_name = &format!("deep-recursive-directory-{}", "x".repeat(28));
    (0..12).for_each(|_| path.push(long_directory_name));
    ::std::fs::create_dir_all(&path).expect("`mkdir -p` to succeed");

    let long_file_name = format!("exported-logs-{}.jsonl.gz", "x".repeat(256));
    path.push(long_file_name);

    let result = DittoLogger::export_to_file(&path).await;
    assert!(
        matches!(
            &result,
            Err(err) if err.kind() == ErrorKind::CoreApi(CoreApiErrorKind::IoOperationFailed)
        ),
        "expected `CoreApiErrorKind::IoOperationFailed`, got {result:?}",
    );
}

#[test_isolated(doctest)]
async fn export_throws_unknown_error_if_no_ditto_instance_has_been_created_within_process_yet() {
    let tempdir = &tempdir();
    let result = DittoLogger::export_to_file(&tempdir.path().join("exported-logs.jsonl.gz")).await;
    assert!(
        result.is_err(),
        "expected `CoreApiErrorKind::Unknown`, got {result:?}",
    );
}
