#![allow(clippy::float_cmp)] // Fixture values are exactly representable protocol literals.

use libremetaverse_rlv::{
    RLV_RESTRICTION_NAMES, RlvAction, RlvAttachmentPoint, RlvCameraQuery, RlvCommon, RlvDirective,
    RlvGroupTarget, RlvParseErrorKind, RlvParser, RlvQuery, RlvRestriction,
    RlvRestrictionOperation, RlvRestrictionType, RlvTarget, RlvValue, RlvWearableType,
    attachment_from_name, restriction_from_name, restriction_name, wearable_from_name,
};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{Guid, Object};

fn sender() -> Guid {
    UUID::parse("ffffffff-ffff-4fff-8fff-ffffffffffff".to_owned())
        .expect("fixture UUID")
        .guid()
}

fn parse(message: &str) -> libremetaverse_rlv::RlvMessage {
    RlvParser::parse_message(message, sender(), "Sender Name").expect("valid RLV message")
}

#[test]
fn message_preserves_original_casing_options_and_byte_locations() {
    let parsed = parse("@FlY=ADD,getdebug_RenderMode:MiXeD=+123");
    assert_eq!(parsed.commands.len(), 2);
    let first = &parsed.commands[0];
    assert_eq!(first.raw_behavior, "FlY");
    assert_eq!(first.behavior, "fly");
    assert_eq!(first.raw_parameter, "ADD");
    assert_eq!(first.parameter, "add");
    assert_eq!(first.source.start, 1);
    assert_eq!(first.source.end, 8);
    assert!(matches!(
        first.directive,
        RlvDirective::Restriction {
            behavior: RlvRestrictionType::Fly,
            operation: RlvRestrictionOperation::Add,
            ref values,
        } if values.is_empty()
    ));
    let second = &parsed.commands[1];
    assert_eq!(second.raw_behavior, "getdebug_RenderMode");
    assert_eq!(second.behavior, "getdebug_rendermode");
    assert_eq!(second.option, "MiXeD");
    assert!(matches!(
        second.directive,
        RlvDirective::Query {
            channel: 123,
            query: RlvQuery::Debug(ref name),
        } if name == "rendermode"
    ));
}

#[test]
fn bare_clear_and_all_parameter_classes_are_typed() {
    assert!(matches!(
        parse("@ClEaR").commands[0].directive,
        RlvDirective::Clear
    ));
    assert!(matches!(
        parse("@unsit=FoRcE").commands[0].directive,
        RlvDirective::Action(RlvAction::Unsit)
    ));
    assert!(matches!(
        parse("@jump=Y").commands[0].directive,
        RlvDirective::Restriction {
            operation: RlvRestrictionOperation::Remove,
            ..
        }
    ));
    assert!(matches!(
        parse("@version= 42 ").commands[0].directive,
        RlvDirective::Query {
            channel: 42,
            query: RlvQuery::Version { modern: false }
        }
    ));
}

#[test]
fn action_aliases_and_targets_remain_distinct() {
    let attach = &parse("@addoutfitallover:Clothing/Hats=force").commands[0].directive;
    assert!(matches!(
        attach,
        RlvDirective::Action(RlvAction::Attach {
            folder_path,
            replace: false,
            recursive: true,
        }) if folder_path == "Clothing/Hats"
    ));
    let this = &parse("@attachthisoverorreplace:R UPPER ARM=force").commands[0].directive;
    assert!(matches!(
        this,
        RlvDirective::Action(RlvAction::AttachThis {
            target: RlvTarget::AttachmentPoint(RlvAttachmentPoint::RightUpperArm),
            replace: true,
            recursive: false,
        })
    ));
    let remove = &parse("@remoutfit:Tattoo=force").commands[0].directive;
    assert!(matches!(
        remove,
        RlvDirective::Action(RlvAction::RemoveOutfit(RlvTarget::WearableType(
            RlvWearableType::Tattoo
        )))
    ));
}

#[test]
fn numeric_uuid_group_and_teleport_options_are_validated() {
    let adjusted = &parse("@adjustheight:1.25;2;bad;ignored=force").commands[0].directive;
    assert!(matches!(
        adjusted,
        RlvDirective::Action(RlvAction::AdjustHeight { distance, factor, delta })
            if *distance == 1.25 && *factor == 2.0 && *delta == 0.0
    ));
    let teleported = &parse("@tpto:Region Name/1/2/3;4=force").commands[0].directive;
    assert!(matches!(
        teleported,
        RlvDirective::Action(RlvAction::Teleport {
            x,
            y,
            z,
            region: Some(region),
            look_at: Some(look_at),
        }) if *x == 1.0 && *y == 2.0 && *z == 3.0 && region == "Region Name" && *look_at == 4.0
    ));
    let grouped = &parse("@setgroup:My Group;Role Name=force").commands[0].directive;
    assert!(matches!(
        grouped,
        RlvDirective::Action(RlvAction::SetGroup {
            target: RlvGroupTarget::Name(name),
            role: Some(role),
        }) if name == "My Group" && role == "Role Name"
    ));
}

#[test]
fn query_variants_preserve_filters_separators_paths_and_aliases() {
    let status = &parse("@getstatus:TP; ! =7").commands[0].directive;
    assert!(matches!(
        status,
        RlvDirective::Query {
            query: RlvQuery::Status { all_senders: false, filter, separator },
            channel: 7,
        } if filter == "tp" && separator == " ! "
    ));
    let folders = &parse("@findfolders:Hat&&Red;|=8").commands[0].directive;
    assert!(matches!(
        folders,
        RlvDirective::Query {
            query: RlvQuery::FindFolders { first_only: false, terms, separator },
            ..
        } if terms == &["Hat", "Red"] && separator == "|"
    ));
    let path = &parse("@getpathnew:root=9").commands[0].directive;
    assert!(matches!(
        path,
        RlvDirective::Query {
            query: RlvQuery::Path {
                legacy: false,
                target: RlvTarget::AttachmentPoint(RlvAttachmentPoint::AvatarCenter),
            },
            ..
        }
    ));
    assert!(matches!(
        parse("@getcam_fov=10").commands[0].directive,
        RlvDirective::Query {
            query: RlvQuery::Camera(RlvCameraQuery::CurrentFov),
            ..
        }
    ));
}

#[test]
fn restriction_options_are_strongly_typed() {
    let notify = &parse("@notify:1234;sendim=add").commands[0].directive;
    assert!(matches!(
        notify,
        RlvDirective::Restriction { values, .. }
            if values == &[RlvValue::Integer(1234), RlvValue::String("sendim".to_owned())]
    ));
    let attachment = &parse("@detach:R Upper Arm=n").commands[0].directive;
    assert!(matches!(
        attachment,
        RlvDirective::Restriction { values, .. }
            if values == &[RlvValue::AttachmentPoint(RlvAttachmentPoint::RightUpperArm)]
    ));
    let wearable = &parse("@remoutfit:UNIVERSAL=n").commands[0].directive;
    assert!(matches!(
        wearable,
        RlvDirective::Restriction { values, .. }
            if values == &[RlvValue::WearableType(RlvWearableType::Universal)]
    ));
    let uuid = "00000000-0000-4000-8000-000000000000";
    assert!(matches!(
        &parse(&format!("@accepttp:{uuid}=add")).commands[0].directive,
        RlvDirective::Restriction { values, .. } if matches!(values.as_slice(), [RlvValue::Uuid(_)])
    ));

    // The pinned parser reads the first camera scalar and deliberately ignores
    // later semicolon fields for this family.
    assert!(matches!(
        &parse("@camzoommax:2.5;ignored;3=n").commands[0].directive,
        RlvDirective::Restriction { values, .. }
            if values == &[RlvValue::Real(2.5)]
    ));
}

#[test]
fn every_pinned_restriction_name_round_trips_without_normalization() {
    assert_eq!(RLV_RESTRICTION_NAMES.len(), 119);
    for (name, restriction) in RLV_RESTRICTION_NAMES {
        assert_eq!(restriction_from_name(name), Some(*restriction));
        assert_eq!(restriction_name(*restriction), *name);
    }
    assert_eq!(
        restriction_from_name("TouchFar"),
        Some(RlvRestrictionType::TouchFar)
    );
    assert_eq!(restriction_from_name("touch-far"), None);
}

#[test]
fn every_pinned_restriction_has_at_least_one_typed_valid_option_form() {
    let uuid = "00000000-0000-4000-8000-000000000000";
    let candidates = [
        "",
        "1",
        "1.0",
        "1;1;1",
        uuid,
        "chest",
        "shirt",
        "Folder Name",
    ];
    for (name, expected) in RLV_RESTRICTION_NAMES {
        let accepted = candidates.iter().any(|option| {
            let message = format!("@{name}:{option}=n");
            RlvParser::parse_message(&message, sender(), "sender")
                .ok()
                .is_some_and(|message| {
                    matches!(
                        message.commands[0].directive,
                        RlvDirective::Restriction { behavior, .. } if behavior == *expected
                    )
                })
        });
        assert!(accepted, "no valid typed option form for {name}");
    }
}

#[test]
fn attachment_and_wearable_alias_tables_are_case_insensitive_but_not_trimmed() {
    assert_eq!(
        attachment_from_name("ROOT"),
        Some(RlvAttachmentPoint::AvatarCenter)
    );
    assert_eq!(
        attachment_from_name("l upper leg"),
        Some(RlvAttachmentPoint::LeftUpperLeg)
    );
    assert_eq!(attachment_from_name(" chest "), None);
    assert_eq!(
        wearable_from_name("UnderShirt"),
        Some(RlvWearableType::Undershirt)
    );
    assert_eq!(
        wearable_from_name("universal"),
        Some(RlvWearableType::Universal)
    );
    assert_eq!(wearable_from_name(" shirt "), None);
}

#[test]
fn mapped_attachment_tag_helper_uses_last_recognized_tag() {
    let mut output = None;
    assert!(RlvCommon::try_get_attachment_point_from_item_name(
        "Item (mouth) (unknown) (SPINE)".to_owned(),
        &mut output,
    ));
    assert_eq!(output, Some(Some(RlvAttachmentPoint::Spine)));
    assert!(!RlvCommon::try_get_attachment_point_from_item_name(
        "Item (unknown)".to_owned(),
        &mut output,
    ));
    assert_eq!(output, None);
}

#[test]
fn malformed_corpus_has_stable_categories_locations_and_never_panics() {
    let cases = [
        ("", RlvParseErrorKind::MissingPrefix),
        ("fly=n", RlvParseErrorKind::MissingPrefix),
        ("@", RlvParseErrorKind::EmptyCommand),
        ("@fly", RlvParseErrorKind::MissingEquals),
        ("@=n", RlvParseErrorKind::EmptyBehavior),
        ("@fly=", RlvParseErrorKind::EmptyParameter),
        ("@unknown=force", RlvParseErrorKind::UnknownAction),
        ("@unknown=n", RlvParseErrorKind::UnknownRestriction),
        ("@unknown=123", RlvParseErrorKind::UnknownQuery),
        ("@fly=maybe", RlvParseErrorKind::UnsupportedParameter),
        ("@version=0", RlvParseErrorKind::ZeroQueryChannel),
        ("@sit:not-a-uuid=force", RlvParseErrorKind::InvalidUuid),
        ("@camdrawmin:0.39=n", RlvParseErrorKind::InvalidOption),
        ("@detach: chest =n", RlvParseErrorKind::InvalidOption),
        ("@fly=n,", RlvParseErrorKind::EmptyCommand),
    ];
    for (message, expected) in cases {
        let result =
            std::panic::catch_unwind(|| RlvParser::parse_message(message, sender(), "sender"));
        let error = result.expect("parser never panics").expect_err(message);
        assert_eq!(error.kind, expected, "{message}");
        assert!(error.span.start <= error.span.end);
        assert!(error.span.end <= message.len());
    }
}

#[test]
fn deterministic_single_byte_mutations_are_bounded_and_never_panic() {
    const SEEDS: &[&str] = &[
        "@fly=n",
        "@version=42",
        "@notify:1234;sendim=add",
        "@attachthisoverorreplace:r upper arm=force",
        "@sit:00000000-0000-4000-8000-000000000000=force",
        "@getstatus:tp;|=7,@getcam_fov=8",
    ];
    const REPLACEMENTS: &[u8] = b"@,:=;0An?-_ ";

    let mut corpus = Vec::new();
    for seed in SEEDS {
        for index in 0..seed.len() {
            let mut deleted = seed.as_bytes().to_vec();
            deleted.remove(index);
            corpus.push(String::from_utf8(deleted).expect("ASCII fixture"));

            for replacement in REPLACEMENTS {
                let mut replaced = seed.as_bytes().to_vec();
                replaced[index] = *replacement;
                corpus.push(String::from_utf8(replaced).expect("ASCII fixture"));
            }
        }
        for index in 0..=seed.len() {
            for insertion in REPLACEMENTS {
                let mut inserted = seed.as_bytes().to_vec();
                inserted.insert(index, *insertion);
                corpus.push(String::from_utf8(inserted).expect("ASCII fixture"));
            }
        }
    }

    assert!(corpus.len() >= 4_000, "mutation corpus unexpectedly small");
    for message in corpus {
        let result = std::panic::catch_unwind(|| {
            RlvParser::parse_message(&message, sender(), "mutated sender")
        });
        let parsed = result.expect("bounded parser must not panic for any mutation");
        if let Err(error) = parsed {
            assert!(error.span.start <= error.span.end, "{message:?}");
            assert!(error.span.end <= message.len(), "{message:?}");
        }
    }
}

#[test]
fn command_count_and_message_size_are_bounded() {
    let oversized_count = format!(
        "@{}",
        std::iter::repeat_n("fly=n", 129)
            .collect::<Vec<_>>()
            .join(",")
    );
    assert_eq!(
        RlvParser::parse_message(&oversized_count, sender(), "sender")
            .expect_err("too many commands")
            .kind,
        RlvParseErrorKind::TooManyCommands
    );
    let oversized_message = format!("@getinv:{}=1", "x".repeat(64 * 1024));
    assert_eq!(
        RlvParser::parse_message(&oversized_message, sender(), "sender")
            .expect_err("oversized message")
            .kind,
        RlvParseErrorKind::MessageTooLong
    );
}

#[test]
fn mapped_restriction_preserves_alias_exception_equality_and_snapshot_args() {
    let first = RlvRestriction::new(
        RlvRestrictionType::FarTouch,
        sender(),
        "Object A".to_owned(),
        vec![Object::Real(2.5)],
    )
    .expect("restriction");
    let second = RlvRestriction::new(
        RlvRestrictionType::FarTouch,
        sender(),
        "Renamed Object".to_owned(),
        vec![Object::Real(2.5)],
    )
    .expect("restriction");
    assert_eq!(first.behavior(), RlvRestrictionType::TouchFar);
    assert_eq!(first.original_behavior(), RlvRestrictionType::FarTouch);
    assert!(!first.is_exception());
    assert_eq!(first, second);
    assert_eq!(first.get_hash_code(), second.get_hash_code());
    assert_eq!(first.args().as_slice(), &[Object::Real(2.5)]);
    assert!(first.to_string().contains("Behavior=TouchFar"));
    assert!(first.equals(Some(Object::opaque(second))));

    let exception = RlvRestriction::from_values(
        RlvRestrictionType::RecvChat,
        sender(),
        "Object".to_owned(),
        vec![RlvValue::Uuid(sender())],
    );
    assert!(exception.is_exception());

    let exact_real = 2.500_000_000_000_001_f64;
    let exact = RlvRestriction::new(
        RlvRestrictionType::CamZoomMax,
        sender(),
        "Exact value".to_owned(),
        vec![Object::Real(exact_real)],
    )
    .expect("restriction");
    assert_eq!(exact.args().as_slice(), &[Object::Real(exact_real)]);
    let rounded = RlvRestriction::new(
        RlvRestrictionType::CamZoomMax,
        sender(),
        "Rounded value".to_owned(),
        vec![Object::Real(2.5)],
    )
    .expect("restriction");
    assert_ne!(
        exact, rounded,
        "mapped equality uses the exact Args snapshot"
    );
}
