// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using Microsoft.Extensions.CommandLineUtils;
using Xunit;

namespace Microsoft.Extensions.Internal;

public class CommandLineApplicationTests
{
    [Fact]
    public void CommandNameCanBeMatched()
    {
        var called = false;

        var app = new CommandLineApplication();
        app.Command("test", c =>
        {
            c.OnExecute(() =>
            {
                called = true;
                return 5;
            });
        });

        var result = app.Execute("test");
        Assert.Equal(5, result);
        Assert.True(called);
    }

    [Fact]
    public void RemainingArgsArePassed()
    {
        CommandArgument first = null;
        CommandArgument second = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            first = c.Argument("first", "First argument");
            second = c.Argument("second", "Second argument");
            c.OnExecute(() => 0);
        });

        app.Execute("test", "one", "two");

        Assert.Equal("one", first.Value);
        Assert.Equal("two", second.Value);
    }

    [Fact]
    public void ExtraArgumentCausesException()
    {
        CommandArgument first = null;
        CommandArgument second = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            first = c.Argument("first", "First argument");
            second = c.Argument("second", "Second argument");
            c.OnExecute(() => 0);
        });

        var ex = Assert.Throws<CommandParsingException>(() => app.Execute("test", "one", "two", "three"));

        Assert.Contains("three", ex.Message);
    }

    [Fact]
    public void ExtraArgumentAddedToRemaining()
    {
        CommandArgument first = null;
        CommandArgument second = null;

        var app = new CommandLineApplication();

        var testCommand = app.Command("test", c =>
        {
            first = c.Argument("first", "First argument");
            second = c.Argument("second", "Second argument");
            c.OnExecute(() => 0);
        },
        throwOnUnexpectedArg: false);

        app.Execute("test", "one", "two", "three");

        Assert.Equal("one", first.Value);
        Assert.Equal("two", second.Value);
        var remaining = Assert.Single(testCommand.RemainingArguments);
        Assert.Equal("three", remaining);
    }

    [Fact]
    public void UnknownCommandCausesException()
    {
        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            c.Argument("first", "First argument");
            c.Argument("second", "Second argument");
            c.OnExecute(() => 0);
        });

        var ex = Assert.Throws<CommandParsingException>(() => app.Execute("test2", "one", "two", "three"));

        Assert.Contains("test2", ex.Message);
    }

    [Fact]
    public void MultipleValuesArgumentConsumesAllArgumentValues()
    {
        CommandArgument argument = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            argument = c.Argument("arg", "Argument that allows multiple values", multipleValues: true);
            c.OnExecute(() => 0);
        });

        app.Execute("test", "one", "two", "three", "four", "five");

        Assert.Equal(new[] { "one", "two", "three", "four", "five" }, argument.Values);
    }

    [Fact]
    public void MultipleValuesArgumentConsumesAllRemainingArgumentValues()
    {
        CommandArgument first = null;
        CommandArgument second = null;
        CommandArgument third = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            first = c.Argument("first", "First argument");
            second = c.Argument("second", "Second argument");
            third = c.Argument("third", "Third argument that allows multiple values", multipleValues: true);
            c.OnExecute(() => 0);
        });

        app.Execute("test", "one", "two", "three", "four", "five");

        Assert.Equal("one", first.Value);
        Assert.Equal("two", second.Value);
        Assert.Equal(new[] { "three", "four", "five" }, third.Values);
    }

    [Fact]
    public void MultipleValuesArgumentMustBeTheLastArgument()
    {
        var app = new CommandLineApplication();
        app.Argument("first", "First argument", multipleValues: true);
        var ex = Assert.Throws<InvalidOperationException>(() => app.Argument("second", "Second argument"));

        Assert.Contains($"The last argument 'first' accepts multiple values. No more argument can be added.",
            ex.Message);
    }

    [Fact]
    public void OptionSwitchMayBeProvided()
    {
        CommandOption first = null;
        CommandOption second = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            first = c.Option("--first <NAME>", "First argument", CommandOptionType.SingleValue);
            second = c.Option("--second <NAME>", "Second argument", CommandOptionType.SingleValue);
            c.OnExecute(() => 0);
        });

        app.Execute("test", "--first", "one", "--second", "two");

        Assert.Equal("one", first.Values[0]);
        Assert.Equal("two", second.Values[0]);
    }

    [Fact]
    public void OptionValueMustBeProvided()
    {
        CommandOption first = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            first = c.Option("--first <NAME>", "First argument", CommandOptionType.SingleValue);
            c.OnExecute(() => 0);
        });

        var ex = Assert.Throws<CommandParsingException>(() => app.Execute("test", "--first"));

        Assert.Contains($"Missing value for option '{first.LongName}'", ex.Message);
    }

    [Fact]
    public void ValuesMayBeAttachedToSwitch()
    {
        CommandOption first = null;
        CommandOption second = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            first = c.Option("--first <NAME>", "First argument", CommandOptionType.SingleValue);
            second = c.Option("--second <NAME>", "Second argument", CommandOptionType.SingleValue);
            c.OnExecute(() => 0);
        });

        app.Execute("test", "--first=one", "--second:two");

        Assert.Equal("one", first.Values[0]);
        Assert.Equal("two", second.Values[0]);
    }

    [Fact]
    public void ShortNamesMayBeDefined()
    {
        CommandOption first = null;
        CommandOption second = null;

        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            first = c.Option("-1 --first <NAME>", "First argument", CommandOptionType.SingleValue);
            second = c.Option("-2 --second <NAME>", "Second argument", CommandOptionType.SingleValue);
            c.OnExecute(() => 0);
        });

        app.Execute("test", "-1=one", "-2", "two");

        Assert.Equal("one", first.Values[0]);
        Assert.Equal("two", second.Values[0]);
    }

    [Fact]
    public void ThrowsExceptionOnUnexpectedCommandOrArgumentByDefault()
    {
        var unexpectedArg = "UnexpectedArg";
        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        });

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedArg));
        Assert.Equal($"Unrecognized command or argument '{unexpectedArg}'", exception.Message);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedArgument()
    {
        var unexpectedArg = "UnexpectedArg";
        var app = new CommandLineApplication();

        var testCmd = app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        },
        throwOnUnexpectedArg: false);

        // (does not throw)
        app.Execute("test", unexpectedArg);
        var arg = Assert.Single(testCmd.RemainingArguments);
        Assert.Equal(unexpectedArg, arg);
    }

    [Fact]
    public void AllowArgumentBeforeNoValueOption()
    {
        var app = new CommandLineApplication();
        var argument = app.Argument("first", "first argument");
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        app.Execute("one", "--first");

        Assert.Equal("one", argument.Value);
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowArgumentAfterNoValueOption()
    {
        var app = new CommandLineApplication();
        var argument = app.Argument("first", "first argument");
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        app.Execute("--first", "one");

        Assert.Equal("one", argument.Value);
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowArgumentBeforeSingleValueOption()
    {
        var app = new CommandLineApplication();
        var argument = app.Argument("first", "first argument");
        var option = app.Option("--first <value>", "first option", CommandOptionType.SingleValue);

        app.Execute("one", "--first", "two");

        Assert.Equal("one", argument.Value);
        Assert.Equal("two", option.Value());
    }

    [Fact]
    public void AllowArgumentAfterSingleValueOption()
    {
        var app = new CommandLineApplication();
        var argument = app.Argument("first", "first argument");
        var option = app.Option("--first <value>", "first option", CommandOptionType.SingleValue);

        app.Execute("--first", "one", "two");

        Assert.Equal("two", argument.Value);
        Assert.Equal("one", option.Value());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedArgumentBeforeNoValueOption_Default()
    {
        var arguments = new[] { "UnexpectedArg", "--first" };
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute(arguments);

        Assert.Equal(arguments, app.RemainingArguments.ToArray());
        Assert.False(option.HasValue());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedArgumentBeforeNoValueOption_Continue()
    {
        var unexpectedArg = "UnexpectedArg";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute(unexpectedArg, "--first");

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedArg, arg);
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedArgumentAfterNoValueOption()
    {
        var unexpectedArg = "UnexpectedArg";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute("--first", unexpectedArg);

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedArg, arg);
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedArgumentBeforeSingleValueOption_Default()
    {
        var arguments = new[] { "UnexpectedArg", "--first", "one" };
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute(arguments);

        Assert.Equal(arguments, app.RemainingArguments.ToArray());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedArgumentBeforeSingleValueOption_Continue()
    {
        var unexpectedArg = "UnexpectedArg";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var option = app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute(unexpectedArg, "--first", "one");

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedArg, arg);
        Assert.Equal("one", option.Value());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedArgumentAfterSingleValueOption()
    {
        var unexpectedArg = "UnexpectedArg";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var option = app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute("--first", "one", unexpectedArg);

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedArg, arg);
        Assert.Equal("one", option.Value());
    }

    [Fact]
    public void ThrowsExceptionOnUnexpectedLongOptionByDefault()
    {
        var unexpectedOption = "--UnexpectedOption";
        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        });

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedOption));
        Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOption()
    {
        var unexpectedOption = "--UnexpectedOption";
        var app = new CommandLineApplication();

        var testCmd = app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        },
        throwOnUnexpectedArg: false);

        // (does not throw)
        app.Execute("test", unexpectedOption);
        var arg = Assert.Single(testCmd.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionBeforeNoValueOption_Default()
    {
        var arguments = new[] { "--unexpected", "--first" };
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute(arguments);

        Assert.Equal(arguments, app.RemainingArguments.ToArray());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionBeforeNoValueOption_Continue()
    {
        var unexpectedOption = "--unexpected";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute(unexpectedOption, "--first");

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionAfterNoValueOption()
    {
        var unexpectedOption = "--unexpected";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute("--first", unexpectedOption);

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionBeforeSingleValueOption_Default()
    {
        var arguments = new[] { "--unexpected", "--first", "one" };
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute(arguments);

        Assert.Equal(arguments, app.RemainingArguments.ToArray());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionBeforeSingleValueOption_Continue()
    {
        var unexpectedOption = "--unexpected";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var option = app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute(unexpectedOption, "--first", "one");

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
        Assert.Equal("one", option.Value());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionAfterSingleValueOption()
    {
        var unexpectedOption = "--unexpected";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var option = app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute("--first", "one", unexpectedOption);

        var arg = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
        Assert.Equal("one", option.Value());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionWithValueBeforeNoValueOption_Default()
    {
        var arguments = new[] { "--unexpected", "value", "--first" };
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute(arguments);

        Assert.Equal(arguments, app.RemainingArguments.ToArray());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionWithValueBeforeNoValueOption_Continue()
    {
        var unexpectedOption = "--unexpected";
        var unexpectedValue = "value";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute(unexpectedOption, unexpectedValue, "--first");

        Assert.Equal(new[] { unexpectedOption, unexpectedValue }, app.RemainingArguments.ToArray());
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionWithValueAfterNoValueOption()
    {
        var unexpectedOption = "--unexpected";
        var unexpectedValue = "value";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var option = app.Option("--first", "first option", CommandOptionType.NoValue);

        // (does not throw)
        app.Execute("--first", unexpectedOption, unexpectedValue);

        Assert.Equal(new[] { unexpectedOption, unexpectedValue }, app.RemainingArguments.ToArray());
        Assert.True(option.HasValue());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionWithValueBeforeSingleValueOption_Default()
    {
        var unexpectedOption = "--unexpected";
        var unexpectedValue = "value";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute(unexpectedOption, unexpectedValue, "--first", "one");

        Assert.Equal(
            new[] { unexpectedOption, unexpectedValue, "--first", "one" },
            app.RemainingArguments.ToArray());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionWithValueBeforeSingleValueOption_Continue()
    {
        var unexpectedOption = "--unexpected";
        var unexpectedValue = "value";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var option = app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute(unexpectedOption, unexpectedValue, "--first", "one");

        Assert.Equal(
            new[] { unexpectedOption, unexpectedValue },
            app.RemainingArguments.ToArray());
        Assert.Equal("one", option.Value());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedLongOptionWithValueAfterSingleValueOption()
    {
        var unexpectedOption = "--unexpected";
        var unexpectedValue = "value";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var option = app.Option("--first", "first option", CommandOptionType.SingleValue);

        // (does not throw)
        app.Execute("--first", "one", unexpectedOption, unexpectedValue);

        Assert.Equal(new[] { unexpectedOption, unexpectedValue }, app.RemainingArguments.ToArray());
        Assert.Equal("one", option.Value());
    }

    [Fact]
    public void ThrowsExceptionOnUnexpectedShortOptionByDefault()
    {
        var unexpectedOption = "-uexp";
        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        });

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedOption));
        Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedShortOption()
    {
        var unexpectedOption = "-uexp";
        var app = new CommandLineApplication();

        var testCmd = app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        },
        throwOnUnexpectedArg: false);

        // (does not throw)
        app.Execute("test", unexpectedOption);
        var arg = Assert.Single(testCmd.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
    }

    [Fact]
    public void ThrowsExceptionOnUnexpectedSymbolOptionByDefault()
    {
        var unexpectedOption = "-?";
        var app = new CommandLineApplication();

        app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        });

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedOption));
        Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedSymbolOption()
    {
        var unexpectedOption = "-?";
        var app = new CommandLineApplication();

        var testCmd = app.Command("test", c =>
        {
            c.OnExecute(() => 0);
        },
        throwOnUnexpectedArg: false);

        // (does not throw)
        app.Execute("test", unexpectedOption);
        var arg = Assert.Single(testCmd.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
    }

    [Fact]
    public void ThrowsExceptionOnUnexpectedOptionBeforeValidSubcommandByDefault()
    {
        var unexpectedOption = "--unexpected";
        CommandLineApplication subCmd = null;
        var app = new CommandLineApplication();

        app.Command("k", c =>
        {
            subCmd = c.Command("run", _ => { });
            c.OnExecute(() => 0);
        });

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute("k", unexpectedOption, "run"));
        Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedOptionBeforeSubcommand()
    {
        var unexpectedOption = "--unexpected";
        var app = new CommandLineApplication();

        CommandLineApplication subCmd = null;
        var testCmd = app.Command("k", c =>
        {
            subCmd = c.Command("run", _ => { });
            c.OnExecute(() => 0);
        },
        throwOnUnexpectedArg: false);

        // (does not throw)
        app.Execute("k", unexpectedOption, "run");

        Assert.Empty(app.RemainingArguments);
        Assert.Equal(new[] { unexpectedOption, "run" }, testCmd.RemainingArguments.ToArray());
        Assert.Empty(subCmd.RemainingArguments);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand()
    {
        var unexpectedOption = "--unexpected";
        var app = new CommandLineApplication();

        CommandLineApplication subCmd = null;
        var testCmd = app.Command("k", c =>
        {
            subCmd = c.Command("run", _ => { }, throwOnUnexpectedArg: false);
            c.OnExecute(() => 0);
        });

        // (does not throw)
        app.Execute("k", "run", unexpectedOption);

        Assert.Empty(app.RemainingArguments);
        Assert.Empty(testCmd.RemainingArguments);
        var arg = Assert.Single(subCmd.RemainingArguments);
        Assert.Equal(unexpectedOption, arg);
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedOptionBeforeValidCommand_Default()
    {
        var arguments = new[] { "--unexpected", "run" };
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var commandRan = false;
        app.Command("run", c => c.OnExecute(() => { commandRan = true; return 0; }));
        app.OnExecute(() => 0);

        app.Execute(arguments);

        Assert.False(commandRan);
        Assert.Equal(arguments, app.RemainingArguments.ToArray());
    }

    [Fact]
    public void AllowNoThrowBehaviorOnUnexpectedOptionBeforeValidCommand_Continue()
    {
        var unexpectedOption = "--unexpected";
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var commandRan = false;
        app.Command("run", c => c.OnExecute(() => { commandRan = true; return 0; }));
        app.OnExecute(() => 0);

        app.Execute(unexpectedOption, "run");

        Assert.True(commandRan);
        var remaining = Assert.Single(app.RemainingArguments);
        Assert.Equal(unexpectedOption, remaining);
    }

    [Fact]
    public void OptionsCanBeInherited()
    {
        var app = new CommandLineApplication();
        var optionA = app.Option("-a|--option-a", "", CommandOptionType.SingleValue, inherited: true);
        string optionAValue = null;

        var optionB = app.Option("-b", "", CommandOptionType.SingleValue, inherited: false);

        var subcmd = app.Command("subcmd", c =>
        {
            c.OnExecute(() =>
            {
                optionAValue = optionA.Value();
                return 0;
            });
        });

        Assert.Equal(2, app.GetOptions().Count());
        Assert.Single(subcmd.GetOptions());

        app.Execute("-a", "A1", "subcmd");
        Assert.Equal("A1", optionAValue);

        Assert.Throws<CommandParsingException>(() => app.Execute("subcmd", "-b", "B"));

        Assert.Contains("-a|--option-a", subcmd.GetHelpText());
    }

    [Fact]
    public void NestedOptionConflictThrows()
    {
        var app = new CommandLineApplication();
        app.Option("-a|--always", "Top-level", CommandOptionType.SingleValue, inherited: true);
        app.Command("subcmd", c =>
        {
            c.Option("-a|--ask", "Nested", CommandOptionType.SingleValue);
        });

        Assert.Throws<InvalidOperationException>(() => app.Execute("subcmd", "-a", "b"));
    }

    [Fact]
    public void OptionsWithSameName()
    {
        var app = new CommandLineApplication();
        var top = app.Option("-a|--always", "Top-level", CommandOptionType.SingleValue, inherited: false);
        CommandOption nested = null;
        app.Command("subcmd", c =>
        {
            nested = c.Option("-a|--ask", "Nested", CommandOptionType.SingleValue);
        });

        app.Execute("-a", "top");
        Assert.Equal("top", top.Value());
        Assert.Null(nested.Value());

        top.Values.Clear();

        app.Execute("subcmd", "-a", "nested");
        Assert.Null(top.Value());
        Assert.Equal("nested", nested.Value());
    }

    [Fact]
    public void NestedInheritedOptions()
    {
        string globalOptionValue = null, nest1OptionValue = null, nest2OptionValue = null;

        var app = new CommandLineApplication();
        CommandLineApplication subcmd2 = null;
        var g = app.Option("-g|--global", "Global option", CommandOptionType.SingleValue, inherited: true);
        var subcmd1 = app.Command("lvl1", s1 =>
        {
            var n1 = s1.Option("--nest1", "Nested one level down", CommandOptionType.SingleValue, inherited: true);
            subcmd2 = s1.Command("lvl2", s2 =>
            {
                var n2 = s2.Option("--nest2", "Nested one level down", CommandOptionType.SingleValue, inherited: true);
                s2.HelpOption("-h|--help");
                s2.OnExecute(() =>
                {
                    globalOptionValue = g.Value();
                    nest1OptionValue = n1.Value();
                    nest2OptionValue = n2.Value();
                    return 0;
                });
            });
        });

        Assert.DoesNotContain(app.GetOptions(), o => o.LongName == "nest2");
        Assert.DoesNotContain(app.GetOptions(), o => o.LongName == "nest1");
        Assert.Contains(app.GetOptions(), o => o.LongName == "global");

        Assert.DoesNotContain(subcmd1.GetOptions(), o => o.LongName == "nest2");
        Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "nest1");
        Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "global");

        Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest2");
        Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest1");
        Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "global");

        Assert.Throws<CommandParsingException>(() => app.Execute("--nest2", "N2", "--nest1", "N1", "-g", "G"));
        Assert.Throws<CommandParsingException>(() => app.Execute("lvl1", "--nest2", "N2", "--nest1", "N1", "-g", "G"));

        app.Execute("lvl1", "lvl2", "--nest2", "N2", "-g", "G", "--nest1", "N1");
        Assert.Equal("G", globalOptionValue);
        Assert.Equal("N1", nest1OptionValue);
        Assert.Equal("N2", nest2OptionValue);
    }

    [Theory]
    [InlineData(new string[0], new string[0], null)]
    [InlineData(new[] { "--" }, new string[0], null)]
    [InlineData(new[] { "-t", "val" }, new string[0], "val")]
    [InlineData(new[] { "-t", "val", "--" }, new string[0], "val")]
    [InlineData(new[] { "--top", "val", "--", "a" }, new[] { "a" }, "val")]
    [InlineData(new[] { "--", "a", "--top", "val" }, new[] { "a", "--top", "val" }, null)]
    [InlineData(new[] { "-t", "val", "--", "a", "--", "b" }, new[] { "a", "--", "b" }, "val")]
    [InlineData(new[] { "--", "--help" }, new[] { "--help" }, null)]
    [InlineData(new[] { "--", "--version" }, new[] { "--version" }, null)]
    public void ArgumentSeparator(string[] input, string[] expectedRemaining, string topLevelValue)
    {
        var app = new CommandLineApplication(throwOnUnexpectedArg: false)
        {
            AllowArgumentSeparator = true
        };
        var optHelp = app.HelpOption("--help");
        var optVersion = app.VersionOption("--version", "1", "1.0");
        var optTop = app.Option("-t|--top <TOP>", "arg for command", CommandOptionType.SingleValue);
        app.Execute(input);

        Assert.Equal(topLevelValue, optTop.Value());
        Assert.False(optHelp.HasValue());
        Assert.False(optVersion.HasValue());
        Assert.Equal(expectedRemaining, app.RemainingArguments.ToArray());
    }

    [Theory]
    [InlineData(new string[0], new string[0], null, false)]
    [InlineData(new[] { "--" }, new[] { "--" }, null, false)]
    [InlineData(new[] { "-t", "val" }, new string[0], "val", false)]
    [InlineData(new[] { "-t", "val", "--" }, new[] { "--" }, "val", false)]
    [InlineData(new[] { "--top", "val", "--", "a" }, new[] { "--", "a" }, "val", false)]
    [InlineData(new[] { "-t", "val", "--", "a", "--", "b" }, new[] { "--", "a", "--", "b" }, "val", false)]
    [InlineData(new[] { "--help", "--" }, new string[0], null, true)]
    [InlineData(new[] { "--version", "--" }, new string[0], null, true)]
    public void ArgumentSeparator_TreatedAsUexpected(
        string[] input,
        string[] expectedRemaining,
        string topLevelValue,
        bool isShowingInformation)
    {
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var optHelp = app.HelpOption("--help");
        var optVersion = app.VersionOption("--version", "1", "1.0");
        var optTop = app.Option("-t|--top <TOP>", "arg for command", CommandOptionType.SingleValue);

        app.Execute(input);

        Assert.Equal(topLevelValue, optTop.Value());
        Assert.Equal(expectedRemaining, app.RemainingArguments.ToArray());
        Assert.Equal(isShowingInformation, app.IsShowingInformation);

        // Help and Version options never get values; parsing ends when encountered.
        Assert.False(optHelp.HasValue());
        Assert.False(optVersion.HasValue());
    }

    [Theory]
    [InlineData(new[] { "--", "a", "--top", "val" }, new[] { "--", "a", "--top", "val" }, null, false)]
    [InlineData(new[] { "--", "--help" }, new[] { "--", "--help" }, null, false)]
    [InlineData(new[] { "--", "--version" }, new[] { "--", "--version" }, null, false)]
    [InlineData(new[] { "unexpected", "--", "--version" }, new[] { "unexpected", "--", "--version" }, null, false)]
    public void ArgumentSeparator_TreatedAsUexpected_Default(
        string[] input,
        string[] expectedRemaining,
        string topLevelValue,
        bool isShowingInformation)
    {
        var app = new CommandLineApplication(throwOnUnexpectedArg: false);
        var optHelp = app.HelpOption("--help");
        var optVersion = app.VersionOption("--version", "1", "1.0");
        var optTop = app.Option("-t|--top <TOP>", "arg for command", CommandOptionType.SingleValue);

        app.Execute(input);

        Assert.Equal(topLevelValue, optTop.Value());
        Assert.Equal(expectedRemaining, app.RemainingArguments.ToArray());
        Assert.Equal(isShowingInformation, app.IsShowingInformation);

        // Help and Version options never get values; parsing ends when encountered.
        Assert.False(optHelp.HasValue());
        Assert.False(optVersion.HasValue());
    }

    [Theory]
    [InlineData(new[] { "--", "a", "--top", "val" }, new[] { "--", "a" }, "val", false)]
    [InlineData(new[] { "--", "--help" }, new[] { "--" }, null, true)]
    [InlineData(new[] { "--", "--version" }, new[] { "--" }, null, true)]
    [InlineData(new[] { "unexpected", "--", "--version" }, new[] { "unexpected", "--" }, null, true)]
    public void ArgumentSeparator_TreatedAsUexpected_Continue(
        string[] input,
        string[] expectedRemaining,
        string topLevelValue,
        bool isShowingInformation)
    {
        var app = new CommandLineApplication(throwOnUnexpectedArg: false, continueAfterUnexpectedArg: true);
        var optHelp = app.HelpOption("--help");
        var optVersion = app.VersionOption("--version", "1", "1.0");
        var optTop = app.Option("-t|--top <TOP>", "arg for command", CommandOptionType.SingleValue);

        app.Execute(input);

        Assert.Equal(topLevelValue, optTop.Value());
        Assert.Equal(expectedRemaining, app.RemainingArguments.ToArray());
        Assert.Equal(isShowingInformation, app.IsShowingInformation);

        // Help and Version options never get values; parsing ends when encountered.
        Assert.False(optHelp.HasValue());
        Assert.False(optVersion.HasValue());
    }

    [Fact]
    public void HelpTextIgnoresHiddenItems()
    {
        var app = new CommandLineApplication()
        {
            Name = "ninja-app",
            Description = "You can't see it until it is too late"
        };

        app.Command("star", c =>
        {
            c.Option("--points <p>", "How many", CommandOptionType.MultipleValue);
            c.ShowInHelpText = false;
        });
        app.Option("--smile", "Be a nice ninja", CommandOptionType.NoValue, o => { o.ShowInHelpText = false; });

        var a = app.Argument("name", "Pseudonym, of course");
        a.ShowInHelpText = false;

        var help = app.GetHelpText();

        Assert.Contains("ninja-app", help);
        Assert.DoesNotContain("--points", help);
        Assert.DoesNotContain("--smile", help);
        Assert.DoesNotContain("name", help);
    }

    [Fact]
    public void HelpTextUsesHelpOptionName()
    {
        var app = new CommandLineApplication
        {
            Name = "superhombre"
        };

        app.HelpOption("--ayuda-me");
        var help = app.GetHelpText();
        Assert.Contains("--ayuda-me", help);
    }

    [Fact]
    public void HelpTextShowsArgSeparator()
    {
        var app = new CommandLineApplication(throwOnUnexpectedArg: false)
        {
            Name = "proxy-command",
            AllowArgumentSeparator = true
        };
        app.HelpOption("-h|--help");
        Assert.Contains("Usage: proxy-command [options] [[--] <arg>...]", app.GetHelpText());
    }

    [Fact]
    public void HelpTextShowsExtendedHelp()
    {
        var app = new CommandLineApplication()
        {
            Name = "befuddle",
            ExtendedHelpText = @"
Remarks:
  This command is so confusing that I want to include examples in the help text.

Examples:
  dotnet befuddle -- I Can Haz Confusion Arguments
"
        };

        Assert.Contains(app.ExtendedHelpText, app.GetHelpText());
    }

    [Theory]
    [InlineData(new[] { "--version", "--flag" }, "1.0")]
    [InlineData(new[] { "-V", "-f" }, "1.0")]
    [InlineData(new[] { "--help", "--flag" }, "some flag")]
    [InlineData(new[] { "-h", "-f" }, "some flag")]
    public void HelpAndVersionOptionStopProcessing(string[] input, string expectedOutData)
    {
        using var outWriter = new StringWriter();
        var app = new CommandLineApplication { Out = outWriter };
        app.HelpOption("-h --help");
        app.VersionOption("-V --version", "1", "1.0");
        var optFlag = app.Option("-f |--flag", "some flag", CommandOptionType.NoValue);

        app.Execute(input);

        outWriter.Flush();
        var outData = outWriter.ToString();
        Assert.Contains(expectedOutData, outData);
        Assert.False(optFlag.HasValue());
    }

    // disable inaccurate analyzer error https://github.com/xunit/xunit/issues/1274
#pragma warning disable xUnit1010
#pragma warning disable xUnit1011
    [Theory]
    [InlineData("-f:File1", "-f:File2")]
    [InlineData("--file:File1", "--file:File2")]
    [InlineData("--file", "File1", "--file", "File2")]
#pragma warning restore xUnit1010
#pragma warning restore xUnit1011
    public void ThrowsExceptionOnSingleValueOptionHavingTwoValues(params string[] inputOptions)
    {
        var app = new CommandLineApplication();
        app.Option("-f |--file", "some file", CommandOptionType.SingleValue);

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOptions));

        Assert.Equal("Unexpected value 'File2' for option 'file'", exception.Message);
    }

    [Theory]
    [InlineData("-v")]
    [InlineData("--verbose")]
    public void NoValueOptionCanBeSet(string input)
    {
        var app = new CommandLineApplication();
        var optVerbose = app.Option("-v |--verbose", "be verbose", CommandOptionType.NoValue);

        app.Execute(input);

        Assert.True(optVerbose.HasValue());
    }

    [Theory]
    [InlineData("-v:true")]
    [InlineData("--verbose:true")]
    public void ThrowsExceptionOnNoValueOptionHavingValue(string inputOption)
    {
        var app = new CommandLineApplication();
        app.Option("-v |--verbose", "be verbose", CommandOptionType.NoValue);

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOption));

        Assert.Equal("Unexpected value 'true' for option 'verbose'", exception.Message);
    }

    [Fact]
    public void ThrowsExceptionOnEmptyCommandOrArgument()
    {
        var inputOption = String.Empty;
        var app = new CommandLineApplication();

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOption));

        Assert.Equal($"Unrecognized command or argument '{inputOption}'", exception.Message);
    }

    [Fact]
    public void ThrowsExceptionOnInvalidOption()
    {
        var inputOption = "-";
        var app = new CommandLineApplication();

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOption));

        Assert.Equal($"Unrecognized option '{inputOption}'", exception.Message);
    }

    [Fact]
    public void TreatUnmatchedOptionsAsArguments()
    {
        CommandArgument first = null;
        CommandArgument second = null;

        CommandOption firstOption = null;
        CommandOption secondOption = null;

        var firstUnmatchedOption = "-firstUnmatchedOption";
        var firstActualOption = "-firstActualOption";
        var seconUnmatchedOption = "--secondUnmatchedOption";
        var secondActualOption = "--secondActualOption";

        var app = new CommandLineApplication(treatUnmatchedOptionsAsArguments: true);

        app.Command("test", c =>
        {
            firstOption = c.Option("-firstActualOption", "first option", CommandOptionType.NoValue);
            secondOption = c.Option("--secondActualOption", "second option", CommandOptionType.NoValue);

            first = c.Argument("first", "First argument");
            second = c.Argument("second", "Second argument");
            c.OnExecute(() => 0);
        });

        app.Execute("test", firstUnmatchedOption, firstActualOption, seconUnmatchedOption, secondActualOption);

        Assert.Equal(firstUnmatchedOption, first.Value);
        Assert.Equal(seconUnmatchedOption, second.Value);

        Assert.Equal(firstActualOption, firstOption.Template);
        Assert.Equal(secondActualOption, secondOption.Template);
    }

    [Fact]
    public void ThrowExceptionWhenUnmatchedOptionAndTreatUnmatchedOptionsAsArgumentsIsFalse()
    {
        CommandArgument first = null;

        var firstOption = "-firstUnmatchedOption";

        var app = new CommandLineApplication(treatUnmatchedOptionsAsArguments: false);
        app.Command("test", c =>
        {
            first = c.Argument("first", "First argument");
            c.OnExecute(() => 0);
        });

        var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", firstOption));

        Assert.Equal($"Unrecognized option '{firstOption}'", exception.Message);
    }
}
