use super::model::*;
use super::*;
use crate::registry::{OverlayRegistry, push_synthetic_mode_flag};
use encoding_rs::SHIFT_JIS;
use std::path::Path;

use crate::parser::{
    LightParseOptions, LightWord, ParseMode, ParseOptions, SourceEncoding, parse_bytes,
    parse_light_bytes_with_encoding, parse_light_file, parse_light_shared_source,
    parse_light_source, parse_light_source_with_options, parse_shared_source, parse_source,
};
use crate::sema::command_schema::{
    CommandKind, CommandModeMask, CommandRegistry, CommandSchema, CommandSourceKind,
    EmptyCommandRegistry, FlagArity, FlagArityByMode, FlagSchema, PositionalSchema,
    PositionalSlotSchema, PositionalSourcePolicy, PositionalTailSchema, ReturnBehavior,
    StaticCommandRegistry, ValueShape,
};

fn test_registry(commands: Vec<CommandSchema>) -> StaticCommandRegistry {
    StaticCommandRegistry::try_new(commands).expect("valid test registry")
}

#[test]
fn overlay_registry_prefers_primary_then_embedded_fallback() {
    let primary_command = CommandSchema {
        name: "addAttr".into(),
        kind: CommandKind::Plugin,
        source_kind: CommandSourceKind::Command,
        mode_mask: CommandModeMask {
            create: true,
            edit: false,
            query: false,
        },
        return_behavior: ReturnBehavior::Unknown,
        positionals: PositionalSchema::unconstrained(),
        flags: Vec::new().into(),
    };
    let registry = test_registry(vec![primary_command]);
    let overlay = OverlayRegistry::new(&registry);

    let add_attr = overlay.lookup("addAttr").expect("primary command");
    assert_eq!(add_attr.kind, CommandKind::Plugin);

    let fallback = overlay.lookup("createNode").expect("embedded fallback");
    assert_eq!(fallback.kind, CommandKind::Builtin);
}

#[test]
fn embedded_registry_keeps_script_source_kind() {
    let registry = MayaCommandRegistry::new();
    let schema = registry
        .lookup("addNewShelfTab")
        .expect("embedded schema for addNewShelfTab");
    assert_eq!(schema.kind, CommandKind::Builtin);
    assert_eq!(schema.source_kind, CommandSourceKind::Script);
}

#[test]
fn embedded_registry_synthesizes_mode_flags() {
    let registry = MayaCommandRegistry::new();
    let schema = registry
        .lookup("addAttr")
        .expect("embedded schema for addAttr");
    assert!(
        schema
            .flags
            .iter()
            .any(|flag| flag.long_name.as_ref() == "create")
    );
    assert!(
        schema
            .flags
            .iter()
            .any(|flag| flag.long_name.as_ref() == "edit")
    );
    assert!(
        schema
            .flags
            .iter()
            .any(|flag| flag.long_name.as_ref() == "query")
    );
}

#[test]
fn embedded_registry_keeps_selection_aware_positional_policy() {
    let registry = MayaCommandRegistry::new();
    for command_name in ["ikHandle", "delete", "sets", "polyListComponentConversion"] {
        let schema = registry
            .lookup(command_name)
            .unwrap_or_else(|| panic!("embedded schema for {command_name}"));
        assert_eq!(
            schema.positionals.prefix[0].source_policy,
            PositionalSourcePolicy::ExplicitOrCurrentSelection
        );
    }
}

#[test]
fn embedded_registry_keeps_relaxed_backlog_positional_shapes() {
    let registry = MayaCommandRegistry::new();

    let filter_expand = registry
        .lookup("filterExpand")
        .expect("embedded schema for filterExpand");
    assert!(matches!(
        filter_expand.positionals.tail,
        PositionalTailSchema::Opaque { min: 0, max: None }
    ));

    let shading_node = registry
        .lookup("shadingNode")
        .expect("embedded schema for shadingNode");
    assert_eq!(shading_node.positionals.prefix.len(), 1);
    assert!(matches!(
        shading_node.positionals.tail,
        PositionalTailSchema::Shaped {
            min: 0,
            max: Some(1),
            value_shapes,
        } if value_shapes == [ValueShape::String]
    ));

    let attribute_exists = registry
        .lookup("attributeExists")
        .expect("embedded schema for attributeExists");
    assert_eq!(attribute_exists.positionals.prefix.len(), 2);

    let namespace_info = registry
        .lookup("namespaceInfo")
        .expect("embedded schema for namespaceInfo");
    assert!(namespace_info.positionals.prefix.is_empty());
    assert!(matches!(
        namespace_info.positionals.tail,
        PositionalTailSchema::Opaque { min: 0, max: None }
    ));

    let particle = registry
        .lookup("particle")
        .expect("embedded schema for particle");
    assert_eq!(
        particle.positionals.prefix[0].source_policy,
        PositionalSourcePolicy::ExplicitOrCurrentSelection
    );
    assert!(matches!(
        particle.positionals.tail,
        PositionalTailSchema::Opaque { min: 0, max: None }
    ));
}

#[test]
fn collects_top_level_command_proc_and_other_items() {
    let parse = parse_source("global proc foo() { }\nsetAttr \".tx\" 1;\nint $x = 1;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    assert!(matches!(facts.items[0], MayaTopLevelItem::Proc { .. }));
    assert!(matches!(facts.items[1], MayaTopLevelItem::Command(_)));
    assert!(matches!(facts.items[2], MayaTopLevelItem::Other { .. }));
}

#[test]
fn shared_full_parse_collects_same_top_level_facts() {
    let parse = parse_shared_source("global proc foo() { }\nsetAttr \".tx\" 1;\n".into());
    let facts = collect_top_level_facts_shared(&parse);
    assert!(matches!(facts.items[0], MayaTopLevelItem::Proc { .. }));
    assert!(matches!(facts.items[1], MayaTopLevelItem::Command(_)));
}

#[test]
fn top_level_command_uses_its_own_normalized_invoke_when_capture_contains_command() {
    let parse = parse_source("print `setAttr \".tx\" 1`;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let normalized = command
        .normalized
        .as_ref()
        .expect("expected normalized command");
    assert_eq!(normalized.head, "print");
    assert_eq!(normalized.schema_name, "print");
}

#[test]
fn proc_like_shell_invoke_does_not_disturb_later_command_matching() {
    let parse = parse_source(
        "global proc foo(string $name) { }\ncreateNode transform;\nfoo bar;\nsetAttr \".v\" 1;\n",
    );
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(create_node) = &facts.items[1] else {
        panic!("expected createNode command");
    };
    assert_eq!(
        create_node
            .normalized
            .as_ref()
            .map(|command| command.schema_name.as_str()),
        Some("createNode")
    );
    let MayaTopLevelItem::Command(proc_like) = &facts.items[2] else {
        panic!("expected proc-like shell invoke");
    };
    assert!(proc_like.normalized.is_none());
    let MayaTopLevelItem::Command(set_attr) = &facts.items[3] else {
        panic!("expected setAttr command");
    };
    assert_eq!(
        set_attr
            .normalized
            .as_ref()
            .map(|command| command.schema_name.as_str()),
        Some("setAttr")
    );
}

#[test]
fn raw_items_preserve_exponent_numeric_literals() {
    let parse = parse_source("setAttr \".v\" .5e+2;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.raw_items[1].kind, MayaRawShellItemKind::Numeric);
    assert_eq!(
        command.raw_items[1].value_text(parse.source_view()),
        Some(".5e+2")
    );
    assert_eq!(
        command.raw_items[1].source_text(parse.source_view()),
        ".5e+2"
    );
}

#[test]
fn shared_light_parse_collects_same_light_and_hybrid_facts() {
    let parse =
        parse_light_shared_source("createNode transform -n \"pCube1\" -p \"|group1\";\n".into());
    let light = collect_top_level_facts_light_shared(&parse);
    assert!(matches!(light.items[0], MayaLightTopLevelItem::Command(_)));

    let hybrid = collect_top_level_facts_hybrid_shared(&parse).expect("hybrid facts");
    let MayaTopLevelItem::Command(command) = &hybrid.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.head, "createNode");
}

#[test]
fn set_attr_data_reference_edits_is_specialized_losslessly() {
    let parse = parse_source("setAttr \".ed\" -type \"dataReferenceEdits\" \"rootRN\" \"\" 5;\n");
    assert!(parse.errors.is_empty());
    let mut flags = vec![FlagSchema {
        long_name: "type".into(),
        short_name: Some("typ".into()),
        mode_mask: CommandModeMask {
            create: true,
            edit: true,
            query: true,
        },
        arity_by_mode: FlagArityByMode {
            create: FlagArity::Exact(1),
            edit: FlagArity::Exact(1),
            query: FlagArity::Exact(1),
        },
        value_shapes: vec![ValueShape::String].into(),
        allows_multiple: false,
    }];
    push_synthetic_mode_flag(&mut flags, true, "create", "c");
    push_synthetic_mode_flag(&mut flags, true, "edit", "e");
    push_synthetic_mode_flag(&mut flags, true, "query", "q");
    let command = CommandSchema {
        name: "setAttr".into(),
        kind: CommandKind::Builtin,
        source_kind: CommandSourceKind::Command,
        mode_mask: CommandModeMask {
            create: true,
            edit: true,
            query: true,
        },
        return_behavior: ReturnBehavior::Unknown,
        positionals: PositionalSchema {
            prefix: &[PositionalSlotSchema {
                value_shapes: &[ValueShape::String],
                source_policy: PositionalSourcePolicy::ExplicitOnly,
            }],
            tail: PositionalTailSchema::Opaque { min: 0, max: None },
        },
        flags: flags.into(),
    };
    let registry = test_registry(vec![command]);

    let facts = collect_top_level_facts_with_registry(&parse, &registry);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::SetAttr(set_attr)) = command.specialized.as_ref() else {
        panic!("expected setAttr specialization");
    };
    assert_eq!(
        set_attr.value_kind,
        MayaSetAttrValueKind::DataReferenceEdits
    );
    assert_eq!(
        set_attr
            .type_name
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("dataReferenceEdits")
    );
    assert_eq!(set_attr.values.len(), 3);
    assert_eq!(
        set_attr.values[2].value_text(parse.source_view()),
        Some("5")
    );
}

#[test]
fn create_node_specialization_extracts_parent_and_name() {
    let parse = parse_source("createNode transform -n \"pCube1\" -p \"|group1\";\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::CreateNode(create_node)) = command.specialized.as_ref() else {
        panic!("expected createNode specialization");
    };
    assert_eq!(
        create_node
            .name
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("pCube1")
    );
    assert_eq!(
        create_node
            .parent
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("|group1")
    );
    assert!(!create_node.shared);
}

#[test]
fn create_node_specialization_extracts_shared_flag() {
    let parse = parse_source("createNode transform -s -n \"pCube1\";\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::CreateNode(create_node)) = command.specialized.as_ref() else {
        panic!("expected createNode specialization");
    };
    assert!(create_node.shared);
}

#[test]
fn rename_specialization_extracts_uuid_slot_without_positional_ambiguity() {
    let parse = parse_source("rename -uid \"12345678-1234-1234-1234-123456789abc\";\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::Rename(rename)) = command.specialized.as_ref() else {
        panic!("expected rename specialization");
    };
    assert_eq!(
        rename
            .uuid
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("12345678-1234-1234-1234-123456789abc")
    );
    assert!(rename.source.is_none());
    assert!(rename.target.is_none());
}

#[test]
fn add_attr_specialization_extracts_common_fields_and_preserves_bool_shorthand() {
    let parse = parse_source("addAttr -ln \"foo\" -sn \"f\" -at \"double\" -k -h off -dv 1.5;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::AddAttr(add_attr)) = command.specialized.as_ref() else {
        panic!("expected addAttr specialization");
    };
    assert_eq!(
        add_attr
            .long_name
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("foo")
    );
    assert_eq!(
        add_attr
            .short_name
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("f")
    );
    assert_eq!(
        add_attr
            .attribute_type
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("double")
    );
    assert_eq!(
        add_attr
            .default_value
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("1.5")
    );
    let keyable = add_attr
        .flags
        .iter()
        .find(|flag| flag.canonical_name.as_deref() == Some("keyable"))
        .expect("keyable flag");
    assert!(keyable.args.is_empty());
    let hidden = add_attr
        .flags
        .iter()
        .find(|flag| flag.canonical_name.as_deref() == Some("hidden"))
        .expect("hidden flag");
    assert_eq!(
        hidden
            .args
            .first()
            .map(|arg| arg.preferred_text(parse.source_view())),
        Some("off")
    );
}

#[test]
fn current_unit_specialization_extracts_named_unit_slots() {
    let parse = parse_source("currentUnit -l cm -a deg -t ntsc;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::CurrentUnit(current_unit)) = command.specialized.as_ref()
    else {
        panic!("expected currentUnit specialization");
    };
    assert_eq!(
        current_unit
            .linear
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("cm")
    );
    assert_eq!(
        current_unit
            .angle
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("deg")
    );
    assert_eq!(
        current_unit
            .time
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("ntsc")
    );
}

#[test]
fn requires_specialization_preserves_option_item_order() {
    let parse =
        parse_source("requires -nodeType \"camera\" -dataType \"foo\" \"plugin\" \"1.0\";\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::Requires(requires)) = command.specialized.as_ref() else {
        panic!("expected requires specialization");
    };
    let actual = requires
        .option_items
        .iter()
        .map(|item| item.preferred_text(parse.source_view()))
        .collect::<Vec<_>>();
    assert_eq!(actual, vec!["-nodeType", "camera", "-dataType", "foo"]);
    assert_eq!(
        requires
            .plugin_name
            .as_ref()
            .map(|item| item.preferred_text(parse.source_view())),
        Some("plugin")
    );
    assert_eq!(
        requires
            .plugin_version
            .as_ref()
            .map(|item| item.preferred_text(parse.source_view())),
        Some("1.0")
    );
}

#[test]
fn file_specialization_extracts_reference_fields() {
    let parse = parse_source(
        "file -r -ns \"foo\" -rfn \"fooRN\" -typ \"mayaAscii\" -op \"v=0;\" \"C:/scene.ma\";\n",
    );
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::File(file)) = command.specialized.as_ref() else {
        panic!("expected file specialization");
    };
    assert!(file.is_reference);
    assert_eq!(
        file.namespace
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("foo")
    );
    assert_eq!(
        file.reference_node
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("fooRN")
    );
    assert_eq!(
        file.file_type
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("mayaAscii")
    );
    assert_eq!(
        file.options
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("v=0;")
    );
    assert_eq!(
        file.path
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("C:/scene.ma")
    );
}

#[test]
fn select_and_connect_attr_specializations_expose_named_flags() {
    let select_parse = parse_source("select -ne \"foo\";\n");
    assert!(select_parse.errors.is_empty());
    let select_facts = collect_top_level_facts(&select_parse);
    let MayaTopLevelItem::Command(select_command) = &select_facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::Select(select)) = select_command.specialized.as_ref() else {
        panic!("expected select specialization");
    };
    assert!(select.no_expand);
    assert_eq!(
        select.targets[0].preferred_text(select_parse.source_view()),
        "foo"
    );

    let connect_parse = parse_source("connectAttr -na -l on \"a.tx\" \"b.tx\";\n");
    assert!(connect_parse.errors.is_empty());
    let connect_facts = collect_top_level_facts(&connect_parse);
    let MayaTopLevelItem::Command(connect_command) = &connect_facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaSpecializedCommand::ConnectAttr(connect_attr)) =
        connect_command.specialized.as_ref()
    else {
        panic!("expected connectAttr specialization");
    };
    assert!(connect_attr.next_available);
    assert_eq!(
        connect_attr
            .lock_arg
            .as_ref()
            .map(|item| item.preferred_text(connect_parse.source_view())),
        Some("on")
    );
}

#[test]
fn normalized_flag_accessor_preserves_flag_source_text() {
    let parse = parse_source("setAttr \".v\" -type \"string\" \"hi\";\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let normalized = command
        .normalized
        .as_ref()
        .expect("expected normalized command");
    let flag = normalized
        .items
        .iter()
        .find_map(|item| match item {
            MayaNormalizedCommandItem::Flag(flag) => Some(flag),
            MayaNormalizedCommandItem::Positional(_) => None,
        })
        .expect("expected flag");
    assert_eq!(flag.source_text(parse.source_view()), "-type");
}

#[test]
fn light_flag_accessor_preserves_flag_source_text() {
    let parse = parse_light_source("setAttr \".v\" -type \"string\" \"hi\";\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts_light(&parse);
    let MayaLightTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaLightSpecializedCommand::SetAttr(set_attr)) = command.specialized.as_ref() else {
        panic!("expected light setAttr specialization");
    };
    let flag = set_attr.flags.first().expect("expected flag");
    assert_eq!(flag.source_text(parse.source_view()), "-type");
}

#[test]
fn grouped_expr_raw_item_preserves_full_source_text() {
    let parse = parse_source("setAttr \".b\" -type \"string\" (\"a\" + \"b\");\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.raw_items[3].kind, MayaRawShellItemKind::GroupedExpr);
    assert_eq!(
        command.raw_items[3].source_text(parse.source_view()),
        "(\"a\" + \"b\")"
    );
}

#[test]
fn variable_raw_item_preserves_full_source_text() {
    let parse = parse_source("python $cmd;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.raw_items[0].kind, MayaRawShellItemKind::Variable);
    assert_eq!(
        command.raw_items[0].source_text(parse.source_view()),
        "$cmd"
    );
}

#[test]
fn brace_list_raw_item_preserves_full_source_text() {
    let parse = parse_source("foo {\"a\", \"b\"};\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.raw_items[0].kind, MayaRawShellItemKind::BraceList);
    assert_eq!(
        command.raw_items[0].source_text(parse.source_view()),
        "{\"a\", \"b\"}"
    );
}

#[test]
fn vector_literal_raw_item_preserves_full_source_text() {
    let parse = parse_source("move <<1, 2, 3>>;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(
        command.raw_items[0].kind,
        MayaRawShellItemKind::VectorLiteral
    );
    assert_eq!(
        command.raw_items[0].source_text(parse.source_view()),
        "<<1, 2, 3>>"
    );
}

#[test]
fn capture_raw_item_preserves_full_source_text() {
    let parse = parse_source("python `someCmd -q`;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.raw_items[0].kind, MayaRawShellItemKind::Capture);
    assert_eq!(
        command.raw_items[0].source_text(parse.source_view()),
        "`someCmd -q`"
    );
}

#[test]
fn mixed_shell_word_kinds_remain_lossless() {
    let parse = parse_source("python $cmd (\"a\" + \"b\") {\"x\"} <<1, 2, 3>> `someCmd -q`;\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let actual = command
        .raw_items
        .iter()
        .map(|item| item.source_text(parse.source_view()))
        .collect::<Vec<_>>();
    assert_eq!(
        actual,
        vec![
            "$cmd",
            "(\"a\" + \"b\")",
            "{\"x\"}",
            "<<1, 2, 3>>",
            "`someCmd -q`"
        ]
    );
}

#[test]
fn grouped_expr_raw_item_uses_display_range_for_lossless_slice() {
    let bytes = b"setAttr \".b\" -type \"string\" (\"\xFF\");\n";
    let parse = parse_bytes(bytes);
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.raw_items[3].kind, MayaRawShellItemKind::GroupedExpr);
    assert_eq!(
        command.raw_items[3].source_text(parse.source_view()),
        "(\"\u{FFFD}\")"
    );
}

#[test]
fn quoted_string_raw_item_exposes_text_range_used_for_value_text() {
    let parse = parse_source("setAttr \".label\" -type \"string\" \"value\";\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let item = &command.raw_items[3];
    assert_eq!(item.kind, MayaRawShellItemKind::QuotedString);
    assert_eq!(item.text_range(), Some(item.span));
    assert_eq!(item.source_text(parse.source_view()), "\"value\"");
    assert_eq!(item.value_text(parse.source_view()), Some("value"));
}

#[test]
fn grouped_expr_raw_item_has_no_text_range() {
    let parse = parse_source("setAttr \".b\" -type \"string\" (\"a\" + \"b\");\n");
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts(&parse);
    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let item = &command.raw_items[3];
    assert_eq!(item.kind, MayaRawShellItemKind::GroupedExpr);
    assert_eq!(item.text_range(), None);
    assert_eq!(item.value_text(parse.source_view()), None);
}

#[test]
fn light_collector_keeps_heavy_set_attr_tail_opaque() {
    let parse = parse_light_source(
        "createNode mesh -n \"meshShape\";\nsetAttr \".fc[0]\" -type \"polyFaces\" f 4 0 1 2 3 mu 0 4 0 1 2 3;\n",
    );
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts_light(&parse);
    assert!(matches!(facts.items[0], MayaLightTopLevelItem::Command(_)));
    let MayaLightTopLevelItem::Command(command) = &facts.items[1] else {
        panic!("expected command");
    };
    let Some(MayaLightSpecializedCommand::SetAttr(set_attr)) = command.specialized.as_ref() else {
        panic!("expected light setAttr specialization");
    };
    assert_eq!(
        set_attr
            .attr_path
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some(".fc[0]")
    );
    assert_eq!(
        set_attr
            .type_name
            .as_ref()
            .and_then(|item| item.value_text(parse.source_view())),
        Some("polyFaces")
    );
    assert!(command.opaque_tail.is_none());
    assert_eq!(
        set_attr.prefix_values[0].value_text(parse.source_view()),
        Some("f")
    );
}

#[test]
fn light_collector_uses_opaque_tail_when_prefix_limit_hits() {
    let parse = parse_light_source_with_options(
        "setAttr \".pt\" -type \"doubleArray\" 1 2 3 4 5 6 7 8 9 10;\n",
        LightParseOptions {
            max_prefix_words: 5,
            max_prefix_bytes: 32,
            ..LightParseOptions::default()
        },
    );
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts_light(&parse);
    let MayaLightTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    let Some(MayaLightSpecializedCommand::SetAttr(set_attr)) = command.specialized.as_ref() else {
        panic!("expected light setAttr specialization");
    };
    assert!(command.opaque_tail.is_some());
    assert!(set_attr.opaque_tail.is_some());
    assert_eq!(set_attr.prefix_values.len(), 2);
}

#[test]
fn light_collector_can_smoke_tmp_mesh_sample_when_present() {
    let path = Path::new("/mnt/e/Projects/RnD/MayaMelParser/tmp/Test_Mesh_Horizon.ma");
    if !path.exists() {
        return;
    }
    let parse = parse_light_file(path).expect("light parse sample");
    assert!(parse.decode_errors.is_empty());
    assert!(parse.errors.is_empty());
    let facts = collect_top_level_facts_light(&parse);
    assert!(!facts.items.is_empty());
}

#[test]
fn hybrid_collector_matches_full_shape_for_non_opaque_command() {
    let full = collect_top_level_facts(&parse_source(
        "createNode transform -n \"pCube1\" -p \"|group1\";\n",
    ));
    let light = parse_light_source("createNode transform -n \"pCube1\" -p \"|group1\";\n");
    let hybrid = collect_top_level_facts_hybrid(&light).expect("hybrid facts");

    let MayaTopLevelItem::Command(full_command) = &full.items[0] else {
        panic!("expected full command");
    };
    let MayaTopLevelItem::Command(hybrid_command) = &hybrid.items[0] else {
        panic!("expected hybrid command");
    };
    assert_eq!(hybrid_command.head, full_command.head);
    assert_eq!(hybrid_command.raw_items, full_command.raw_items);
    assert_eq!(hybrid_command.normalized, full_command.normalized);
    assert_eq!(hybrid_command.specialized, full_command.specialized);
    assert_eq!(full_command.promotion_kind, MayaPromotionKind::FullParse);
    assert_eq!(
        hybrid_command.promotion_kind,
        MayaPromotionKind::LightSynthesized
    );
}

#[test]
fn hybrid_collector_promotes_opaque_set_attr_tail_to_full_raw_items() {
    let parse = parse_light_source_with_options(
        "setAttr \".pt\" -type \"doubleArray\" 1 2 3 4 5 6 7 8 9 10;\n",
        LightParseOptions {
            max_prefix_words: 5,
            max_prefix_bytes: 32,
            ..LightParseOptions::default()
        },
    );
    let hybrid = collect_top_level_facts_hybrid(&parse).expect("hybrid facts");
    let MayaTopLevelItem::Command(command) = &hybrid.items[0] else {
        panic!("expected command");
    };
    assert!(command.raw_items.len() > 5);
    assert_eq!(
        command.promotion_kind,
        MayaPromotionKind::OpaqueTailPromoted
    );
    let Some(MayaSpecializedCommand::SetAttr(set_attr)) = command.specialized.as_ref() else {
        panic!("expected setAttr specialization");
    };
    assert_eq!(set_attr.values.len(), 10);
}

#[test]
fn promoted_command_keeps_cp932_source_slices() {
    let parse = parse_light_bytes_with_encoding(
        SHIFT_JIS
            .encode("setAttr \".名\" -type \"string\" \"値\";\n")
            .0
            .as_ref(),
        SourceEncoding::Cp932,
    );
    let hybrid = collect_top_level_facts_hybrid_with_registry(
        &parse,
        &EmptyCommandRegistry,
        MayaPromotionPolicy::Always,
    )
    .expect("hybrid facts");
    let MayaTopLevelItem::Command(command) = &hybrid.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.promotion_kind, MayaPromotionKind::PolicyPromoted);
    assert_eq!(
        command.raw_items[0].source_text(parse.source_view()),
        "\".名\""
    );
    assert_eq!(
        command.raw_items[3].value_text(parse.source_view()),
        Some("値")
    );
}

#[test]
fn hybrid_always_policy_promotes_file_command_with_grouped_expr() {
    let parse = parse_light_source("file -command (\"print \\\"hi\\\";\");\n");
    let hybrid = collect_top_level_facts_hybrid_with_registry(
        &parse,
        &EmptyCommandRegistry,
        MayaPromotionPolicy::Always,
    )
    .expect("hybrid facts");
    let MayaTopLevelItem::Command(command) = &hybrid.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.promotion_kind, MayaPromotionKind::PolicyPromoted);
    assert_eq!(command.raw_items[1].kind, MayaRawShellItemKind::GroupedExpr);
    let Some(MayaSpecializedCommand::File(file)) = command.specialized.as_ref() else {
        panic!("expected file specialization");
    };
    assert_eq!(file.flags.len(), 1);
}

#[test]
fn hybrid_custom_decider_promotes_grouped_expr_command() {
    let parse = parse_light_source("file -command (\"print \\\"hi\\\";\");\n");
    let decider: &dyn MayaPromotionDecider = &|candidate: MayaPromotionCandidate<'_>| {
        candidate
            .command
            .words
            .iter()
            .any(|word| matches!(word, LightWord::GroupedExpr { .. }))
    };
    let hybrid = collect_top_level_facts_hybrid_with_decider(
        &parse,
        &MayaPromotionOptions::default(),
        decider,
    )
    .expect("hybrid facts");

    let MayaTopLevelItem::Command(command) = &hybrid.items[0] else {
        panic!("expected command");
    };
    assert_eq!(
        command.promotion_kind,
        MayaPromotionKind::CustomDeciderPromoted
    );
    assert_eq!(command.raw_items[1].kind, MayaRawShellItemKind::GroupedExpr);
}

#[test]
fn opaque_tail_promotion_takes_precedence_over_custom_decider() {
    let parse = parse_light_source_with_options(
        "setAttr \".pt\" -type \"doubleArray\" 1 2 3 4 5 6 7 8 9 10;\n",
        LightParseOptions {
            max_prefix_words: 5,
            max_prefix_bytes: 32,
            ..LightParseOptions::default()
        },
    );
    let hybrid = collect_top_level_facts_hybrid_with_decider(
        &parse,
        &MayaPromotionOptions::default(),
        &|_: MayaPromotionCandidate<'_>| true,
    )
    .expect("hybrid facts");

    let MayaTopLevelItem::Command(command) = &hybrid.items[0] else {
        panic!("expected command");
    };
    assert_eq!(
        command.promotion_kind,
        MayaPromotionKind::OpaqueTailPromoted
    );
}

#[test]
fn hybrid_promotes_data_reference_edits_tail() {
    let parse = parse_light_source_with_options(
        "setAttr \".ed\" -type \"dataReferenceEdits\" \"rootRN\" \"\" 5;\n",
        LightParseOptions {
            max_prefix_words: 3,
            max_prefix_bytes: 24,
            ..LightParseOptions::default()
        },
    );
    let mut flags = vec![FlagSchema {
        long_name: "type".into(),
        short_name: Some("typ".into()),
        mode_mask: CommandModeMask {
            create: true,
            edit: true,
            query: true,
        },
        arity_by_mode: FlagArityByMode {
            create: FlagArity::Exact(1),
            edit: FlagArity::Exact(1),
            query: FlagArity::Exact(1),
        },
        value_shapes: vec![ValueShape::String].into(),
        allows_multiple: false,
    }];
    push_synthetic_mode_flag(&mut flags, true, "create", "c");
    push_synthetic_mode_flag(&mut flags, true, "edit", "e");
    push_synthetic_mode_flag(&mut flags, true, "query", "q");
    let command = CommandSchema {
        name: "setAttr".into(),
        kind: CommandKind::Builtin,
        source_kind: CommandSourceKind::Command,
        mode_mask: CommandModeMask {
            create: true,
            edit: true,
            query: true,
        },
        return_behavior: ReturnBehavior::Unknown,
        positionals: PositionalSchema {
            prefix: &[PositionalSlotSchema {
                value_shapes: &[ValueShape::String],
                source_policy: PositionalSourcePolicy::ExplicitOnly,
            }],
            tail: PositionalTailSchema::Opaque { min: 0, max: None },
        },
        flags: flags.into(),
    };
    let registry = test_registry(vec![command]);

    let hybrid = collect_top_level_facts_hybrid_with_registry(
        &parse,
        &registry,
        MayaPromotionPolicy::default(),
    )
    .expect("hybrid facts");
    let MayaTopLevelItem::Command(command) = &hybrid.items[0] else {
        panic!("expected command");
    };
    assert_eq!(
        command.promotion_kind,
        MayaPromotionKind::OpaqueTailPromoted
    );
    let Some(MayaSpecializedCommand::SetAttr(set_attr)) = command.specialized.as_ref() else {
        panic!("expected setAttr specialization");
    };
    assert_eq!(
        set_attr.value_kind,
        MayaSetAttrValueKind::DataReferenceEdits
    );
    assert_eq!(set_attr.values.len(), 3);
}

#[test]
fn hybrid_report_keeps_light_command_when_policy_promotion_fails() {
    let parse = parse_light_source("createNode transform -n \"pCube1\"");
    let report = collect_top_level_facts_hybrid_report(
        &parse,
        &MayaPromotionOptions {
            policy: MayaPromotionPolicy::Always,
            ..MayaPromotionOptions::default()
        },
    );

    assert_eq!(report.promotion_diagnostics.len(), 1);
    assert_eq!(
        report.promotion_diagnostics[0].attempted_kind,
        MayaPromotionKind::PolicyPromoted
    );
    let MayaTopLevelItem::Command(command) = &report.facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.promotion_kind, MayaPromotionKind::LightSynthesized);
}

#[test]
fn hybrid_report_keeps_light_command_when_custom_decider_promotion_fails() {
    let parse = parse_light_source("file -command (\"print \\\"hi\\\";\")");
    let report = collect_top_level_facts_hybrid_report_with_decider(
        &parse,
        &MayaPromotionOptions::default(),
        &|candidate: MayaPromotionCandidate<'_>| {
            candidate
                .command
                .words
                .iter()
                .any(|word| matches!(word, LightWord::GroupedExpr { .. }))
        },
    );

    assert_eq!(report.promotion_diagnostics.len(), 1);
    assert_eq!(
        report.promotion_diagnostics[0].attempted_kind,
        MayaPromotionKind::CustomDeciderPromoted
    );
    let MayaTopLevelItem::Command(command) = &report.facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.promotion_kind, MayaPromotionKind::LightSynthesized);
}

#[test]
fn hybrid_report_collects_set_attr_validation_diagnostic() {
    let parse = parse_light_source("setAttr \".tx\" -type \"string\";\n");
    let report = collect_top_level_facts_hybrid_report(&parse, &MayaPromotionOptions::default());

    assert_eq!(report.validation_diagnostics.len(), 1);
    assert_eq!(
        report.validation_diagnostics[0].head.as_deref(),
        Some("setAttr")
    );
    assert_eq!(
        report.validation_diagnostics[0].message,
        "setAttr requires at least one value after the attribute path when -type is present"
    );
}

#[test]
fn hybrid_report_leaves_validation_diagnostics_empty_for_valid_set_attr() {
    let parse = parse_light_source("setAttr \".tx\" -type \"string\" \"value\";\n");
    let report = collect_top_level_facts_hybrid_report(&parse, &MayaPromotionOptions::default());

    assert!(report.validation_diagnostics.is_empty());
}

#[test]
fn hybrid_strict_options_forward_parse_mode_to_promotion() {
    let parse = parse_light_source("createNode transform -n \"pCube1\"");
    let facts = collect_top_level_facts_hybrid_with_registry_and_options(
        &parse,
        &EmptyCommandRegistry,
        &MayaPromotionOptions {
            policy: MayaPromotionPolicy::Always,
            parse_options: ParseOptions {
                mode: ParseMode::AllowTrailingStmtWithoutSemi,
                ..ParseOptions::default()
            },
        },
    )
    .expect("hybrid facts");

    let MayaTopLevelItem::Command(command) = &facts.items[0] else {
        panic!("expected command");
    };
    assert_eq!(command.promotion_kind, MayaPromotionKind::PolicyPromoted);
    assert_eq!(
        command.raw_items[0].source_text(parse.source_view()),
        "transform"
    );
}

#[test]
fn selective_collector_extracts_target_commands_only() {
    let source = "rename \"a\" \"b\";\ncreateNode mesh -n \"meshShape\";\nsetAttr \".b\" yes;\n";
    let mut items = Vec::new();
    let report =
        collect_selective_top_level_source_with_sink(source, &mut |item: MayaSelectiveItem| {
            items.push(item)
        });

    assert!(report.errors.is_empty());
    assert_eq!(items.len(), 2);
    let MayaSelectiveItem::CreateNode(create_node) = &items[0] else {
        panic!("expected createNode item");
    };
    assert_eq!(
        create_node
            .node_type_range
            .map(|range| report.source_slice(range)),
        Some("mesh")
    );
    let MayaSelectiveItem::SetAttr(set_attr) = &items[1] else {
        panic!("expected setAttr item");
    };
    assert_eq!(set_attr.tracked_attr, Some(MayaTrackedSetAttrAttr::B));
}

#[test]
fn selective_collector_can_include_other_commands_as_passthrough() {
    let source = "rename \"a\" \"b\";\ncreateNode transform;\n";
    let mut items = Vec::new();
    let report = collect_selective_top_level_source_with_options_and_sink(
        source,
        LightParseOptions::default(),
        &MayaSelectiveOptions {
            passthrough: MayaSelectivePassthrough::IncludeOtherCommands,
        },
        &DefaultMayaSelectiveSetAttrSelector,
        &mut |item: MayaSelectiveItem| items.push(item),
    );

    assert!(report.errors.is_empty());
    assert_eq!(items.len(), 2);
    let MayaSelectiveItem::OtherCommand { head_range, .. } = &items[0] else {
        panic!("expected passthrough command");
    };
    assert_eq!(report.source_slice(*head_range), "rename");
}

#[test]
fn selective_collector_keeps_opaque_set_attr_without_promotion() {
    let source = "setAttr \".f\" -type \"doubleArray\" 1 2 3 4 5 6 7 8 9 10;\n";
    let mut items = Vec::new();
    let report = collect_selective_top_level_source_with_options_and_sink(
        source,
        LightParseOptions {
            max_prefix_words: 4,
            max_prefix_bytes: 24,
            ..LightParseOptions::default()
        },
        &MayaSelectiveOptions::default(),
        &DefaultMayaSelectiveSetAttrSelector,
        &mut |item: MayaSelectiveItem| items.push(item),
    );

    assert!(report.errors.is_empty());
    let MayaSelectiveItem::SetAttr(set_attr) = &items[0] else {
        panic!("expected setAttr item");
    };
    assert_eq!(set_attr.tracked_attr, Some(MayaTrackedSetAttrAttr::F));
    assert!(set_attr.opaque_tail.is_some());
}

#[test]
fn selective_collector_uses_custom_set_attr_selector() {
    let source = "setAttr \".custom\" 1;\n";
    let mut items = Vec::new();
    let report = collect_selective_top_level_source_with_options_and_sink(
        source,
        LightParseOptions::default(),
        &MayaSelectiveOptions::default(),
        &|attr_path: &str| (attr_path == ".custom").then_some(MayaTrackedSetAttrAttr::Fn),
        &mut |item: MayaSelectiveItem| items.push(item),
    );

    assert!(report.errors.is_empty());
    let MayaSelectiveItem::SetAttr(set_attr) = &items[0] else {
        panic!("expected setAttr item");
    };
    assert_eq!(set_attr.tracked_attr, Some(MayaTrackedSetAttrAttr::Fn));
}

#[test]
fn selective_collector_preserves_cp932_source_slices() {
    let bytes = SHIFT_JIS
        .encode("setAttr \".名\" -type \"string\" \"値\";\n")
        .0;
    let mut items = Vec::new();
    let report = collect_selective_top_level_bytes_with_encoding_and_sink(
        bytes.as_ref(),
        SourceEncoding::Cp932,
        &MayaSelectiveOptions::default(),
        &DefaultMayaSelectiveSetAttrSelector,
        &mut |item: MayaSelectiveItem| items.push(item),
    );

    let MayaSelectiveItem::SetAttr(set_attr) = &items[0] else {
        panic!("expected setAttr item");
    };
    assert_eq!(
        set_attr
            .attr_path_range
            .map(|range| report.source_slice(range)),
        Some("\".名\"")
    );
}
