/// Because the `TryFrom` implementations don't return proper errors
/// on failure
#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("")]
    TryFromError,
}

/// Needed for roundtripping object types that take a `hash_kind` parameter.
/// This is the same as `round_trip`, but for types that have `from_bytes()` with `hash_kind`.
macro_rules! round_trip_with_hash_kind {
    ($owned:ty, $borrowed:ty, $( $files:literal ), +) => {
        #[test]
        fn round_trip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
            use std::convert::TryFrom;
            use std::io::Write;
            use crate::object_fixture;
            use gix_object::{ObjectRef, Object, WriteTo};
            use bstr::ByteSlice;
            let hash_kind = crate::fixture_hash_kind();

            for input_name in &[
                $( $files ),*
            ] {
                let input = object_fixture(input_name)?;
                // Test the parse->borrowed->owned->write chain for an object kind
                let mut output = Vec::new();
                let item = <$borrowed>::from_bytes(&input, hash_kind)?;
                item.write_to(&mut output)?;
                assert_eq!(output.as_bstr(), input.as_bstr(), "borrowed: {input_name}");

                let item: $owned = item.try_into()?;
                output.clear();
                item.write_to(&mut output)?;
                assert_eq!(output.as_bstr(), input.as_bstr());

                // Test the parse->borrowed->owned->write chain for the top-level objects
                let item = ObjectRef::from(<$borrowed>::from_bytes(&input, hash_kind)?);
                output.clear();
                item.write_to(&mut output)?;
                assert_eq!(output.as_bstr(), input.as_bstr(), "object-ref");

                let item: Object = Object::try_from(item)?;
                output.clear();
                item.write_to(&mut output)?;
                assert_eq!(output.as_bstr(), input.as_bstr(), "owned");

                // Test the loose serialisation -> parse chain for an object kind
                let item = <$borrowed>::from_bytes(&input, hash_kind)?;
                // serialise a borowed item to a tagged loose object
                output.clear();
                {
                    let w = &mut output;
                    w.write_all(&item.loose_header())?;
                    item.write_to(w)?;
                    let parsed = ObjectRef::from_loose(&output, hash_kind)?;
                    let item2 = <$borrowed>::try_from(parsed).or(Err(super::Error::TryFromError))?;
                    assert_eq!(item2, item, "object-ref loose: {input_name} {:?}\n{:?}", output.as_bstr(), input.as_bstr());
                }

                let item: $owned = item.try_into()?;
                // serialise an owned to a tagged loose object
                output.clear();
                let w = &mut output;
                w.write_all(&item.loose_header())?;
                item.write_to(w)?;
                let parsed = ObjectRef::from_loose(&output, hash_kind)?;
                let parsed_borrowed = <$borrowed>::try_from(parsed).or(Err(super::Error::TryFromError))?;
                let item2: $owned = parsed_borrowed.try_into().or(Err(super::Error::TryFromError))?;
                assert_eq!(item2, item, "object-ref loose owned: {input_name} {:?}\n{:?}", output.as_bstr(), input.as_bstr());
            }
            Ok(())
        }
    };
}

mod tag {
    round_trip_with_hash_kind!(
        gix_object::Tag,
        gix_object::TagRef,
        "tag/empty_missing_nl.txt",
        "tag/empty.txt",
        "tag/no-tagger.txt",
        "tag/whitespace.txt",
        "tag/with-newlines.txt",
        "tag/signed.txt"
    );
}

mod commit {
    round_trip_with_hash_kind!(
        gix_object::Commit,
        gix_object::CommitRef,
        "commit/email-with-space.txt",
        "commit/signed-whitespace.txt",
        "commit/two-multiline-headers.txt",
        "commit/mergetag.txt",
        "commit/merge.txt",
        "commit/signed.txt",
        "commit/signed-singleline.txt",
        "commit/signed-with-encoding.txt",
        "commit/unsigned.txt",
        "commit/whitespace.txt",
        "commit/with-encoding.txt",
        "commit/subtle.txt"
    );
}

mod tree {
    use gix_object::{tree, tree::EntryKind, WriteTo};

    #[test]
    fn write_to_does_not_validate() {
        let hash_kind = crate::fixture_hash_kind();
        let mut tree = gix_object::Tree::empty();
        tree.entries.push(tree::Entry {
            mode: EntryKind::Blob.into(),
            filename: "".into(),
            oid: hash_kind.null(),
        });
        tree.entries.push(tree::Entry {
            mode: EntryKind::Tree.into(),
            filename: "something\nwith\newlines\n".into(),
            oid: hash_kind.empty_tree(),
        });
        tree.write_to(&mut std::io::sink())
            .expect("write succeeds, no validation is performed");
    }

    #[test]
    fn write_to_does_not_allow_separator() {
        let hash_kind = crate::fixture_hash_kind();
        let mut tree = gix_object::Tree::empty();
        tree.entries.push(tree::Entry {
            mode: EntryKind::Blob.into(),
            filename: "hi\0ho".into(),
            oid: hash_kind.null(),
        });
        let err = tree.write_to(&mut std::io::sink()).unwrap_err();
        assert_eq!(
            err.to_string(),
            r#"Nullbytes are invalid in file paths as they are separators: "hi\0ho""#
        );
    }

    round_trip_with_hash_kind!(gix_object::Tree, gix_object::TreeRef, "tree/everything.tree");
}

mod blob {
    use std::{convert::TryFrom, io::Write};

    use bstr::ByteSlice;
    use gix_object::{Blob, BlobRef, Object, ObjectRef, WriteTo};

    use crate::fixture_bytes;

    #[test]
    fn round_trip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let input_name = "tree/everything.tree";
        let input = fixture_bytes(input_name);
        // It doesn't matter which data we use - it's not interpreted.

        let mut output = Vec::new();
        let item = BlobRef::from_bytes(&input)?;
        item.write_to(&mut output)?;
        assert_eq!(output.as_bstr(), input.as_bstr(), "borrowed: {input_name}");

        let item: Blob = item.into();
        output.clear();
        item.write_to(&mut output)?;
        assert_eq!(output.as_bstr(), input.as_bstr());

        let item = ObjectRef::from(BlobRef::from_bytes(&input)?);
        output.clear();
        item.write_to(&mut output)?;
        assert_eq!(output.as_bstr(), input.as_bstr(), "object-ref");

        let item: Object = Object::try_from(item)?;
        output.clear();
        item.write_to(&mut output)?;
        assert_eq!(output.as_bstr(), input.as_bstr(), "owned");

        let item = BlobRef::from_bytes(&input)?;
        output.clear();
        {
            let w = &mut output;
            w.write_all(&item.loose_header())?;
            item.write_to(w)?;
            let parsed = ObjectRef::from_loose(&output, gix_testtools::hash_kind_from_env().unwrap_or_default())?;
            let item2 = BlobRef::try_from(parsed).or(Err(super::Error::TryFromError))?;
            assert_eq!(
                item2,
                item,
                "object-ref loose: {input_name} {:?}\n{:?}",
                output.as_bstr(),
                input.as_bstr()
            );
        }

        let item: Blob = item.into();
        output.clear();
        let w = &mut output;
        w.write_all(&item.loose_header())?;
        item.write_to(w)?;
        let parsed = ObjectRef::from_loose(&output, gix_testtools::hash_kind_from_env().unwrap_or_default())?;
        let parsed_borrowed = BlobRef::try_from(parsed).or(Err(super::Error::TryFromError))?;
        let item2: Blob = parsed_borrowed.into();
        assert_eq!(
            item2,
            item,
            "object-ref loose owned: {input_name} {:?}\n{:?}",
            output.as_bstr(),
            input.as_bstr()
        );

        Ok(())
    }
}

mod loose_header {
    use bstr::ByteSlice;
    use gix_object::{decode, encode, Kind};

    #[test]
    fn round_trip() -> Result<(), Box<dyn std::error::Error>> {
        for (kind, size, expected) in &[
            (Kind::Tree, 1234, "tree 1234\0".as_bytes()),
            (Kind::Blob, 0, b"blob 0\0"),
            (Kind::Commit, 24241, b"commit 24241\0"),
            (Kind::Tag, 9999999999, b"tag 9999999999\0"),
        ] {
            let buf = encode::loose_header(*kind, *size);
            assert_eq!(buf.as_bstr(), expected.as_bstr());
            let (actual_kind, actual_size, actual_read) = decode::loose_header(&buf)?;
            assert_eq!(actual_kind, *kind);
            assert_eq!(actual_size, *size);
            assert_eq!(actual_read, buf.len());
        }
        Ok(())
    }
}
