//  Copyright Vladimir Prus 2002-2004.
//
//  SPDX-License-Identifier: BSL-1.0
//  Distributed under the Boost Software License, Version 1.0.
//  (See accompanying file LICENSE_1_0.txt
//  or copy at http://www.boost.org/LICENSE_1_0.txt)

#include <hpx/assert.hpp>
#include <hpx/modules/testing.hpp>

#include <hpx/program_options/cmdline.hpp>
#include <hpx/program_options/detail/cmdline.hpp>
#include <hpx/program_options/errors.hpp>
#include <hpx/program_options/option.hpp>
#include <hpx/program_options/options_description.hpp>
#include <hpx/program_options/value_semantic.hpp>

#include <cstddef>
#include <iostream>
#include <sstream>
#include <utility>
#include <vector>

using namespace hpx::program_options;
using hpx::program_options::detail::cmdline;

using namespace std;

/* To facilitate testing, declare a number of error codes. Otherwise,
   we'd have to specify the type of exception that should be thrown.
*/

const int s_success = 0;
const int s_unknown_option = 1;
const int s_ambiguous_option = 2;
// const int s_long_not_allowed = 3;
// const int s_long_adjacent_not_allowed = 4;
// const int s_short_adjacent_not_allowed = 5;
const int s_empty_adjacent_parameter = 6;
const int s_missing_parameter = 7;
const int s_extra_parameter = 8;
// const int s_unrecognized_line = 9;

int translate_syntax_error_kind(invalid_command_line_syntax::kind_t k)
{
    invalid_command_line_syntax::kind_t table[] = {
        invalid_command_line_syntax::long_not_allowed,
        invalid_command_line_syntax::long_adjacent_not_allowed,
        invalid_command_line_syntax::short_adjacent_not_allowed,
        invalid_command_line_syntax::empty_adjacent_parameter,
        invalid_command_line_syntax::missing_parameter,
        invalid_command_line_syntax::extra_parameter,
        invalid_command_line_syntax::unrecognized_line};
    invalid_command_line_syntax::kind_t *b, *e, *i;
    b = table;
    e = table + sizeof(table) / sizeof(table[0]);
    i = std::find(b, e, k);
    HPX_TEST_NEQ(i, e);
    return int(std::distance(b, i)) + 3;
}

struct test_case
{
    const char* input;
    int expected_status;
    const char* expected_result;
};

/* Parses the syntax description in 'syntax' and initialized
   'cmd' accordingly'
   The "hpx::program_options" in parameter type is needed because CW9
   has std::detail and it causes an ambiguity.
*/
void apply_syntax(options_description& desc, const char* syntax)
{
    string s;
    stringstream ss;
    ss << syntax;
    while (ss >> s)
    {
        value_semantic* v = nullptr;

        if (*(s.end() - 1) == '=')
        {
            v = value<string>();
            s.resize(s.size() - 1);
        }
        else if (*(s.end() - 1) == '?')
        {
            v = value<string>()->implicit_value("default");
            s.resize(s.size() - 1);
        }
        else if (*(s.end() - 1) == '*')
        {
            v = value<vector<string>>()->multitoken();
            s.resize(s.size() - 1);
        }
        else if (*(s.end() - 1) == '+')
        {
            v = value<vector<string>>()->multitoken();
            s.resize(s.size() - 1);
        }
        if (v)
        {
            desc.add_options()(s.c_str(), v, "");
        }
        else
        {
            desc.add_options()(s.c_str(), "");
        }
    }
}

void test_cmdline(const char* syntax, command_line_style::style_t style,
    const test_case* cases)
{
    for (int i = 0; cases[i].input; ++i)
    {
        // Parse input
        vector<string> xinput;
        {
            string s;
            stringstream ss;
            ss << cases[i].input;
            while (ss >> s)
            {
                xinput.push_back(s);
            }
        }
        options_description desc;
        apply_syntax(desc, syntax);

        cmdline cmd(xinput);
        cmd.style(style);
        cmd.set_options_description(desc);

        string result;
        int status = 0;

        try
        {
            vector<option> options = cmd.run();

            for (option opt : options)
            {
                if (opt.position_key != -1)
                {
                    if (!result.empty())
                        result += " ";
                    result += opt.value[0];
                }
                else
                {
                    if (!result.empty())
                        result += " ";
                    result += opt.string_key + ":";
                    for (size_t k = 0; k < opt.value.size(); ++k)
                    {
                        if (k != 0)
                            result += "-";
                        result += opt.value[k];
                    }
                }
            }
        }
        catch (unknown_option&)
        {
            status = s_unknown_option;
        }
        catch (ambiguous_option&)
        {
            status = s_ambiguous_option;
        }
        catch (invalid_command_line_syntax& e)
        {
            status = translate_syntax_error_kind(e.kind());
        }
        HPX_TEST_EQ(status, cases[i].expected_status);
        HPX_TEST_EQ(result, cases[i].expected_result);
    }
}

void test_long_options()
{
    using namespace command_line_style;
    cmdline::style_t style = cmdline::style_t(allow_long | long_allow_adjacent);

    test_case test_cases1[] = {
        // Test that long options are recognized and everything else
        // is treated like arguments
        {"--foo foo -123 /asd", s_success, "foo: foo -123 /asd"},

        // Unknown option
        {"--unk", s_unknown_option, ""},

        // Test that abbreviated names do not work
        {"--fo", s_unknown_option, ""},

        // Test for disallowed parameter
        {"--foo=13", s_extra_parameter, ""},

        // Test option with required parameter
        {"--bar=", s_empty_adjacent_parameter, ""},
        {"--bar", s_missing_parameter, ""},

        {"--bar=123", s_success, "bar:123"},

        {nullptr, 0, nullptr}};

    test_cmdline("foo bar=", style, test_cases1);

    style = cmdline::style_t(allow_long | long_allow_next);

    test_case test_cases2[] = {{"--bar 10", s_success, "bar:10"},
        {"--bar", s_missing_parameter, ""},
        // Since --bar accepts a parameter, --foo is
        // considered a value, even though it looks like
        // an option.
        {"--bar --foo", s_success, "bar:--foo"}, {nullptr, 0, nullptr}};

    test_cmdline("foo bar=", style, test_cases2);
    style =
        cmdline::style_t(allow_long | long_allow_adjacent | long_allow_next);

    test_case test_cases3[] = {{"--bar=10", s_success, "bar:10"},
        {"--bar 11", s_success, "bar:11"}, {nullptr, 0, nullptr}};
    test_cmdline("foo bar=", style, test_cases3);

    style = cmdline::style_t(
        allow_long | long_allow_adjacent | long_allow_next | case_insensitive);

    // Test case insensitive style.
    // Note that option names are normalized to lower case.
    test_case test_cases4[] = {{"--foo", s_success, "foo:"},
        {"--Foo", s_success, "foo:"}, {"--bar=Ab", s_success, "bar:Ab"},
        {"--Bar=ab", s_success, "bar:ab"}, {"--giz", s_success, "Giz:"},
        {nullptr, 0, nullptr}};
    test_cmdline("foo bar= baz? Giz", style, test_cases4);
}

void test_short_options()
{
    using namespace command_line_style;
    cmdline::style_t style;

    style = cmdline::style_t(
        allow_short | allow_dash_for_short | short_allow_adjacent);

    test_case test_cases1[] = {{"-d d /bar", s_success, "-d: d /bar"},
        // This is treated as error when long options are disabled
        {"--foo", s_success, "--foo"}, {"-d13", s_extra_parameter, ""},
        {"-f14", s_success, "-f:14"}, {"-g -f1", s_success, "-g: -f:1"},
        {"-f", s_missing_parameter, ""}, {nullptr, 0, nullptr}};
    test_cmdline(",d ,f= ,g", style, test_cases1);

    style =
        cmdline::style_t(allow_short | allow_dash_for_short | short_allow_next);

    test_case test_cases2[] = {{"-f 13", s_success, "-f:13"},
        {"-f -13", s_success, "-f:-13"}, {"-f", s_missing_parameter, ""},
        {"-f /foo", s_success, "-f:/foo"}, {"-f -d", s_missing_parameter, ""},
        {nullptr, 0, nullptr}};
    test_cmdline(",d ,f=", style, test_cases2);

    style = cmdline::style_t(allow_short | short_allow_next |
        allow_dash_for_short | short_allow_adjacent);

    test_case test_cases3[] = {{"-f10", s_success, "-f:10"},
        {"-f 10", s_success, "-f:10"}, {"-f -d", s_missing_parameter, ""},
        {nullptr, 0, nullptr}};
    test_cmdline(",d ,f=", style, test_cases3);

    style = cmdline::style_t(allow_short | short_allow_next |
        allow_dash_for_short | short_allow_adjacent | allow_sticky);

    test_case test_cases4[] = {{"-de", s_success, "-d: -e:"},
        {"-df10", s_success, "-d: -f:10"},
        // FIXME: review
        //{"-d12", s_extra_parameter, ""},
        {"-f12", s_success, "-f:12"}, {"-fe", s_success, "-f:e"},
        {nullptr, 0, nullptr}};
    test_cmdline(",d ,f= ,e", style, test_cases4);
}

void test_dos_options()
{
    using namespace command_line_style;
    cmdline::style_t style;

    style = cmdline::style_t(
        allow_short | allow_slash_for_short | short_allow_adjacent);

    test_case test_cases1[] = {{"/d d -bar", s_success, "-d: d -bar"},
        {"--foo", s_success, "--foo"}, {"/d13", s_extra_parameter, ""},
        {"/f14", s_success, "-f:14"}, {"/f", s_missing_parameter, ""},
        {nullptr, 0, nullptr}};
    test_cmdline(",d ,f=", style, test_cases1);

    style = cmdline::style_t(allow_short | allow_slash_for_short |
        short_allow_next | short_allow_adjacent | allow_sticky);

    test_case test_cases2[] = {{"/de", s_extra_parameter, ""},
        {"/fe", s_success, "-f:e"}, {nullptr, 0, nullptr}};
    test_cmdline(",d ,f= ,e", style, test_cases2);
}

void test_disguised_long()
{
    using namespace command_line_style;
    cmdline::style_t style;

    style = cmdline::style_t(allow_short | short_allow_adjacent |
        allow_dash_for_short | short_allow_next | allow_long_disguise |
        long_allow_adjacent);

    test_case test_cases1[] = {{"-foo -f", s_success, "foo: foo:"},
        {"-goo=x -gy", s_success, "goo:x goo:y"},
        {"-bee=x -by", s_success, "bee:x bee:y"}, {nullptr, 0, nullptr}};
    test_cmdline("foo,f goo,g= bee,b?", style, test_cases1);

    style = cmdline::style_t(style | allow_slash_for_short);
    test_case test_cases2[] = {{"/foo -f", s_success, "foo: foo:"},
        {"/goo=x", s_success, "goo:x"}, {nullptr, 0, nullptr}};
    test_cmdline("foo,f goo,g= bee,b?", style, test_cases2);
}

void test_guessing()
{
    using namespace command_line_style;
    cmdline::style_t style;

    style = cmdline::style_t(allow_short | short_allow_adjacent |
        allow_dash_for_short | allow_long | long_allow_adjacent |
        allow_guessing | allow_long_disguise);

    test_case test_cases1[] = {{"--opt1", s_success, "opt123:"},
        {"--opt", s_ambiguous_option, ""}, {"--f=1", s_success, "foo:1"},
        {"-far", s_success, "foo:ar"}, {nullptr, 0, nullptr}};
    test_cmdline("opt123 opt56 foo,f=", style, test_cases1);

    test_case test_cases2[] = {
        {"--fname file --fname2 file2", s_success, "fname: file fname2: file2"},
        {"--fnam file --fnam file2", s_ambiguous_option, ""},
        {"--fnam file --fname2 file2", s_ambiguous_option, ""},
        {"--fname2 file2 --fnam file", s_ambiguous_option, ""},
        {nullptr, 0, nullptr}};
    test_cmdline("fname fname2", style, test_cases2);
}

void test_arguments()
{
    using namespace command_line_style;
    cmdline::style_t style;

    style = cmdline::style_t(allow_short | allow_long | allow_dash_for_short |
        short_allow_adjacent | long_allow_adjacent);

    test_case test_cases1[] = {
        {"-f file -gx file2", s_success, "-f: file -g:x file2"},
        {"-f - -gx - -- -e", s_success, "-f: - -g:x - -e"},
        {nullptr, 0, nullptr}};
    test_cmdline(",f ,g= ,e", style, test_cases1);

    // "--" should stop options regardless of whether long options are
    // allowed or not.

    style = cmdline::style_t(
        allow_short | short_allow_adjacent | allow_dash_for_short);

    test_case test_cases2[] = {
        {"-f - -gx - -- -e", s_success, "-f: - -g:x - -e"},
        {nullptr, 0, nullptr}};
    test_cmdline(",f ,g= ,e", style, test_cases2);
}

void test_prefix()
{
    using namespace command_line_style;
    cmdline::style_t style;

    style = cmdline::style_t(allow_short | allow_long | allow_dash_for_short |
        short_allow_adjacent | long_allow_adjacent);

    test_case test_cases1[] = {
        {"--foo.bar=12", s_success, "foo.bar:12"}, {nullptr, 0, nullptr}};

    test_cmdline("foo*=", style, test_cases1);
}

pair<string, string> at_option_parser(string const& s)
{
    if ('@' == s[0])
        return std::make_pair(string("response-file"), s.substr(1));
    else
        return pair<string, string>();
}

pair<string, string> at_option_parser_broken(string const& s)
{
    if ('@' == s[0])
        return std::make_pair(string("some garbage"), s.substr(1));
    else
        return pair<string, string>();
}

void test_additional_parser()
{
    options_description desc;
    // clang-format off
    desc.add_options()
        ("response-file", value<string>(), "response file")
        ("foo", value<int>(), "foo");
    // clang-format on
    desc.add_options()("bar,baz", value<int>(), "bar");

    vector<string> input;
    input.emplace_back("@config");
    input.emplace_back("--foo=1");
    // the long_names() API function was introduced in Boost V1.68
    input.emplace_back("--baz=11");

    cmdline cmd(input);
    cmd.set_options_description(desc);
    cmd.set_additional_parser(at_option_parser);

    vector<option> result = cmd.run();

    // the long_names() API function was introduced in Boost V1.68
    HPX_TEST_EQ(result.size(), std::size_t(3));
    HPX_TEST_EQ(result[0].string_key, "response-file");
    HPX_TEST_EQ(result[0].value[0], "config");
    HPX_TEST_EQ(result[1].string_key, "foo");
    HPX_TEST_EQ(result[1].value[0], "1");
    // the long_names() API function was introduced in Boost V1.68
    HPX_TEST_EQ(result[2].string_key, "bar");
    HPX_TEST_EQ(result[2].value[0], "11");

    // Test that invalid options returned by additional style
    // parser are detected.
    cmdline cmd2(input);
    cmd2.set_options_description(desc);
    cmd2.set_additional_parser(at_option_parser_broken);

    bool caught_exception = false;
    try
    {
        cmd2.run();
    }
    catch (unknown_option const&)
    {
        caught_exception = true;
    }
    HPX_TEST(caught_exception);
}

vector<option> at_option_parser2(vector<string>& args)
{
    vector<option> result;
    if ('@' == args[0][0])
    {
        // Simulate reading the response file.
        result.emplace_back(option("foo", vector<string>(1, "1")));
        result.emplace_back(option("bar", vector<string>(1, "1")));
        args.erase(args.begin());
    }
    return result;
}

void test_style_parser()
{
    options_description desc;
    desc.add_options()("foo", value<int>(), "foo")("bar", value<int>(), "bar");

    vector<string> input;
    input.emplace_back("@config");

    cmdline cmd(input);
    cmd.set_options_description(desc);
    cmd.extra_style_parser(at_option_parser2);

    vector<option> result = cmd.run();

    HPX_TEST_EQ(result.size(), std::size_t(2));
    HPX_TEST_EQ(result[0].string_key, "foo");
    HPX_TEST_EQ(result[0].value[0], "1");
    HPX_TEST_EQ(result[1].string_key, "bar");
    HPX_TEST_EQ(result[1].value[0], "1");
}

void test_unregistered()
{
    // Check unregistered option when no options are registered at all.
    options_description desc;

    vector<string> input;
    input.emplace_back("--foo=1");
    input.emplace_back("--bar");
    input.emplace_back("1");
    input.emplace_back("-b");
    input.emplace_back("-biz");

    cmdline cmd(input);
    cmd.set_options_description(desc);
    cmd.allow_unregistered();

    vector<option> result = cmd.run();
    HPX_TEST_EQ(result.size(), std::size_t(5));
    // --foo=1
    HPX_TEST_EQ(result[0].string_key, "foo");
    HPX_TEST_EQ(result[0].unregistered, true);
    HPX_TEST_EQ(result[0].value[0], "1");
    // --bar
    HPX_TEST_EQ(result[1].string_key, "bar");
    HPX_TEST_EQ(result[1].unregistered, true);
    HPX_TEST(result[1].value.empty());
    // '1' is considered a positional option, not a value to
    // --bar
    HPX_TEST(result[2].string_key.empty());
    HPX_TEST_EQ(result[2].position_key, 0);
    HPX_TEST_EQ(result[2].unregistered, false);
    HPX_TEST_EQ(result[2].value[0], "1");
    // -b
    HPX_TEST_EQ(result[3].string_key, "-b");
    HPX_TEST_EQ(result[3].unregistered, true);
    HPX_TEST(result[3].value.empty());
    // -biz
    HPX_TEST_EQ(result[4].string_key, "-b");
    HPX_TEST_EQ(result[4].unregistered, true);
    HPX_TEST_EQ(result[4].value[0], "iz");

    // Check sticky short options together with unregistered options.

    desc.add_options()("help,h", "")("magic,m", value<string>(), "");

    input.clear();
    input.emplace_back("-hc");
    input.emplace_back("-mc");

    cmdline cmd2(input);
    cmd2.set_options_description(desc);
    cmd2.allow_unregistered();

    result = cmd2.run();

    HPX_TEST_EQ(result.size(), std::size_t(3));
    HPX_TEST_EQ(result[0].string_key, "help");
    HPX_TEST_EQ(result[0].unregistered, false);
    HPX_TEST(result[0].value.empty());
    HPX_TEST_EQ(result[1].string_key, "-c");
    HPX_TEST_EQ(result[1].unregistered, true);
    HPX_TEST(result[1].value.empty());
    HPX_TEST_EQ(result[2].string_key, "magic");
    HPX_TEST_EQ(result[2].unregistered, false);
    HPX_TEST_EQ(result[2].value[0], "c");

    // CONSIDER:
    // There's a corner case:
    //   -foo
    // when 'allow_long_disguise' is set. Should this be considered
    // disguised long option 'foo' or short option '-f' with value 'oo'?
    // It's not clear yet, so I'm leaving the decision till later.
}

void test_implicit_value()
{
    using namespace command_line_style;
    cmdline::style_t style;

    style = cmdline::style_t(allow_long | long_allow_adjacent);

    test_case test_cases1[] = {
        // 'bar' does not even look like option, so is consumed
        {"--foo bar", s_success, "foo:bar"},
        // '--bar' looks like option, and such option exists, so we don't
        // consume this token
        {"--foo --bar", s_success, "foo: bar:"},
        // '--biz' looks like option, but does not match any existing one.
        // Presently this results in parse error, since
        // (1) in cmdline.cpp:finish_option, we only consume following tokens
        //     if they are required
        // (2) in cmdline.cpp:run, we let options consume following positional
        //     options
        // For --biz, an exception is thrown between 1 and 2.
        // We might want to fix that in future.
        {"--foo --biz", s_unknown_option, ""}, {nullptr, 0, nullptr}};

    test_cmdline("foo? bar?", style, test_cases1);
}

int main(int /*ac*/, char** /*av*/)
{
    test_long_options();
    test_short_options();
    test_dos_options();
    test_disguised_long();
    test_guessing();
    test_arguments();
    test_prefix();
    test_additional_parser();
    test_style_parser();
    test_unregistered();
    test_implicit_value();

    return hpx::util::report_errors();
}
