# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

from mirage.commands.spec import (SPECS, CommandSpec, Operand, Option,
                                  ParsedArgs, parse_command, parse_to_kwargs)


def test_parse_simple_path_args():
    spec = CommandSpec(rest=Operand(type="path"))
    parsed = parse_command(spec, ["a.txt", "b.txt"], cwd="/home")
    assert parsed.args == [("/home/a.txt", "path"), ("/home/b.txt", "path")]
    assert parsed.flags == {}


def test_parse_absolute_path():
    spec = CommandSpec(rest=Operand(type="path"))
    parsed = parse_command(spec, ["/data/file.csv"], cwd="/home")
    assert parsed.args == [("/data/file.csv", "path")]


def test_parse_bool_flags():
    spec = CommandSpec(options=(Option(short="-r"), Option(short="-f"),
                                Option(short="-v")),
                       rest=Operand(type="path"))
    parsed = parse_command(spec, ["-rf", "file.txt"], cwd="/")
    assert parsed.flags == {"-r": True, "-f": True}
    assert parsed.args == [("/file.txt", "path")]


def test_parse_value_flag_space():
    spec = CommandSpec(options=(Option(short="-n", type="str"), ),
                       rest=Operand(type="path"))
    parsed = parse_command(spec, ["-n", "10", "file.txt"], cwd="/")
    assert parsed.flags == {"-n": "10"}
    assert parsed.args == [("/file.txt", "path")]


def test_parse_value_flag_joined():
    spec = CommandSpec(options=(Option(short="-n", type="str"), ),
                       rest=Operand(type="path"))
    parsed = parse_command(spec, ["-n10", "file.txt"], cwd="/")
    assert parsed.flags == {"-n": "10"}
    assert parsed.args == [("/file.txt", "path")]


def test_parse_long_bool_flag():
    spec = CommandSpec(options=(Option(long="--hidden"), ),
                       rest=Operand(type="path"))
    parsed = parse_command(spec, ["--hidden", "dir/"], cwd="/")
    assert parsed.flags == {"--hidden": True}
    assert parsed.args == [("/dir", "path")]


def test_parse_long_value_flag():
    spec = CommandSpec(options=(Option(long="--type", type="str"), ),
                       rest=Operand(type="path"))
    parsed = parse_command(spec, ["--type", "py", "src/"], cwd="/")
    assert parsed.flags == {"--type": "py"}
    assert parsed.args == [("/src", "path")]


def test_parse_positional_text_then_path():
    spec = CommandSpec(
        options=(Option(short="-i"), Option(short="-v")),
        positional=(Operand(type="str"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-i", "pattern", "file1.txt", "file2.txt"],
                           cwd="/data")
    assert parsed.flags == {"-i": True}
    assert parsed.args == [
        ("pattern", "str"),
        ("/data/file1.txt", "path"),
        ("/data/file2.txt", "path"),
    ]


def test_search_spec_parses_query_paths_and_options():
    parsed = parse_command(
        SPECS["search"],
        [
            "--method", "hybrid", "--top-k", "5", "--threshold", "0.4",
            "login docs", "guides/*.md"
        ],
        cwd="/knowledge",
    )

    assert parsed.flag("--method") == "hybrid"
    assert parsed.flag("--top-k") == "5"
    assert parsed.flag("--threshold") == "0.4"
    assert parsed.args == [("login docs", "str"),
                           ("/knowledge/guides/*.md", "path")]


def test_parse_double_dash_stops_flags():
    spec = CommandSpec(options=(Option(short="-r"), ),
                       rest=Operand(type="path"))
    parsed = parse_command(spec, ["--", "-r"], cwd="/")
    assert parsed.flags == {}
    assert parsed.args == [("/-r", "path")]


def test_parse_text_rest():
    spec = CommandSpec(rest=Operand(type="str"))
    parsed = parse_command(spec, ["hello", "world"], cwd="/")
    assert parsed.args == [("hello", "str"), ("world", "str")]


def test_parsed_args_paths():
    p = ParsedArgs(flags={},
                   args=[("/a", "path"), ("text", "str"), ("/b", "path")])
    assert p.paths() == ["/a", "/b"]


def test_parsed_args_texts():
    p = ParsedArgs(flags={}, args=[("/a", "path"), ("text", "str")])
    assert p.texts() == ["text"]


def test_parsed_args_flag():
    p = ParsedArgs(flags={"-n": "10", "-r": True}, args=[])
    assert p.flag("-n") == "10"
    assert p.flag("-r") is True
    assert p.flag("-x") is None
    assert p.flag("-x", "default") == "default"


def test_parse_combined_bool_and_value():
    spec = CommandSpec(options=(Option(short="-r"), Option(short="-i"),
                                Option(short="-m", type="str")),
                       rest=Operand(type="path"))
    parsed = parse_command(spec, ["-ri", "-m", "5", "file.txt"], cwd="/")
    assert parsed.flags == {"-r": True, "-i": True, "-m": "5"}
    assert parsed.args == [("/file.txt", "path")]


def test_clustered_flags_with_unknown_short_reported_as_invalid():
    # Regression: a real user ran `grep -RIl "Base3\|base3" /r2/Review` and
    # the pattern + path got swapped because `-I` was missing from the spec.
    # The offending character is reported as invalid instead of the token
    # becoming the pattern and shifting the real pattern into the paths.
    spec_missing_I = CommandSpec(
        options=(Option(short="-R"),
                 Option(short="-l")),  # -I deliberately missing
        positional=(Operand(type="str"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec_missing_I,
                           ["-RIl", "Base3\\|base3", "/r2/Review"],
                           cwd="/")
    assert parsed.texts() == ["Base3\\|base3"]
    assert parsed.paths() == ["/r2/Review"]
    assert parsed.invalid_options == ["I"]


def test_clustered_flags_with_all_known_short_classifies_correctly():
    spec_full = CommandSpec(
        options=(Option(short="-R"), Option(short="-I"), Option(short="-l")),
        positional=(Operand(type="str"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec_full, ["-RIl", "Base3\\|base3", "/r2/Review"],
                           cwd="/")
    assert parsed.flags == {"-R": True, "-I": True, "-l": True}
    assert parsed.texts() == ["Base3\\|base3"]
    assert parsed.paths() == ["/r2/Review"]


def test_grep_spec_recognizes_capital_I_for_ignore_binary():
    grep_spec = SPECS["grep"]
    parsed = parse_command(grep_spec, ["-RIl", "Base3\\|base3", "/r2/Review"],
                           cwd="/")
    assert parsed.flags.get("-I") is True
    assert parsed.flags.get("-R") is True
    assert parsed.flags.get("-l") is True
    assert parsed.texts() == ["Base3\\|base3"]
    assert parsed.paths() == ["/r2/Review"]


def test_all_commands_have_specs():
    expected = {
        "ls",
        "stat",
        "pwd",
        "find",
        "tree",
        "du",
        "df",
        "cat",
        "head",
        "tail",
        "wc",
        "md5",
        "diff",
        "file",
        "nl",
        "grep",
        "search",
        "rg",
        "sort",
        "uniq",
        "cut",
        "mkdir",
        "touch",
        "chmod",
        "chown",
        "chgrp",
        "cp",
        "mv",
        "rm",
        "sed",
        "echo",
        "tee",
        "tr",
        "curl",
        "wget",
        "jq",
        "awk",
        "base64",
        "bash",
        "tar",
        "gzip",
        "gunzip",
        "zip",
        "unzip",
        "md5sum",
        "sha1sum",
        "sha256sum",
        "sha384sum",
        "sha512sum",
        "rmdir",
        "unlink",
        "tac",
        "paste",
        "ln",
        "readlink",
        "basename",
        "dirname",
        "realpath",
        "printf",
        "seq",
        "split",
        "xxd",
        "patch",
        "shuf",
        "comm",
        "column",
        "fold",
        "fmt",
        "cmp",
        "iconv",
        "strings",
        "rev",
        "zcat",
        "zgrep",
        "mktemp",
        "bc",
        "expr",
        "date",
        "csplit",
        "expand",
        "unexpand",
        "tsort",
        "look",
        "sleep",
        "join",
        "python",
        "python3",
        "js",
        "node",
        "history",
        "numfmt",
        "od",
        "truncate",
    }
    assert set(SPECS.keys()) == expected


def test_grep_spec_parses_correctly():
    spec = SPECS["grep"]
    parsed = parse_command(spec, ["-rni", "pattern", "/data/file.txt"],
                           cwd="/")
    assert parsed.flags["-r"] is True
    assert parsed.flags["-n"] is True
    assert parsed.flags["-i"] is True
    assert parsed.args[0] == ("pattern", "str")
    assert parsed.args[1] == ("/data/file.txt", "path")


def test_head_spec_parses_n_flag():
    spec = SPECS["head"]
    parsed = parse_command(spec, ["-n", "20", "file.txt"], cwd="/data")
    assert parsed.flag("--lines") == "20"
    assert parsed.paths() == ["/data/file.txt"]


def test_head_spec_parses_joined_n():
    spec = SPECS["head"]
    parsed = parse_command(spec, ["-n20", "file.txt"], cwd="/")
    assert parsed.flag("--lines") == "20"
    assert parsed.paths() == ["/file.txt"]


def test_echo_spec_text_args():
    spec = SPECS["echo"]
    parsed = parse_command(spec, ["hello", "world"], cwd="/")
    assert parsed.texts() == ["hello", "world"]


def test_rm_spec_combined_flags():
    spec = SPECS["rm"]
    parsed = parse_command(spec, ["-rf", "a", "b", "c"], cwd="/data")
    assert parsed.flags["-r"] is True
    assert parsed.flags["-f"] is True
    assert parsed.paths() == ["/data/a", "/data/b", "/data/c"]


def test_cp_spec():
    spec = SPECS["cp"]
    parsed = parse_command(spec, ["-r", "src/", "dst/"], cwd="/data")
    assert parsed.flags["-r"] is True
    assert parsed.paths() == ["/data/src", "/data/dst"]


def test_rg_spec_with_long_flags():
    spec = SPECS["rg"]
    parsed = parse_command(spec,
                           ["--type", "py", "--hidden", "pattern", "src/"],
                           cwd="/")
    assert parsed.flag("--type") == "py"
    assert parsed.flag("--hidden") is True
    assert parsed.args[0] == ("pattern", "str")
    assert parsed.args[1] == ("/src", "path")


def test_find_spec():
    spec = SPECS["find"]
    parsed = parse_command(spec, ["/data", "-name", "*.py", "-type", "f"],
                           cwd="/")
    assert parsed.paths() == ["/data"]
    assert parsed.flag("-name") == ["*.py"]
    assert parsed.flag("-type") == ["f"]


def test_curl_spec():
    spec = SPECS["curl"]
    parsed = parse_command(spec, ["-H", "Accept: json", "http://example.com"],
                           cwd="/")
    assert parsed.flag("-H") == "Accept: json"
    assert parsed.texts() == ["http://example.com"]


def test_wget_spec():
    spec = SPECS["wget"]
    parsed = parse_command(spec, ["http://example.com/file.zip", "out.zip"],
                           cwd="/data")
    assert parsed.args[0] == ("http://example.com/file.zip", "str")
    assert parsed.args[1] == ("/data/out.zip", "path")


def test_sed_spec():
    spec = SPECS["sed"]
    parsed = parse_command(spec, ["-i", "s/foo/bar/", "file.txt"], cwd="/data")
    assert parsed.flag("-i") is True
    assert parsed.args[0] == ("s/foo/bar/", "str")
    assert parsed.args[1] == ("/data/file.txt", "path")


def test_sort_spec():
    spec = SPECS["sort"]
    parsed = parse_command(spec, ["-k", "2", "-t", ",", "-rn", "data.csv"],
                           cwd="/")
    assert parsed.flag("--key") == ["2"]
    assert parsed.flag("--field-separator") == ","
    assert parsed.flag("--reverse") is True
    assert parsed.flag("--numeric-sort") is True
    assert parsed.paths() == ["/data.csv"]


def test_parse_to_kwargs_short_bool():
    parsed = ParsedArgs(flags={"-l": True, "-a": True}, args=[])
    kw = parse_to_kwargs(parsed)
    assert kw == {"args_l": True, "a": True}


def test_parse_to_kwargs_short_value():
    parsed = ParsedArgs(flags={"-n": "5"}, args=[])
    kw = parse_to_kwargs(parsed)
    assert kw == {"n": "5"}


def test_parse_to_kwargs_long_flags():
    parsed = ParsedArgs(flags={"--hidden": True, "--type": "py"}, args=[])
    kw = parse_to_kwargs(parsed)
    assert kw == {"hidden": True, "type": "py"}


def test_parse_to_kwargs_mixed():
    parsed = ParsedArgs(flags={
        "-i": True,
        "--glob": "*.py",
        "-m": "10"
    },
                        args=[])
    kw = parse_to_kwargs(parsed)
    assert kw == {"i": True, "glob": "*.py", "m": "10"}


def test_parse_to_kwargs_ambiguous_names():
    parsed = ParsedArgs(flags={"-l": True, "-O": True, "-I": True}, args=[])
    kw = parse_to_kwargs(parsed)
    assert kw == {"args_l": True, "args_O": True, "args_I": True}


def test_parse_to_kwargs_dash_one_maps_to_args_1():
    parsed = ParsedArgs(flags={"-1": True}, args=[])
    kw = parse_to_kwargs(parsed)
    assert kw == {"args_1": True}


def test_ls_spec_parses_dash_one_and_dash_l_together():
    parsed = parse_command(SPECS["ls"], ["-1", "-l", "/data"], cwd="/")
    kw = parse_to_kwargs(parsed)
    assert kw["args_1"] is True
    assert kw["args_l"] is True


def test_parse_to_kwargs_empty():
    parsed = ParsedArgs(flags={}, args=[])
    kw = parse_to_kwargs(parsed)
    assert kw == {}


def test_cache_flag_single_path():
    spec = SPECS["grep"]
    parsed = parse_command(spec,
                           ["pattern", "file.txt", "--cache", "file.txt"], "/")
    assert parsed.cache_paths == ["/file.txt"]


def test_cache_flag_multiple_paths():
    spec = SPECS["grep"]
    parsed = parse_command(
        spec, ["pattern", "f1.txt", "--cache", "f1.txt", "f2.txt"], "/")
    assert parsed.cache_paths == ["/f1.txt", "/f2.txt"]


def test_cache_flag_empty():
    spec = SPECS["cat"]
    parsed = parse_command(spec, ["file.txt"], "/")
    assert parsed.cache_paths == []


def test_cache_flag_with_other_flags():
    spec = SPECS["grep"]
    parsed = parse_command(
        spec, ["-i", "pattern", "file.txt", "--cache", "file.txt"], "/")
    assert parsed.cache_paths == ["/file.txt"]
    assert parsed.flags.get("-i") is True


def test_option_bool_flag():
    opt = Option(short="-v")
    assert opt.short == "-v"
    assert opt.long is None
    assert opt.type == "bool"


def test_option_value_flag():
    opt = Option(short="-n", type="str")
    assert opt.type == "str"


def test_option_long_with_alias():
    opt = Option(short="-r", long="--recursive")
    assert opt.short == "-r"
    assert opt.long == "--recursive"


def test_option_path_value():
    opt = Option(short="-f", type="path")
    assert opt.type == "path"


def test_operand_default():
    op = Operand()
    assert op.type == "path"


def test_operand_text():
    op = Operand(type="str")
    assert op.type == "str"


def test_command_spec_new_style():
    spec = CommandSpec(
        options=(
            Option(short="-r"),
            Option(short="-n", type="str"),
            Option(long="--hidden"),
        ),
        positional=(Operand(type="str"), ),
        rest=Operand(type="path"),
    )
    assert len(spec.options) == 3
    assert len(spec.positional) == 1
    assert spec.rest.type == "path"


def test_parse_new_spec_bool_flag():
    spec = CommandSpec(
        options=(Option(short="-r"), Option(short="-f")),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-rf", "file.txt"], cwd="/")
    assert parsed.flags == {"-r": True, "-f": True}
    assert parsed.args == [("/file.txt", "path")]


def test_parse_new_spec_value_flag():
    spec = CommandSpec(
        options=(Option(short="-n", type="str"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-n", "10", "file.txt"], cwd="/")
    assert parsed.flags == {"-n": "10"}
    assert parsed.args == [("/file.txt", "path")]


def test_parse_new_spec_long_flags():
    spec = CommandSpec(
        options=(
            Option(long="--hidden"),
            Option(long="--type", type="str"),
        ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["--hidden", "--type", "py", "src/"], cwd="/")
    assert parsed.flags == {"--hidden": True, "--type": "py"}
    assert parsed.args == [("/src", "path")]


def test_parse_new_spec_aliased_option():
    spec = CommandSpec(
        options=(Option(short="-r", long="--recursive"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["--recursive", "dir/"], cwd="/")
    assert parsed.flags == {"--recursive": True}

    parsed2 = parse_command(spec, ["-r", "dir/"], cwd="/")
    # Both spellings land on the canonical long dest.
    assert parsed2.flags == {"--recursive": True}


def test_parse_new_spec_path_value_flag():
    spec = CommandSpec(
        options=(
            Option(short="-c"),
            Option(short="-f", type="path"),
        ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-c", "-f", "archive.tar", "file.txt"],
                           cwd="/data")
    assert parsed.flags == {"-c": True, "-f": "/data/archive.tar"}
    assert parsed.paths() == ["/data/file.txt"]
    assert parsed.path_flag_values == ["/data/archive.tar"]


def test_parse_new_spec_positional_text_then_path():
    spec = CommandSpec(
        options=(Option(short="-i"), ),
        positional=(Operand(type="str"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-i", "pattern", "file1.txt"], cwd="/data")
    assert parsed.flags == {"-i": True}
    assert parsed.args == [
        ("pattern", "str"),
        ("/data/file1.txt", "path"),
    ]


def test_parse_new_spec_no_rest():
    # Overflow past a fixed arity is classified like the last positional
    # slot and passed through; the command owns the refusal (#452).
    spec = CommandSpec(positional=(Operand(type="str"), Operand(type="str")), )
    parsed = parse_command(spec, ["hello", "world", "extra"], cwd="/")
    assert parsed.args == [
        ("hello", "str"),
        ("world", "str"),
        ("extra", "str"),
    ]


def test_parse_new_spec_joined_value_flag():
    spec = CommandSpec(
        options=(Option(short="-n", type="str"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-n10", "file.txt"], cwd="/")
    assert parsed.flags == {"-n": "10"}
    assert parsed.args == [("/file.txt", "path")]


def test_numeric_shorthand_treats_dash_n_as_flag():
    spec = CommandSpec(
        options=(Option(short="-n", type="str", numeric_shorthand=True), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-3", "file.txt"], cwd="/")
    assert parsed.flags == {"-n": "3"}
    assert parsed.args == [("/file.txt", "path")]


def test_numeric_shorthand_keeps_dash_n_value_form():
    spec = CommandSpec(
        options=(Option(short="-n", type="str", numeric_shorthand=True), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-n", "3", "file.txt"], cwd="/")
    assert parsed.flags == {"-n": "3"}


def test_numeric_shorthand_opt_in_only():
    spec = CommandSpec(
        options=(Option(short="-n", type="str"), ),
        rest=Operand(type="path"),
    )
    parsed = parse_command(spec, ["-3", "file.txt"], cwd="/")
    assert "-n" not in parsed.flags


def test_head_spec_supports_numeric_shorthand():
    parsed = parse_command(SPECS["head"], ["-3", "/file.txt"], cwd="/")
    assert parsed.flags.get("--lines") == "3"
    assert parsed.paths() == ["/file.txt"]


def test_tail_spec_supports_numeric_shorthand():
    parsed = parse_command(SPECS["tail"], ["-5", "/file.txt"], cwd="/")
    assert parsed.flags.get("-n") == "5"
    assert parsed.paths() == ["/file.txt"]


def test_option_description_default_none():
    opt = Option(short="-v")
    assert opt.description is None


def test_option_description_round_trip():
    opt = Option(short="-v", description="Verbose output.")
    assert opt.description == "Verbose output."


def test_command_spec_description_default_none():
    spec = CommandSpec()
    assert spec.description is None


def test_command_spec_description_round_trip():
    spec = CommandSpec(description="Do a thing.")
    assert spec.description == "Do a thing."
