#[cfg(feature = "jpeg2000")]
use crate::assets::AssetTexture;
use crate::assets::{AssetMesh, AssetMutable, AssetSound};
use crate::{
    AssetCache, AssetCacheComputeAssetCacheFilenameDelegate, Error, GridClient, ImageCodec,
};
#[cfg(feature = "jpeg2000")]
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
use libremetaverse_types::{AssetType, UUID};
use std::fs;
use std::path::PathBuf;

fn fixture(max_size: i64) -> (GridClient, AssetCache, PathBuf) {
    let path =
        std::env::temp_dir().join(format!("metacrate-asset-cache-{}", UUID::random().unwrap()));
    let mut client = GridClient::new().unwrap();
    let settings = client.settings();
    settings.asset_cache_mut().dir = path.to_string_lossy().into_owned();
    settings.asset_cache_mut().enabled = true;
    settings.asset_cache_mut().max_size = max_size;
    let cache = AssetCache::new(client.clone()).unwrap();
    (client, cache, path)
}

#[test]
fn asset_models_retain_type_and_reject_malformed_meshes() {
    let asset = AssetMutable::new_with_asset_type_uuid_bytes(
        AssetType::Sound,
        UUID::random().unwrap(),
        vec![1, 2, 3],
    )
    .unwrap();
    assert_eq!(asset.asset_type(), AssetType::Sound);
    assert!(asset.decode().unwrap());
    let malformed =
        AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).unwrap();
    assert!(!malformed.decode().unwrap());
}

#[test]
#[cfg(feature = "vorbis")]
fn sound_codec_produces_real_valid_payload() {
    let ogg = AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)).unwrap();
    assert!(ogg.starts_with(b"OggS"));
}

#[test]
#[cfg(not(feature = "vorbis"))]
fn sound_codec_is_explicitly_feature_gated() {
    assert_eq!(
        AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)),
        Err(Error::InvalidOperation)
    );
    assert_eq!(
        AssetSound::pcm_to_ogg(Vec::new(), 44_100, 1, Some(16)),
        Err(Error::Argument)
    );
}

#[test]
#[cfg(feature = "jpeg2000")]
fn texture_codec_produces_real_valid_payload() {
    let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
    image.red.fill(255);
    let texture = AssetTexture::new_with_managed_image(image).unwrap();
    texture.encode().unwrap();
    assert!(texture.decode().unwrap());
}

#[test]
fn cache_writes_are_atomic_and_corruption_is_typed() {
    let (_client, cache, path) = fixture(1024);
    let id = UUID::random().unwrap();
    cache
        .save_asset_to_cache_with_uuid_bytes(id, vec![1, 2, 3, 4])
        .unwrap();
    cache
        .save_asset_to_cache_with_uuid_bytes(id, vec![9, 9, 9, 9])
        .unwrap();
    assert_eq!(
        cache.get_cached_asset_bytes_with_uuid(id).unwrap(),
        Some(vec![1, 2, 3, 4])
    );

    let image = cache.get_cached_image(id).unwrap().unwrap();
    assert_eq!(image.codec, ImageCodec::J2C);
    assert_eq!(image.base.asset_data, vec![1, 2, 3, 4]);

    let corrupt = UUID::random().unwrap();
    fs::write(path.join(corrupt.to_string()), []).unwrap();
    assert_eq!(
        cache.get_cached_asset_bytes_with_uuid(corrupt),
        Err(Error::Argument)
    );
    fs::remove_dir_all(path).unwrap();
}

#[test]
fn custom_cache_naming_is_used_but_cannot_escape_the_cache_directory() {
    let (_client, mut cache, path) = fixture(1024);
    cache.compute_asset_cache_filename = Some(
        AssetCacheComputeAssetCacheFilenameDelegate::from_handler(|directory, id| {
            Ok(PathBuf::from(directory)
                .join(format!("{id}.asset"))
                .to_string_lossy()
                .into_owned())
        }),
    );
    let id = UUID::random().unwrap();
    cache
        .save_asset_to_cache_with_uuid_bytes(id, vec![7, 8, 9])
        .unwrap();
    assert_eq!(
        cache.get_cached_asset_bytes_with_uuid(id).unwrap(),
        Some(vec![7, 8, 9])
    );
    assert!(path.join(format!("{id}.asset")).is_file());
    cache.clear().unwrap();
    assert!(!path.join(format!("{id}.asset")).exists());

    cache.compute_asset_cache_filename = Some(
        AssetCacheComputeAssetCacheFilenameDelegate::from_handler(|directory, _| {
            Ok(PathBuf::from(directory)
                .join("..")
                .join("escape")
                .to_string_lossy()
                .into_owned())
        }),
    );
    assert_eq!(
        cache.has_asset(UUID::random().unwrap()),
        Err(Error::Argument)
    );
    fs::remove_dir_all(path).unwrap();
}

#[tokio::test]
async fn cache_pruning_reclaims_old_entries_to_below_the_limit() {
    let (_client, cache, path) = fixture(12);
    cache
        .save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![1; 8])
        .unwrap();
    std::thread::sleep(std::time::Duration::from_millis(5));
    cache
        .save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![2; 8])
        .unwrap();
    cache.prune(None).await.unwrap();
    let bytes: u64 = fs::read_dir(&path)
        .unwrap()
        .filter_map(Result::ok)
        .filter_map(|entry| entry.metadata().ok())
        .map(|metadata| metadata.len())
        .sum();
    assert!(bytes <= 10);
    fs::remove_dir_all(path).unwrap();
}
