use std::path::Path;
pub use unindent::Unindent;

use super::resources;
use mytest::*;
use rstest_test::{assert_in, assert_not_in, Project, Stringable, TestResults};

const SRC_LIB_RS: &str = {
    const SEP: u8 = std::path::MAIN_SEPARATOR as u8;
    const SRC_LIB_RS: [u8; 11] = [
        SEP, b's', b'r', b'c', SEP, b'l', b'i', b'b', b'.', b'r', b's',
    ];
    unsafe { std::str::from_utf8_unchecked(&SRC_LIB_RS) }
};

fn prj(res: &str) -> Project {
    let path = Path::new("fixture").join(res);
    crate::prj().set_code_file(resources(path))
}

fn run_test(res: &str) -> (std::process::Output, String) {
    let prj = prj(res);
    (
        prj.run_tests().unwrap(),
        prj.get_name().to_owned().to_string(),
    )
}

mod should {
    use rstest_test::{assert_regex, CountMessageOccurrence};

    use super::*;

    #[test]
    fn use_input_fixtures() {
        let (output, _) = run_test("simple_injection.rs");

        TestResults::new().ok("success").fail("fail").assert(output);
    }

    #[test]
    fn create_a_struct_that_return_the_fixture() {
        let (output, _) = run_test("fixture_struct.rs");

        TestResults::new()
            .ok("resolve_new")
            .ok("resolve_default")
            .ok("injected_new")
            .ok("injected_default")
            .assert(output);
    }

    #[test]
    fn be_accessible_from_other_module() {
        let (output, _) = run_test("from_other_module.rs");

        TestResults::new().ok("struct_access").assert(output);
    }

    #[test]
    fn not_show_any_warning() {
        let (output, _) = run_test("no_warning.rs");

        assert_not_in!(output.stderr.str(), "warning:");
    }

    #[test]
    fn rename() {
        let (output, _) = run_test("rename.rs");

        TestResults::new().ok("test").assert(output);
    }

    mod accept_and_return {
        use super::*;

        #[test]
        fn impl_traits() {
            let (output, _) = run_test("impl.rs");

            TestResults::new()
                .ok("base_impl_return")
                .ok("nested_impl_return")
                .ok("nested_multiple_impl_return")
                .ok("base_impl_input")
                .ok("nested_impl_input")
                .ok("nested_multiple_impl_input")
                .assert(output);
        }

        #[test]
        fn dyn_traits() {
            let (output, _) = run_test("dyn.rs");

            TestResults::new()
                .ok("test_dyn_box")
                .ok("test_dyn_ref")
                .ok("test_dyn_box_resolve")
                .ok("test_dyn_ref_resolve")
                .assert(output);
        }
    }

    #[rstest]
    #[case::base("async_fixture.rs")]
    #[case::use_global("await_complete_fixture.rs")]
    #[case::use_selective("await_partial_fixture.rs")]
    fn resolve_async_fixture(#[case] code: &str) {
        let prj = prj(code);
        prj.add_dependency("async-std", r#"{version="*", features=["attributes"]}"#);

        let output = prj.run_tests().unwrap();

        TestResults::new()
            .ok("default_is_async")
            .ok("use_async_fixture")
            .ok("use_async_impl_output")
            .ok("use_async_nest_fixture_default")
            .ok("use_async_nest_fixture_injected")
            .ok("use_async_nest_fixture_with_default")
            .ok("use_two_args_mix_fixture")
            .ok("use_two_args_mix_fixture_inject_first")
            .ok("use_two_args_mix_fixture_inject_both")
            .assert(output);
    }

    #[test]
    fn resolve_fixture_generics_by_fixture_input() {
        let (output, _) = run_test("resolve.rs");

        TestResults::new()
            .ok("test_u32")
            .ok("test_i32")
            .assert(output);
    }

    #[test]
    fn use_defined_return_type_if_any() {
        let (output, _) = run_test("defined_return_type.rs");

        TestResults::new()
            .ok("resolve")
            .ok("resolve_partial")
            .ok("resolve_attrs")
            .ok("resolve_partial_attrs")
            .assert(output);
    }

    #[test]
    fn clean_up_default_from_unused_generics() {
        let (output, _) = run_test("clean_up_default_generics.rs");

        TestResults::new()
            .ok("resolve")
            .ok("resolve_partial")
            .assert(output);
    }

    #[test]
    fn apply_partial_fixture() {
        let (output, _) = run_test("partial.rs");

        TestResults::new()
            .ok("default")
            .ok("t_partial_1")
            .ok("t_partial_2")
            .ok("t_complete")
            .assert(output);
    }

    #[test]
    fn apply_partial_fixture_from_value_attribute() {
        let (output, _) = run_test("partial_in_attr.rs");

        TestResults::new()
            .ok("default")
            .ok("t_partial_1")
            .ok("t_partial_2")
            .ok("t_complete")
            .assert(output);
    }

    #[rstest]
    #[case::compact_form("default.rs")]
    #[case::attrs_form("default_in_attrs.rs")]
    fn use_input_values_if_any(#[case] file: &str) {
        let (output, _) = run_test(file);

        TestResults::new()
            .ok("test_simple")
            .ok("test_simple_changed")
            .ok("test_double")
            .ok("test_double_changed")
            .ok("test_mixed")
            .assert(output);
    }

    #[test]
    fn convert_literal_string_for_default_values() {
        let (output, _) = run_test("default_conversion.rs");

        assert_regex!(
            "Cannot parse 'error' to get [a-z:_0-9]*MyType",
            output.stdout.str()
        );

        TestResults::new()
            .ok("test_base")
            .ok("test_byte_array")
            .ok("test_convert_custom")
            .fail("test_fail_conversion")
            .assert(output);
    }

    #[rstest]
    #[case("once.rs")]
    #[case::no_return("once_no_return.rs")]
    #[case::defined_type("once_defined_type.rs")]
    fn accept_once_attribute_and_call_fixture_just_once(#[case] fname: &str) {
        let project = prj(fname).with_nocapture();

        let output = project.run_tests().unwrap();

        // Just to see the errors if fixture doesn't compile
        assert_in!(output.stderr.str(), "Exec fixture() just once");

        let occurrences = output.stderr.str().count("Exec fixture() just once");

        assert_eq!(1, occurrences);
    }

    mod show_correct_errors {
        use super::*;
        use std::process::Output;

        use rstest::{fixture, rstest};

        #[fixture]
        #[once]
        fn errors_rs() -> (Output, String) {
            run_test("errors.rs")
        }

        #[rstest]
        fn when_cannot_resolve_fixture(errors_rs: &(Output, String)) {
            let (output, name) = errors_rs.clone();

            assert_in!(output.stderr.str(), "error[E0433]: ");
            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                      --> {name}{SRC_LIB_RS}:14:33
                       |
                    14 | fn error_cannot_resolve_fixture(no_fixture: u32) {{"#
                )
                .unindent()
            );
        }

        #[rstest]
        fn on_mismatched_types_inner(errors_rs: &(Output, String)) {
            let (output, name) = errors_rs.clone();

            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error[E0308]: mismatched types
                      --> {name}{SRC_LIB_RS}:10:18
                       |
                    10 |     let a: u32 = "";
                    "#
                )
                .unindent()
            );
        }

        #[rstest]
        fn on_mismatched_types_argument(errors_rs: &(Output, String)) {
            let (output, name) = errors_rs.clone();

            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error[E0308]: mismatched types
                      --> {name}{SRC_LIB_RS}:17:29
                    "#
                )
                .unindent()
            );

            assert_in!(
                output.stderr.str(),
                r#"
                17 | fn error_fixture_wrong_type(fixture: String) {}
                   |                             ^^^^^^"#
                    .unindent()
            );
        }

        #[rstest]
        fn on_invalid_fixture(errors_rs: &(Output, String)) {
            let (output, name) = errors_rs.clone();

            assert_in!(
                output.stderr.str(),
                format!(
                    "
                    error: Missed argument: 'not_a_fixture' should be a test function argument.
                      --> {name}{SRC_LIB_RS}:19:11
                       |
                    19 | #[fixture(not_a_fixture(24))]
                       |           ^^^^^^^^^^^^^
                    "
                )
                .unindent()
            );
        }

        #[rstest]
        fn on_duplicate_fixture_argument(errors_rs: &(Output, String)) {
            let (output, name) = errors_rs.clone();

            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error: Duplicate argument: 'f' is already defined.
                      --> {name}{SRC_LIB_RS}:32:23
                       |
                    32 | #[fixture(f("first"), f("second"))]
                       |                       ^
                    "#
                )
                .unindent()
            );
        }

        #[rstest]
        fn on_destruct_implicit_fixture(errors_rs: &(Output, String)) {
            let (output, name) = errors_rs.clone();

            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error: To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute.
                      --> {name}{SRC_LIB_RS}:48:35
                       |
                    48 | fn error_destruct_without_resolve(T(a): T) {{}}
                       |                                   ^^^^^^^
                    "#
                )
                .unindent()
            );
        }

        #[rstest]
        fn on_destruct_explicit_fixture_without_from(errors_rs: &(Output, String)) {
            let (output, name) = errors_rs.clone();

            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error: To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute.
                      --> {name}{SRC_LIB_RS}:51:57
                       |
                    51 | fn error_destruct_without_resolve_also_with(#[with(21)] T(a): T) {{}}
                       |                                                         ^^^^^^^
                    "#
                )
                .unindent()
            );
            assert_eq!(
                1,
                output.stderr.str().count("51 | fn error_destruct_without")
            )
        }

        #[fixture]
        #[once]
        fn errors_once_rs() -> (Output, String) {
            run_test("errors_once.rs")
        }

        #[rstest]
        fn deny_docs() {
            let (output, _) = run_test("deny_docs.rs");

            assert_not_in!(output.stdout.str(), "missing_docs");

            TestResults::new().ok("success").fail("fail").assert(output);
        }

        #[rstest]
        fn once_async(errors_once_rs: &(Output, String)) {
            let (output, name) = errors_once_rs.clone();
            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error: Cannot apply #[once] to async fixture.
                     --> {name}{SRC_LIB_RS}:4:1
                      |
                    4 | #[once]
                    "#
                )
                .unindent()
            );
        }

        #[rstest]
        fn once_generic_type(errors_once_rs: &(Output, String)) {
            let (output, name) = errors_once_rs.clone();

            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error: Cannot apply #[once] on generic fixture.
                     --> {name}{SRC_LIB_RS}:9:1
                      |
                    9 | #[once]
                    "#
                )
                .unindent()
            );
        }

        #[rstest]
        fn once_generic_impl(errors_once_rs: &(Output, String)) {
            let (output, name) = errors_once_rs.clone();
            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                error: Cannot apply #[once] on generic fixture.
                  --> {name}{SRC_LIB_RS}:15:1
                   |
                15 | #[once]
                "#
                )
                .unindent()
            );
        }

        #[rstest]
        fn once_on_not_sync_type(errors_once_rs: &(Output, String)) {
            let (output, name) = errors_once_rs.clone();
            assert_in!(
                output.stderr.str(),
                format!(
                    r#"
                    error[E0277]: `Cell<u32>` cannot be shared between threads safely
                      --> {name}{SRC_LIB_RS}:20:1
                       |
                    20 | #[fixture]
                       | ^^^^^^^^^^ `Cell<u32>` cannot be shared between threads safely
                    "#
                )
                .unindent(),
            );
        }
    }
}
