// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

chromium::import! {
    "//testing/rust_gtest_interop:gtest_attribute";
}

use std::pin::Pin;

/// Use `prelude:::*` to get access to all macros defined in this crate.
pub mod prelude {
    // The #[extern_test_suite("cplusplus::Type") macro.
    pub use gtest_attribute::extern_test_suite;
    // The #[gtest(TestSuite, TestName)] macro.
    pub use gtest_attribute::gtest;
    // Gtest expectation macros, which should be used to verify test expectations.
    // These replace the standard practice of using assert/panic in Rust tests
    // which would crash the test binary.
    pub use crate::expect_eq;
    pub use crate::expect_false;
    pub use crate::expect_ge;
    pub use crate::expect_gt;
    pub use crate::expect_le;
    pub use crate::expect_lt;
    pub use crate::expect_ne;
    pub use crate::expect_true;
}

// The gtest_attribute proc-macro crate makes use of small_ctor, with a path
// through this crate here to ensure it's available.
#[doc(hidden)]
pub extern crate small_ctor;

/// A marker trait that promises the Rust type is an FFI wrapper around a C++
/// class which subclasses `testing::Test`. In particular, casting a
/// `testing::Test` pointer to the implementing class type is promised to be
/// valid.
///
/// Implement this trait with the `#[extern_test_suite]` macro:
/// ```rs
/// #[extern_test_suite("cpp::type::wrapped::by::Foo")
/// unsafe impl TestSuite for Foo {}
/// ```
#[allow(clippy::missing_safety_doc)] // TODO(https://crbug.com/472355480)
pub unsafe trait TestSuite {
    /// Gives the Gtest factory function on the C++ side which constructs the
    /// C++ class for which the implementing Rust type is an FFI wrapper.
    #[doc(hidden)]
    fn gtest_factory_fn_ptr() -> GtestFactoryFunction;
}

/// Matches the C++ type `rust_gtest_interop::GtestFactoryFunction`, with the
/// `testing::Test` type erased to `OpaqueTestingTest`.
///
/// We replace `testing::Test*` with `OpaqueTestingTest` because but we don't
/// know that C++ type in Rust, as we don't have a Rust generator giving access
/// to that type.
#[doc(hidden)]
pub type GtestFactoryFunction = unsafe extern "C" fn(
    f: extern "C" fn(Pin<&mut OpaqueTestingTest>),
) -> Pin<&'static mut OpaqueTestingTest>;

/// Opaque replacement of a C++ `testing::Test` type, which can only be used as
/// a pointer, since its size is incorrect. Only appears in the
/// GtestFactoryFunction signature, which is a function pointer that passed to
/// C++, and never run from within Rust.
///
/// See https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs
///
/// TODO(danakj): If there was a way, without making references to it into wide
/// pointers, we should make this type be !Sized.
#[repr(C)]
#[doc(hidden)]
pub struct OpaqueTestingTest {
    data: [u8; 0],
    marker: std::marker::PhantomData<(*mut u8, std::marker::PhantomPinned)>,
}

#[doc(hidden)]
pub trait TestResult {
    fn into_error_message(self) -> Option<String>;
}
impl TestResult for () {
    fn into_error_message(self) -> Option<String> {
        None
    }
}
// This impl requires an `Error` not just a `String` so that in the future we
// could print things like the backtrace too (though that field is currently
// unstable).
impl<E: Into<Box<dyn std::error::Error>>> TestResult for std::result::Result<(), E> {
    fn into_error_message(self) -> Option<String> {
        match self {
            Ok(_) => None,
            Err(e) => Some(format!("Test returned error: {}", e.into())),
        }
    }
}

// Internals used by code generated from the gtest-attriute proc-macro. Should
// not be used by human-written code.
#[doc(hidden)]
pub mod __private {
    use super::{GtestFactoryFunction, OpaqueTestingTest, Pin};

    /// Rust wrapper around C++'s rust_gtest_add_failure().
    ///
    /// The wrapper converts the file name into a C++-friendly string,
    /// and the line number into a C++-friendly signed int.
    ///
    /// TODO(crbug.com/40215436): We should be able to receive a C++-friendly
    /// file path.
    ///
    /// TODO(danakj): We should be able to pass a `c_int` directly to C++:
    /// https://github.com/dtolnay/cxx/issues/1015.
    pub fn add_failure_at(file: &'static str, line: u32, message: &str) {
        let null_term_file = std::ffi::CString::new(make_canonical_file_path(file)).unwrap();
        let null_term_message = std::ffi::CString::new(message).unwrap();

        unsafe extern "C" {
            // SAFETY: Both pointers have to be valid, probably
            fn rust_gtest_add_failure_at(
                file: *const std::ffi::c_char,
                line: i32,
                message: *const std::ffi::c_char,
            );
        }

        // SAFETY: Both pointers come from valid `ffi::CString`s.
        unsafe {
            rust_gtest_add_failure_at(
                null_term_file.as_ptr(),
                line.try_into().unwrap_or(-1),
                null_term_message.as_ptr(),
            )
        }
    }

    /// Turn a file!() string for a source file into a path from the root of the
    /// source tree.
    pub fn make_canonical_file_path(file: &str) -> String {
        // The path of the file here is relative to and prefixed with the crate root's
        // source file with the current directory being the build's output
        // directory. So for a generated crate root at gen/foo/, the file path
        // would look like `gen/foo/../../../../real/path.rs`. The last two `../
        // ` move up from the build output directory to the source tree root. As such,
        // we need to strip pairs of `something/../` until there are none left, and
        // remove the remaining `../` path components up to the source tree
        // root.
        //
        // Note that std::fs::canonicalize() does not work here since it requires the
        // file to exist, but we're working with a relative path that is rooted
        // in the build directory, not the current directory. We could try to
        // get the path to the build directory.. but this is simple enough.
        let (keep_rev, _) = std::path::Path::new(file).iter().rev().fold(
            (Vec::new(), 0),
            // Build the set of path components we want to keep, which we do by keeping a count of
            // the `..` components and then dropping stuff that comes before them.
            |(mut keep, dotdot_count), path_component| {
                if path_component == ".." {
                    // The `..` component will skip the next downward component.
                    (keep, dotdot_count + 1)
                } else if dotdot_count > 0 {
                    // Skip the component as we drop it with `..` later in the path.
                    (keep, dotdot_count - 1)
                } else {
                    // Keep this component.
                    keep.push(path_component);
                    (keep, dotdot_count)
                }
            },
        );
        // Reverse the path components, join them together, and write them into a
        // string.
        keep_rev
            .into_iter()
            .rev()
            .fold(std::path::PathBuf::new(), |path, path_component| path.join(path_component))
            .to_string_lossy()
            .to_string()
    }

    unsafe extern "C" {
        /// extern for C++'s rust_gtest_default_factory().
        /// TODO(danakj): We do this by hand because cxx doesn't support passing
        /// raw function pointers: https://github.com/dtolnay/cxx/issues/1011.
        pub fn rust_gtest_default_factory(
            f: extern "C" fn(Pin<&mut OpaqueTestingTest>),
        ) -> Pin<&'static mut OpaqueTestingTest>;

        /// extern for C++'s rust_gtest_add_test().
        ///
        /// Note that the `factory` parameter is actually a C++ function
        /// pointer. TODO(danakj): We do this by hand because cxx
        /// doesn't support passing raw function pointers nor passing `*const c_char`: https://github.com/dtolnay/cxx/issues/1011 and
        /// https://github.com/dtolnay/cxx/issues/1015.
        pub fn rust_gtest_add_test(
            factory: GtestFactoryFunction,
            run_test_fn: extern "C" fn(Pin<&mut OpaqueTestingTest>),
            test_suite_name: *const std::os::raw::c_char,
            test_name: *const std::os::raw::c_char,
            file: *const std::os::raw::c_char,
            line: i32,
        );
    }

    /// Information used to register a function pointer as a test with the C++
    /// Gtest framework.
    pub struct TestRegistration {
        pub func: extern "C" fn(suite: Pin<&mut OpaqueTestingTest>),
        // TODO(danakj): These a C-String-Literals. Maybe we should expose that as a type
        // somewhere.
        pub test_suite_name: &'static [std::os::raw::c_char],
        pub test_name: &'static [std::os::raw::c_char],
        pub file: &'static [std::os::raw::c_char],
        pub line: u32,
        pub factory: GtestFactoryFunction,
    }

    /// Register a given test function with the C++ Gtest framework.
    ///
    /// This function is called from static initializers. It may only be called
    /// from the main thread, before main() is run. It may not panic, or
    /// call anything that may panic.
    pub fn register_test(r: TestRegistration) {
        let line = r.line.try_into().unwrap_or(-1);
        // SAFETY: The `factory` parameter to rust_gtest_add_test() must be a C++
        // function that returns a `testing::Test*` disguised as a
        // `OpaqueTestingTest`. The #[gtest] macro will use
        // `rust_gtest_interop::rust_gtest_default_factory()` by default.
        unsafe {
            rust_gtest_add_test(
                r.factory,
                r.func,
                r.test_suite_name.as_ptr(),
                r.test_name.as_ptr(),
                r.file.as_ptr(),
                line,
            )
        };
    }
}

mod expect_macros;
