#![allow(clippy::float_cmp)] // Protocol fixture values are exactly representable.

use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::thread;

use libremetaverse_rlv::{
    CameraSettings, InventoryMap, RlvAttachmentPoint, RlvBlacklist, RlvInventoryItem,
    RlvPermissionsService, RlvPermissionsServiceHoverTextLocation,
    RlvPermissionsServiceObjectLocation, RlvPermissionsServiceTouchLocation, RlvRestriction,
    RlvRestrictionManager, RlvRestrictionType, RlvSharedFolder, RlvValue, RlvWearableType,
};
use libremetaverse_types::compat::Guid;

fn guid(byte: u8) -> Guid {
    Guid([byte; 16])
}

fn restriction(behavior: RlvRestrictionType, sender: u8, values: Vec<RlvValue>) -> RlvRestriction {
    RlvRestriction::from_values(behavior, guid(sender), format!("sender-{sender}"), values)
}

fn poll_ready<F: Future>(future: F) -> F::Output {
    let waker = Waker::noop();
    let mut context = Context::from_waker(waker);
    let mut future = std::pin::pin!(future);
    match future.as_mut().poll(&mut context) {
        Poll::Ready(output) => output,
        Poll::Pending => panic!("pure state future unexpectedly yielded"),
    }
}

#[test]
fn manager_deduplicates_updates_and_dispatches_after_unlocking() {
    let manager = RlvRestrictionManager::new();
    let events = Arc::new(Mutex::new(Vec::new()));
    let captured = Arc::clone(&events);
    let callback_manager = manager.clone();
    let _subscription = manager.subscribe_restriction_updated(Some(Arc::new(move |event| {
        // A re-entrant read would deadlock if callbacks ran under the state lock.
        let count = callback_manager
            .find_restrictions(None, None)
            .unwrap()
            .len();
        captured
            .lock()
            .unwrap()
            .push((event.is_new(), event.is_deleted(), count));
    })));

    let fly = restriction(RlvRestrictionType::Fly, 1, vec![]);
    assert!(manager.add_restriction(fly.clone()).unwrap());
    assert!(!manager.add_restriction(fly.clone()).unwrap());
    assert_eq!(
        manager.get_tracked_prim_ids().unwrap().collect::<Vec<_>>(),
        [guid(1)]
    );
    assert!(manager.remove_restriction(&fly));
    assert!(!manager.remove_restriction(&fly));
    assert_eq!(
        *events.lock().unwrap(),
        [(true, false, 1), (false, true, 0)]
    );
}

#[test]
fn manager_handles_concurrent_independent_sources_deterministically() {
    let manager = RlvRestrictionManager::new();
    let mut workers = Vec::new();
    for sender in 1..=24 {
        let worker_manager = manager.clone();
        workers.push(thread::spawn(move || {
            worker_manager
                .add_restriction(restriction(RlvRestrictionType::Jump, sender, vec![]))
                .unwrap()
        }));
    }
    for worker in workers {
        assert!(worker.join().unwrap());
    }

    let restrictions = manager
        .get_restrictions_by_type(RlvRestrictionType::Jump)
        .unwrap();
    assert_eq!(restrictions.len(), 24);
    let mut tracked = manager.get_tracked_prim_ids().unwrap().collect::<Vec<_>>();
    tracked.sort_by_key(|value| value.0);
    assert_eq!(tracked, (1..=24).map(guid).collect::<Vec<_>>());
}

#[test]
fn concurrent_lock_rebuilds_cannot_publish_a_stale_revision() {
    let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
    let first = root.add_child(guid(2), "first".into()).unwrap();
    let second = root.add_child(guid(3), "second".into()).unwrap();
    first
        .add_item(
            guid(4),
            "first attachment".into(),
            false,
            None,
            Some(Some(guid(40))),
            None,
            None,
        )
        .unwrap();
    second
        .add_item(
            guid(5),
            "second attachment".into(),
            false,
            None,
            Some(Some(guid(50))),
            None,
            None,
        )
        .unwrap();
    let manager = RlvRestrictionManager::new();
    manager.set_inventory_map(Some(InventoryMap::new(root, vec![]).unwrap()));
    let barrier = Arc::new(std::sync::Barrier::new(3));
    let mut workers = Vec::new();
    for sender in [40, 50] {
        let worker_manager = manager.clone();
        let worker_barrier = Arc::clone(&barrier);
        workers.push(thread::spawn(move || {
            worker_barrier.wait();
            worker_manager
                .add_restriction(restriction(RlvRestrictionType::DetachThis, sender, vec![]))
                .unwrap();
        }));
    }
    barrier.wait();
    for worker in workers {
        worker.join().unwrap();
    }
    let locked = manager.get_locked_folders().unwrap();
    assert_eq!(locked.len(), 2);
    assert!(!locked[&first.id()].can_detach());
    assert!(!locked[&second.id()].can_detach());
}

#[test]
fn source_removal_counts_repeated_inputs_before_deduplication() {
    let manager = RlvRestrictionManager::new();
    manager
        .add_restriction(restriction(RlvRestrictionType::Fly, 1, vec![]))
        .unwrap();
    let repeated = std::iter::repeat_n(guid(1), 100_001);
    assert!(poll_ready(manager.remove_restrictions_for_objects(Box::new(repeated), None)).is_err());
    assert_eq!(manager.find_restrictions(None, None).unwrap().len(), 1);
}

#[test]
fn blacklist_is_case_insensitive_sorted_and_thread_safe() {
    let blacklist = RlvBlacklist::new();
    let mut workers = Vec::new();
    for behavior in ["Fly", "JUMP", "sit", "CamZoomMax"] {
        let worker_blacklist = blacklist.clone();
        workers.push(thread::spawn(move || {
            worker_blacklist
                .blacklist_behavior(behavior.to_owned())
                .unwrap();
        }));
    }
    for worker in workers {
        worker.join().unwrap();
    }
    assert_eq!(
        blacklist.get_blacklist().unwrap(),
        ["camzoommax", "fly", "jump", "sit"]
    );
    assert!(blacklist.is_blacklisted("fLy".into()).unwrap());
    blacklist.un_blacklist_behavior("FLY".into()).unwrap();
    assert!(!blacklist.is_blacklisted("fly".into()).unwrap());
}

#[test]
fn camera_restrictions_apply_minimum_maximum_alias_and_last_value_rules() {
    let manager = RlvRestrictionManager::new();
    for (sender, value) in [(4, 0.4), (7, 0.7)] {
        manager
            .add_restriction(restriction(
                RlvRestrictionType::CamZoomMin,
                sender,
                vec![RlvValue::Real(value)],
            ))
            .unwrap();
    }
    for (sender, value) in [(20, 2.0), (12, 1.2)] {
        manager
            .add_restriction(restriction(
                RlvRestrictionType::CamZoomMax,
                sender,
                vec![RlvValue::Real(value)],
            ))
            .unwrap();
    }
    manager
        .add_restriction(restriction(
            RlvRestrictionType::CamDistMin,
            31,
            vec![RlvValue::Real(3.0)],
        ))
        .unwrap();
    manager
        .add_restriction(restriction(
            RlvRestrictionType::CamDrawColor,
            32,
            vec![
                RlvValue::Real(0.0),
                RlvValue::Real(0.5),
                RlvValue::Real(1.0),
            ],
        ))
        .unwrap();
    manager
        .add_restriction(restriction(
            RlvRestrictionType::CamDrawColor,
            33,
            vec![
                RlvValue::Real(1.0),
                RlvValue::Real(1.5),
                RlvValue::Real(-1.0),
            ],
        ))
        .unwrap();
    manager
        .add_restriction(restriction(RlvRestrictionType::CamUnlock, 34, vec![]))
        .unwrap();

    let camera = RlvPermissionsService::new(manager)
        .get_camera_restrictions()
        .unwrap();
    assert_eq!(camera.zoom_min().flatten(), Some(0.7));
    assert_eq!(camera.zoom_max().flatten(), Some(1.2));
    assert_eq!(camera.av_dist_min().flatten(), Some(3.0));
    assert_eq!(camera.draw_color().flatten().unwrap().0, [0.5, 0.75, 0.5]);
    assert!(camera.is_locked());
}

#[test]
fn camera_settings_preserve_constructor_field_order() {
    let settings = CameraSettings::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0).unwrap();
    assert_eq!(settings.av_dist_min(), 1.0);
    assert_eq!(settings.av_dist_max(), 2.0);
    assert_eq!(settings.fov_min(), 3.0);
    assert_eq!(settings.fov_max(), 4.0);
    assert_eq!(settings.zoom_min(), 5.0);
    assert_eq!(settings.current_fov(), 6.0);
}

#[test]
fn permission_exceptions_are_targeted_and_secure_rules_take_precedence() {
    let manager = RlvRestrictionManager::new();
    let permissions = RlvPermissionsService::new(manager.clone());
    let allowed_user = guid(90);
    let other_user = guid(91);
    manager
        .add_restriction(restriction(RlvRestrictionType::SendIm, 1, vec![]))
        .unwrap();
    assert!(
        !permissions
            .can_send_im("hello".into(), Some(Some(allowed_user)), None)
            .unwrap()
    );
    manager
        .add_restriction(restriction(
            RlvRestrictionType::SendIm,
            1,
            vec![RlvValue::Uuid(allowed_user)],
        ))
        .unwrap();
    assert!(
        permissions
            .can_send_im("hello".into(), Some(Some(allowed_user)), None)
            .unwrap()
    );
    assert!(
        !permissions
            .can_send_im("hello".into(), Some(Some(other_user)), None)
            .unwrap()
    );
    manager
        .add_restriction(restriction(RlvRestrictionType::SendImSec, 2, vec![]))
        .unwrap();
    assert!(
        !permissions
            .can_send_im("hello".into(), Some(Some(allowed_user)), None)
            .unwrap()
    );
}

#[test]
fn simple_interaction_and_location_permissions_compose() {
    let manager = RlvRestrictionManager::new();
    let permissions = RlvPermissionsService::new(manager.clone());
    assert!(permissions.can_fly().unwrap());
    assert!(permissions.can_sit().unwrap());
    assert!(permissions.can_rez().unwrap());
    assert!(
        permissions
            .can_edit(RlvPermissionsServiceObjectLocation::RezzedInWorld, None)
            .unwrap()
    );

    let interact = restriction(RlvRestrictionType::Interact, 1, vec![]);
    manager.add_restriction(interact.clone()).unwrap();
    assert!(!permissions.can_sit().unwrap());
    assert!(!permissions.can_rez().unwrap());
    assert!(
        !permissions
            .can_edit(RlvPermissionsServiceObjectLocation::Hud, None)
            .unwrap()
    );
    assert!(manager.remove_restriction(&interact));
    manager
        .add_restriction(restriction(RlvRestrictionType::EditWorld, 2, vec![]))
        .unwrap();
    assert!(
        !permissions
            .can_edit(RlvPermissionsServiceObjectLocation::RezzedInWorld, None)
            .unwrap()
    );
    assert!(
        permissions
            .can_edit(RlvPermissionsServiceObjectLocation::Attached, None)
            .unwrap()
    );
}

#[test]
fn distance_and_channel_outputs_apply_defaults_minima_and_precedence() {
    let manager = RlvRestrictionManager::new();
    let permissions = RlvPermissionsService::new(manager.clone());
    let mut distance = -1.0;
    assert!(!permissions.can_sit_tp(&mut distance).unwrap());
    assert_eq!(distance, 1.5);
    manager
        .add_restriction(restriction(
            RlvRestrictionType::SitTp,
            1,
            vec![RlvValue::Real(4.0)],
        ))
        .unwrap();
    manager
        .add_restriction(restriction(
            RlvRestrictionType::SitTp,
            2,
            vec![RlvValue::Real(2.0)],
        ))
        .unwrap();
    assert!(permissions.can_sit_tp(&mut distance).unwrap());
    assert_eq!(distance, 2.0);

    manager
        .add_restriction(restriction(
            RlvRestrictionType::RedirChat,
            3,
            vec![RlvValue::Integer(7)],
        ))
        .unwrap();
    manager
        .add_restriction(restriction(
            RlvRestrictionType::RedirChat,
            4,
            vec![RlvValue::Integer(7)],
        ))
        .unwrap();
    let mut channels = Vec::new();
    assert!(permissions.try_get_redir_chat_channels(&mut channels));
    assert_eq!(channels, [7]);

    manager
        .add_restriction(restriction(RlvRestrictionType::SendChannel, 5, vec![]))
        .unwrap();
    assert!(!permissions.can_chat(8, "private".into()).unwrap());
    manager
        .add_restriction(restriction(
            RlvRestrictionType::SendChannel,
            5,
            vec![RlvValue::Integer(8)],
        ))
        .unwrap();
    assert!(permissions.can_chat(8, "private".into()).unwrap());
    manager
        .add_restriction(restriction(
            RlvRestrictionType::SendChannelExcept,
            6,
            vec![RlvValue::Integer(8)],
        ))
        .unwrap();
    assert!(!permissions.can_chat(8, "private".into()).unwrap());
}

#[test]
fn touch_and_hover_rules_apply_early_overrides_and_explicit_targets() {
    let manager = RlvRestrictionManager::new();
    let permissions = RlvPermissionsService::new(manager.clone());
    let prim = guid(80);
    manager
        .add_restriction(restriction(RlvRestrictionType::TouchAll, 1, vec![]))
        .unwrap();
    assert!(
        !permissions
            .can_touch(
                RlvPermissionsServiceTouchLocation::RezzedInWorld,
                prim,
                None,
                None,
            )
            .unwrap()
    );
    manager
        .add_restriction(restriction(RlvRestrictionType::TouchMe, 80, vec![]))
        .unwrap();
    assert!(
        permissions
            .can_touch(
                RlvPermissionsServiceTouchLocation::RezzedInWorld,
                prim,
                None,
                None,
            )
            .unwrap()
    );
    manager
        .add_restriction(restriction(
            RlvRestrictionType::FarTouch,
            2,
            vec![RlvValue::Real(1.5)],
        ))
        .unwrap();
    assert!(
        !permissions
            .can_touch(
                RlvPermissionsServiceTouchLocation::RezzedInWorld,
                prim,
                None,
                Some(Some(2.0)),
            )
            .unwrap()
    );

    manager
        .add_restriction(restriction(
            RlvRestrictionType::ShowHoverText,
            3,
            vec![RlvValue::Uuid(prim)],
        ))
        .unwrap();
    assert!(
        !permissions
            .can_show_hover_text(
                RlvPermissionsServiceHoverTextLocation::World,
                Some(Some(prim)),
            )
            .unwrap()
    );
    assert!(
        permissions
            .can_show_hover_text(
                RlvPermissionsServiceHoverTextLocation::World,
                Some(Some(guid(81))),
            )
            .unwrap()
    );
}

#[test]
fn attach_and_detach_permissions_combine_item_type_source_and_folder_locks() {
    let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
    let folder = root.add_child(guid(2), "clothes".into()).unwrap();
    let item = folder
        .add_item(
            guid(3),
            "shirt".into(),
            false,
            Some(Some(RlvAttachmentPoint::Chest)),
            Some(Some(guid(30))),
            Some(Some(RlvWearableType::Shirt)),
            None,
        )
        .unwrap();
    let manager = RlvRestrictionManager::new();
    manager.set_inventory_map(Some(InventoryMap::new(root, vec![]).unwrap()));
    let permissions = RlvPermissionsService::new(manager.clone());
    assert!(
        permissions
            .can_attach_with_rlv_inventory_item_boolean(item.clone(), true)
            .unwrap()
    );
    assert!(
        permissions
            .can_detach_with_rlv_inventory_item(item.clone())
            .unwrap()
    );

    manager
        .add_restriction(restriction(RlvRestrictionType::DetachThis, 30, vec![]))
        .unwrap();
    assert!(
        !permissions
            .can_detach_with_rlv_inventory_item(item.clone())
            .unwrap()
    );
    manager
        .add_restriction(restriction(
            RlvRestrictionType::AttachThis,
            31,
            vec![RlvValue::String("clothes".into())],
        ))
        .unwrap();
    assert!(
        !permissions
            .can_attach_with_rlv_inventory_item_boolean(item.clone(), true)
            .unwrap()
    );
    manager
        .add_restriction(restriction(
            RlvRestrictionType::RemOutfit,
            32,
            vec![RlvValue::WearableType(RlvWearableType::Shirt)],
        ))
        .unwrap();
    assert!(
        !permissions
            .can_detach_with_rlv_inventory_item(item)
            .unwrap()
    );
}

#[test]
fn recursive_folder_locks_and_exceptions_publish_isolated_snapshots() {
    let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
    let locked = root.add_child(guid(2), "locked".into()).unwrap();
    let nested = locked.add_child(guid(3), "nested".into()).unwrap();
    locked
        .add_item(
            guid(4),
            "worn object".into(),
            false,
            Some(Some(RlvAttachmentPoint::Chest)),
            Some(Some(guid(44))),
            None,
            None,
        )
        .unwrap();
    let inventory = InventoryMap::new(root, vec![]).unwrap();
    let manager = RlvRestrictionManager::new();
    manager.set_inventory_map(Some(inventory));
    let lock = restriction(RlvRestrictionType::DetachAllThis, 44, vec![]);
    manager.add_restriction(lock.clone()).unwrap();

    let first_snapshot = manager.get_locked_folders().unwrap();
    assert!(!first_snapshot[&locked.id()].can_detach());
    assert!(!first_snapshot[&nested.id()].can_detach());
    let exception = restriction(
        RlvRestrictionType::DetachAllThisExcept,
        45,
        vec![RlvValue::String("locked".into())],
    );
    manager.add_restriction(exception).unwrap();
    let second_snapshot = manager.get_locked_folders().unwrap();
    assert!(second_snapshot[&locked.id()].can_detach());
    assert!(second_snapshot[&nested.id()].can_detach());
    // Previously returned snapshots remain immutable after later manager updates.
    assert!(!first_snapshot[&locked.id()].can_detach());
    assert!(manager.remove_restriction(&lock));
}

#[test]
fn external_inventory_dictionary_membership_is_snapshotted_with_reference_values() {
    let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
    let mut external = RlvInventoryItem::new(
        guid(2),
        "outside".into(),
        false,
        Some(Some(guid(3))),
        None,
        None,
        None,
        None,
    )
    .unwrap();
    let map = InventoryMap::new(root, vec![external.clone()]).unwrap();
    external.set_name("changed".into());
    // Items use C# reference semantics, while the dictionary's membership is a
    // fixed immutable snapshot.  Mutating the referenced item is visible.
    assert_eq!(map.external_items().len(), 1);
    assert_eq!(
        map.external_items().get(&guid(2)).unwrap().name(),
        "changed"
    );
    external.set_attached_prim_id(Some(Some(guid(4))));
    assert_eq!(
        map.external_items()
            .get(&guid(2))
            .unwrap()
            .attached_prim_id(),
        Some(Some(guid(4)))
    );
}
