use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use libremetaverse::GridClient;
use libremetaverse::assets::{
    OarFile, OarFileAssetLoadedCallback, OarFileSceneObjectLoadedCallback,
    OarFileSettingsLoadedCallback, OarFileTerrainLoadedCallback, RegionSettings,
};
use libremetaverse::import_export::{ColladaLoader, ModelFace, ModelPrim, ModelUploader};
use libremetaverse::rendering::Vertex;
use libremetaverse_structured_data::OSD;
use libremetaverse_types::{Quaternion, UUID, Vector2, Vector3};

fn temporary_directory(label: &str) -> PathBuf {
    let id = UUID::random().expect("temporary UUID").to_string();
    let path = std::env::temp_dir().join(format!("metacrate-{label}-{id}"));
    fs::create_dir_all(&path).expect("create temporary directory");
    path
}

#[test]
fn oar_packaging_is_deterministic_and_sorted() {
    let root = temporary_directory("oar");
    for directory in ["assets", "objects", "terrains", "landdata", "settings"] {
        fs::create_dir_all(root.join(directory)).expect("create OAR directory");
    }
    fs::write(root.join("assets/z.texture"), b"z").expect("write z asset");
    fs::write(root.join("assets/a.texture"), b"a").expect("write a asset");
    fs::write(root.join("terrains/region.r32"), 1.0_f32.to_le_bytes()).expect("write terrain");
    let first = root.join("first.oar");
    let second = root.join("second.oar");
    OarFile::package_archive(
        root.to_string_lossy().into_owned(),
        first.to_string_lossy().into_owned(),
    )
    .expect("first package");
    OarFile::package_archive(
        root.to_string_lossy().into_owned(),
        second.to_string_lossy().into_owned(),
    )
    .expect("second package");
    assert_eq!(
        fs::read(first).expect("first bytes"),
        fs::read(second).expect("second bytes")
    );
    fs::remove_dir_all(root).expect("remove temporary OAR directory");
}

#[test]
fn oar_canonical_asset_and_settings_names_round_trip() {
    let root = temporary_directory("oar-roundtrip");
    for directory in ["assets", "objects", "terrains", "landdata", "settings"] {
        fs::create_dir_all(root.join(directory)).expect("create OAR directory");
    }
    let asset_id = UUID::random().expect("asset UUID");
    let asset_bytes = b"native-j2c-payload".to_vec();
    fs::write(
        root.join(format!("assets/{asset_id}_texture.jp2")),
        &asset_bytes,
    )
    .expect("write canonical asset");
    let mut settings = RegionSettings::new().expect("region settings");
    settings.allow_damage = true;
    settings.water_height = 23.5;
    settings
        .to_xml(
            root.join("settings/Native Region.xml")
                .to_string_lossy()
                .into_owned(),
        )
        .expect("write settings");
    let archive = root.join("roundtrip.oar");
    OarFile::package_archive(
        root.to_string_lossy().into_owned(),
        archive.to_string_lossy().into_owned(),
    )
    .expect("package round trip");

    let loaded_asset = Arc::new(Mutex::new(None));
    let loaded_asset_out = Arc::clone(&loaded_asset);
    let loaded_settings = Arc::new(Mutex::new(None));
    let loaded_settings_out = Arc::clone(&loaded_settings);
    OarFile::unpackage_archive(
        archive.to_string_lossy().into_owned(),
        OarFileAssetLoadedCallback::from_callback(move |asset, _, _| {
            *loaded_asset_out.lock().expect("asset lock") =
                Some((asset.asset_id(), asset.asset_type(), asset.asset_data));
        }),
        OarFileTerrainLoadedCallback::from_callback(|_, _, _| {}),
        OarFileSceneObjectLoadedCallback::from_callback(|_, _, _| {}),
        OarFileSettingsLoadedCallback::from_callback(move |name, settings| {
            *loaded_settings_out.lock().expect("settings lock") =
                Some((name, settings.allow_damage, settings.water_height));
        }),
    )
    .expect("unpackage round trip");

    let asset = loaded_asset.lock().expect("loaded asset lock").clone();
    assert_eq!(
        asset,
        Some((
            asset_id,
            libremetaverse_types::AssetType::Texture,
            asset_bytes
        ))
    );
    assert_eq!(
        loaded_settings
            .lock()
            .expect("loaded settings lock")
            .clone(),
        Some(("Native Region".to_owned(), true, 23.5))
    );
    fs::remove_dir_all(root).expect("remove round-trip directory");
}

#[test]
fn model_face_deduplicates_vertices_and_asset_is_deterministic() {
    let vertex = || Vertex {
        position: Vector3 {
            x: -0.5,
            y: 0.25,
            z: 0.5,
        },
        normal: Vector3 {
            x: 0.0,
            y: 0.0,
            z: 1.0,
        },
        tex_coord: Vector2 { x: 0.0, y: 1.0 },
    };
    let mut face = ModelFace::new().expect("model face");
    face.add_vertex(vertex()).expect("first vertex");
    face.add_vertex(vertex()).expect("duplicate vertex");
    assert_eq!(face.vertices.len(), 1);
    assert_eq!(face.indices, [0, 0]);

    let mut prim = ModelPrim::new().expect("model prim");
    prim.faces.push(face);
    prim.create_asset(UUID::zero()).expect("first model asset");
    let first = prim.asset.clone();
    prim.create_asset(UUID::zero()).expect("second model asset");
    assert!(!first.is_empty());
    assert_eq!(prim.asset, first);
}

#[test]
fn model_upload_resources_are_deterministic_and_offline() {
    let mut face = ModelFace::new().expect("model face");
    face.material.texture = "surface.jp2".into();
    face.material.texture_data = vec![1, 2, 3, 4];
    for position in [
        Vector3::zero(),
        Vector3 {
            x: 1.0,
            y: 0.0,
            z: 0.0,
        },
        Vector3 {
            x: 0.0,
            y: 1.0,
            z: 0.0,
        },
    ] {
        face.add_vertex(Vertex {
            position,
            normal: Vector3 {
                x: 0.0,
                y: 0.0,
                z: 1.0,
            },
            tex_coord: Vector2::zero(),
        })
        .expect("upload vertex");
    }
    let mut prim = ModelPrim::new().expect("model prim");
    prim.faces.push(face);
    prim.create_asset(UUID::zero()).expect("mesh asset");
    let uploader = ModelUploader::new(
        GridClient::new().expect("offline client"),
        vec![prim],
        "Offline mesh".into(),
        "No network required".into(),
    )
    .expect("model uploader");
    let first = uploader.asset_resources(false).expect("offline resources");
    let second = uploader.asset_resources(false).expect("repeat resources");
    assert_eq!(first, second);
    let OSD::Map(map) = first else {
        panic!("resource map");
    };
    let OSD::Array(textures) = map.get("texture_list").expect("texture list") else {
        panic!("texture array");
    };
    assert_eq!(textures, &[OSD::Binary(Vec::new())]);
    let OSD::Map(upload) = uploader.asset_resources(true).expect("upload resources") else {
        panic!("upload map");
    };
    assert_eq!(
        upload.get("texture_list"),
        Some(&OSD::Array(vec![OSD::Binary(vec![1, 2, 3, 4])]))
    );
}

#[test]
fn collada_triangle_converts_to_native_model_asset() {
    let root = temporary_directory("collada");
    let file = root.join("triangle.dae");
    fs::write(
        &file,
        r##"<?xml version="1.0" encoding="utf-8"?>
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
  <library_geometries><geometry id="triangle"><mesh>
    <source id="positions"><float_array id="positions-array" count="9">0 0 0 1 0 0 0 1 0</float_array>
      <technique_common><accessor source="#positions-array" count="3" stride="3"/></technique_common></source>
    <vertices id="vertices"><input semantic="POSITION" source="#positions"/></vertices>
    <triangles count="1"><input semantic="VERTEX" source="#vertices" offset="0"/><p>0 1 2</p></triangles>
  </mesh></geometry></library_geometries>
</COLLADA>"##,
    )
    .expect("write Collada fixture");
    let prims = ColladaLoader::new(None)
        .expect("Collada loader")
        .load(file.to_string_lossy().into_owned(), false)
        .expect("load Collada");
    assert_eq!(prims.len(), 1);
    assert_eq!(prims[0].faces.len(), 1);
    assert_eq!(prims[0].faces[0].indices, [0, 1, 2]);
    assert!(!prims[0].asset.is_empty());
    assert_eq!(prims[0].rotation, Quaternion::identity());
    fs::remove_dir_all(root).expect("remove temporary Collada directory");
}

#[test]
fn collada_applies_units_effects_and_instance_material_bindings() {
    let root = temporary_directory("collada-material");
    let file = root.join("material.dae");
    fs::write(
        &file,
        r##"<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema">
  <asset><unit meter="2"/><up_axis>Z_UP</up_axis></asset>
  <library_effects><effect id="green"><profile_COMMON><technique sid="common"><lambert>
    <diffuse><color>0.1 0.8 0.2 1</color></diffuse>
  </lambert></technique></profile_COMMON></effect></library_effects>
  <library_materials><material id="surface"><instance_effect url="#green"/></material></library_materials>
  <library_geometries><geometry id="triangle"><mesh>
    <source id="positions"><float_array>0 0 0 1 0 0 0 1 0</float_array>
      <technique_common><accessor stride="3"/></technique_common></source>
    <vertices id="vertices"><input semantic="POSITION" source="#positions"/></vertices>
    <triangles count="1" material="slot"><input semantic="VERTEX" source="#vertices" offset="0"/><p>0 1 2</p></triangles>
  </mesh></geometry></library_geometries>
  <library_visual_scenes><visual_scene id="scene"><node id="instance"><instance_geometry url="#triangle">
    <bind_material><technique_common><instance_material symbol="slot" target="#surface"/></technique_common></bind_material>
  </instance_geometry></node></visual_scene></library_visual_scenes>
</COLLADA>"##,
    )
    .expect("write material Collada fixture");
    let prims = ColladaLoader::new(None)
        .expect("Collada loader")
        .load(file.to_string_lossy().into_owned(), false)
        .expect("load material Collada");
    assert_eq!(prims.len(), 1);
    assert_eq!(prims[0].scale.x, 2.0);
    assert_eq!(prims[0].scale.y, 2.0);
    assert_eq!(prims[0].faces[0].material.id, "surface");
    assert_eq!(prims[0].faces[0].material.diffuse_color.g, 0.8);
    fs::remove_dir_all(root).expect("remove material Collada directory");
}

#[test]
fn collada_texture_path_cannot_escape_document_root() {
    let root = temporary_directory("collada-traversal");
    let file = root.join("traversal.dae");
    fs::write(
        &file,
        r##"<COLLADA><library_images><image id="escape"><init_from>../secret.jp2</init_from></image></library_images>
<library_materials><material id="mat"><diffuse><texture texture="escape"/></diffuse></material></library_materials></COLLADA>"##,
    )
    .expect("write traversal fixture");
    assert!(
        ColladaLoader::new(None)
            .expect("Collada loader")
            .load(file.to_string_lossy().into_owned(), true)
            .is_err()
    );
    fs::remove_dir_all(root).expect("remove traversal directory");
}
