// 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);
        }
    }
}
