use std::collections::HashSet;
use std::sync::Arc;

use hashtree_core::{
    collect_hashes, tree_diff, DirEntry, HashTree, HashTreeConfig, HashTreeError, LinkType,
    MemoryStore,
};
use proptest::prelude::*;

async fn build_directory_from_contents(
    tree: &HashTree<MemoryStore>,
    contents: &[Vec<u8>],
) -> hashtree_core::Cid {
    let mut entries = Vec::with_capacity(contents.len());
    for (idx, bytes) in contents.iter().enumerate() {
        let (cid, size) = tree.put(bytes).await.unwrap();
        entries.push(
            DirEntry::from_cid(format!("file-{idx}.bin"), &cid)
                .with_size(size)
                .with_link_type(LinkType::Blob),
        );
    }
    tree.put_directory(entries).await.unwrap()
}

proptest! {
    #[test]
    fn prop_diff_identity_empty(
        files in prop::collection::vec(prop::collection::vec(any::<u8>(), 0..256), 0..12),
    ) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let store = Arc::new(MemoryStore::new());
            let tree = HashTree::new(HashTreeConfig::new(store).public());

            let root = build_directory_from_contents(&tree, &files).await;
            let diff = tree_diff(&tree, Some(&root), &root, 8).await.unwrap();
            assert!(diff.is_empty());
            assert!(diff.added.is_empty());
        });
    }

    #[test]
    fn prop_diff_matches_set_difference(
        old_files in prop::collection::vec(prop::collection::vec(any::<u8>(), 0..128), 0..10),
        new_files in prop::collection::vec(prop::collection::vec(any::<u8>(), 0..128), 0..10),
    ) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let store = Arc::new(MemoryStore::new());
            let tree = HashTree::new(HashTreeConfig::new(store).public());

            let old_root = build_directory_from_contents(&tree, &old_files).await;
            let new_root = build_directory_from_contents(&tree, &new_files).await;

            let old_set = collect_hashes(&tree, &old_root, 8).await.unwrap();
            let new_set = collect_hashes(&tree, &new_root, 8).await.unwrap();
            let expected: HashSet<_> = new_set.difference(&old_set).copied().collect();

            let diff = tree_diff(&tree, Some(&old_root), &new_root, 8).await.unwrap();
            let actual: HashSet<_> = diff.added.iter().copied().collect();

            assert_eq!(actual, expected);
        });
    }

    #[test]
    fn prop_first_push_returns_all_reachable(
        files in prop::collection::vec(prop::collection::vec(any::<u8>(), 0..128), 0..10),
    ) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let store = Arc::new(MemoryStore::new());
            let tree = HashTree::new(HashTreeConfig::new(store).public());

            let root = build_directory_from_contents(&tree, &files).await;
            let expected = collect_hashes(&tree, &root, 8).await.unwrap();
            let diff = tree_diff(&tree, None, &root, 8).await.unwrap();
            let actual: HashSet<_> = diff.added.iter().copied().collect();

            assert_eq!(actual, expected);
        });
    }
}

#[tokio::test]
async fn test_diff_encrypted_wrong_key_returns_decryption_error() {
    let store = Arc::new(MemoryStore::new());
    let tree = HashTree::new(HashTreeConfig::new(store));

    let (cid, _size) = tree.put(b"encrypted-content").await.unwrap();
    let mut wrong_key = cid.key.unwrap();
    wrong_key[0] ^= 0xff;
    let bad_root = hashtree_core::Cid {
        hash: cid.hash,
        key: Some(wrong_key),
    };

    let err = tree_diff(&tree, None, &bad_root, 4).await.unwrap_err();
    match err {
        HashTreeError::Decryption(_) => {}
        other => panic!("expected HashTreeError::Decryption, got {other:?}"),
    }
}
