// Copyright (C) 2016-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <config.h>

#include <dhcp6/parser_context.h>
#include <dhcpsrv/parsers/simple_parser6.h>
#include <testutils/io_utils.h>
#include <testutils/log_utils.h>
#include <testutils/test_to_element.h>
#include <testutils/user_context_utils.h>
#include <testutils/gtest_utils.h>

#include <gtest/gtest.h>

#include <fstream>
#include <set>

#include <boost/algorithm/string.hpp>

using namespace isc::data;
using namespace std;
using namespace isc::test;

namespace isc {
namespace dhcp {
namespace test {

/// @brief compares two JSON trees
///
/// If differences are discovered, gtest failure is reported (using EXPECT_EQ)
///
/// @param a first to be compared
/// @param b second to be compared
void compareJSON(ConstElementPtr a, ConstElementPtr b) {
    expectEqWithDiff(a, b);
}

/// @brief Tests if the input string can be parsed with specific parser
///
/// The input text will be passed to bison parser of specified type.
/// Then the same input text is passed to legacy JSON parser and outputs
/// from both parsers are compared. The legacy comparison can be disabled,
/// if the feature tested is not supported by the old parser (e.g.
/// new comment styles)
///
/// @param txt text to be compared
/// @param parser_type bison parser type to be instantiated
/// @param compare whether to compare the output with legacy JSON parser
void testParser(const std::string& txt, Parser6Context::ParserType parser_type,
    bool compare = true) {
    SCOPED_TRACE("\n=== tested config ===\n" + txt + "=====================");

    ConstElementPtr test_json;
    ASSERT_NO_THROW({
            try {
                Parser6Context ctx;
                test_json = ctx.parseString(txt, parser_type);
            } catch (const std::exception &e) {
                cout << "EXCEPTION: " << e.what() << endl;
                throw;
            }

    });

    if (!compare) {
        return;
    }

    // Now compare if both representations are the same.
    ElementPtr reference_json;
    ASSERT_NO_THROW(reference_json = Element::fromJSON(txt, true));
    compareJSON(reference_json, test_json);
}

TEST(ParserTest, mapInMap) {
    string txt = "{ \"xyzzy\": { \"foo\": 123, \"baz\": 456 } }";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, listInList) {
    string txt = "[ [ \"Britain\", \"Wales\", \"Scotland\" ], "
                 "[ \"Pomorze\", \"Wielkopolska\", \"Tatry\"] ]";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, nestedMaps) {
    string txt = "{ \"europe\": { \"UK\": { \"London\": { \"street\": \"221B Baker\" }}}}";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, nestedLists) {
    string txt = "[ \"half\", [ \"quarter\", [ \"eighth\", [ \"sixteenth\" ]]]]";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, listsInMaps) {
    string txt = "{ \"constellations\": { \"orion\": [ \"rigel\", \"betelgeuse\" ], "
                 "\"cygnus\": [ \"deneb\", \"albireo\"] } }";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, mapsInLists) {
    string txt = "[ { \"body\": \"earth\", \"gravity\": 1.0 }, "
                 "{ \"body\": \"mars\", \"gravity\": 0.376 } ]";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, types) {
    string txt = "{ \"string\": \"foo\", "
                 "\"integer\": 42, "
                 "\"boolean\": true, "
                 "\"map\": { \"foo\": \"bar\" }, "
                 "\"list\": [ 1, 2, 3 ], "
                 "\"null\": null }";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, keywordJSON) {
    string txt = "{ \"name\": \"user\", "
                 "\"type\": \"password\", "
                 "\"user\": \"name\", "
                 "\"password\": \"type\" }";
    testParser(txt, Parser6Context::PARSER_JSON);
}

TEST(ParserTest, keywordDhcp6) {
     string txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
                  "  \"interfaces\": [ \"type\", \"htype\" ] },\n"
                  "\"preferred-lifetime\": 3000,\n"
                  "\"rebind-timer\": 2000, \n"
                  "\"renew-timer\": 1000, \n"
                  "\"subnet6\": [ { "
                  "    \"id\": 1, "
                  "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
                  "    \"subnet\": \"2001:db8:1::/48\", "
                  "    \"interface\": \"test\" } ],\n"
                  "\"valid-lifetime\": 4000 } }";
     testParser(txt, Parser6Context::PARSER_DHCP6);
}

// Tests if bash (#) comments are supported. That's the only comment type that
// was supported by the old parser.
TEST(ParserTest, bashComments) {
    string txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "\"preferred-lifetime\": 3000,\n"
                 "# this is a comment\n"
                 "\"rebind-timer\": 2000, \n"
                 "# lots of comments here\n"
                 "# and here\n"
                 "\"renew-timer\": 1000, \n"
                 "\"subnet6\": [ { "
                 "    \"id\": 1, "
                 "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
                 "    \"subnet\": \"2001:db8:1::/48\", "
                 "    \"interface\": \"eth0\""
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
    testParser(txt, Parser6Context::PARSER_DHCP6);
}

// Tests if C++ (//) comments can start anywhere, not just in the first line.
TEST(ParserTest, cppComments) {
    string txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "\"preferred-lifetime\": 3000, // this is a comment \n"
                 "\"rebind-timer\": 2000, // everything after // is ignored\n"
                 "\"renew-timer\": 1000, // this will be ignored, too\n"
                 "\"subnet6\": [ { "
                 "    \"id\": 1, "
                 "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
                 "    \"subnet\": \"2001:db8:1::/48\", "
                 "    \"interface\": \"eth0\""
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
    testParser(txt, Parser6Context::PARSER_DHCP6, false);
}

// Tests if bash (#) comments can start anywhere, not just in the first line.
TEST(ParserTest, bashCommentsInline) {
    string txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "\"preferred-lifetime\": 3000, # this is a comment \n"
                 "\"rebind-timer\": 2000, # everything after # is ignored\n"
                 "\"renew-timer\": 1000, # this will be ignored, too\n"
                 "\"subnet6\": [ { "
                 "    \"id\": 1, "
                 "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
                 "    \"subnet\": \"2001:db8:1::/48\", "
                 "    \"interface\": \"eth0\""
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
    testParser(txt, Parser6Context::PARSER_DHCP6, false);
}

// Tests if multi-line C style comments are handled correctly.
TEST(ParserTest, multilineComments) {
    string txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "\"preferred-lifetime\": 3000, /* this is a C style comment\n"
                 "that\n can \n span \n multiple \n lines */ \n"
                 "\"rebind-timer\": 2000,\n"
                 "\"renew-timer\": 1000, \n"
                 "\"subnet6\": [ { "
                 "    \"id\": 1, "
                 "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
                 "    \"subnet\": \"2001:db8:1::/48\", "
                 "    \"interface\": \"eth0\""
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
    testParser(txt, Parser6Context::PARSER_DHCP6, false);
}

// Tests if embedded comments are handled correctly.
TEST(ParserTest, embbededComments) {
    string txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "\"comment\": \"a comment\",\n"
                 "\"preferred-lifetime\": 3000,\n"
                 "\"rebind-timer\": 2000,\n"
                 "\"renew-timer\": 1000, \n"
                 "\"subnet6\": [ { "
                 "    \"id\": 1, "
                 "    \"user-context\": { \"comment\": \"indirect\" },"
                 "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
                 "    \"subnet\": \"2001:db8:1::/48\", "
                 "    \"interface\": \"eth0\""
                 " } ],"
                 "\"user-context\": { \"compatible\": true },"
                 "\"valid-lifetime\": 4000 } }";
    testParser(txt, Parser6Context::PARSER_DHCP6, false);
}

// Test that output_options is an alias of output-options.
TEST(ParserTest, outputDashOptions) {
    string txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "\"preferred-lifetime\": 3000,\n"
                 "\"rebind-timer\": 2000,\n"
                 "\"renew-timer\": 1000, \n"
                 "\"valid-lifetime\": 4000,\n"
                 "\"loggers\": [ { "
                 "    \"name\": \"kea-dhcp6\","
                 "    \"output_options\": [ { \"output\": \"stdout\" } ],"
                 "    \"severity\": \"INFO\" } ]\n"
                 "} }";
    testParser(txt, Parser6Context::PARSER_DHCP6, false);
}

/// @brief Loads specified example config file
///
/// This test loads specified example file twice: first, using the legacy
/// JSON file and then second time using bison parser. Two created Element
/// trees are then compared. The input is decommented before it is passed
/// to legacy parser (as legacy support for comments is very limited).
///
/// @param fname name of the file to be loaded
void testFile(const std::string& fname) {
    ElementPtr json;
    ElementPtr reference_json;
    ConstElementPtr test_json;

    string decommented = decommentJSONfile(fname);

    cout << "Parsing file " << fname << "(" << decommented << ")" << endl;

    EXPECT_NO_THROW_LOG(json = Element::fromJSONFile(decommented, true));
    reference_json = moveComments(json);

    // remove the temporary file
    EXPECT_NO_THROW(::remove(decommented.c_str()));

    EXPECT_NO_THROW(
    try {
        Parser6Context ctx;
        test_json = ctx.parseFile(fname, Parser6Context::PARSER_DHCP6);
    } catch (const std::exception &x) {
        cout << "EXCEPTION: " << x.what() << endl;
        throw;
    });

    ASSERT_TRUE(reference_json);
    ASSERT_TRUE(test_json);

    compareJSON(reference_json, test_json);
}

// This test loads all available existing files. Each config is loaded
// twice: first with the existing Element::fromJSONFile() and then
// the second time with Parser6. Both JSON trees are then compared.
TEST(ParserTest, file) {
    vector<string> configs = { "advanced.json",
                               "all-keys.json",
                               "all-options.json",
                               "backends.json",
                               "classify.json",
                               "classify2.json",
                               "comments.json",
                               "config-backend.json",
                               "dhcpv4-over-dhcpv6.json",
                               "duid.json",
                               "global-reservations.json",
                               "ha-hot-standby-server1-with-tls.json",
                               "ha-hot-standby-server2.json",
                               "hooks.json",
                               "iPXE.json",
                               "leases-expiration.json",
                               "multiple-options.json",
                               "mysql-reservations.json",
                               "pgsql-reservations.json",
                               "reservations.json",
                               "several-subnets.json",
                               "shared-network.json",
                               "simple.json",
                               "softwire46.json",
                               "stateless.json",
                               "tee-times.json",
                               "with-ddns.json" };

    for (unsigned i = 0; i<configs.size(); i++) {
        testFile(string(CFG_EXAMPLES) + "/" + configs[i]);
    }
}

// This test loads the all-keys.json file and checks global parameters.
TEST(ParserTest, globalParameters) {
    ConstElementPtr json;
    Parser6Context ctx;
    string fname = string(CFG_EXAMPLES) + "/" + "all-keys.json";
    EXPECT_NO_THROW(json = ctx.parseFile(fname, Parser6Context::PARSER_DHCP6));
    EXPECT_NO_THROW(json = json->get("Dhcp6"));
    SimpleParser6 parser;
    EXPECT_NO_THROW(parser.checkKeywords(parser.GLOBAL6_PARAMETERS, json));
}

/// @brief Tests error conditions in Dhcp6Parser
///
/// @param txt text to be parsed
/// @param parser_type type of the parser to be used in the test
/// @param msg expected content of the exception
void testError(const std::string& txt,
               Parser6Context::ParserType parser_type,
               const std::string& msg) {
    SCOPED_TRACE("\n=== tested config ===\n" + txt + "=====================");

    try {
        Parser6Context ctx;
        ConstElementPtr parsed = ctx.parseString(txt, parser_type);
        FAIL() << "Expected Dhcp6ParseError but nothing was raised (expected: "
               << msg << ")";
    }
    catch (const Dhcp6ParseError& ex) {
        EXPECT_EQ(msg, ex.what());
    }
    catch (...) {
        FAIL() << "Expected Dhcp6ParseError but something else was raised";
    }
}

// Verify that error conditions are handled correctly.
TEST(ParserTest, errors) {
    // no input
    testError("", Parser6Context::PARSER_JSON,
              "<string>:1.1: syntax error, unexpected end of file");
    testError(" ", Parser6Context::PARSER_JSON,
              "<string>:1.2: syntax error, unexpected end of file");
    testError("\n", Parser6Context::PARSER_JSON,
              "<string>:2.1: syntax error, unexpected end of file");
    testError("\t", Parser6Context::PARSER_JSON,
              "<string>:1.2: syntax error, unexpected end of file");
    testError("\r", Parser6Context::PARSER_JSON,
              "<string>:1.2: syntax error, unexpected end of file");

    // comments
    testError("# nothing\n",
              Parser6Context::PARSER_JSON,
              "<string>:2.1: syntax error, unexpected end of file");
    testError(" #\n",
              Parser6Context::PARSER_JSON,
              "<string>:2.1: syntax error, unexpected end of file");
    testError("// nothing\n",
              Parser6Context::PARSER_JSON,
              "<string>:2.1: syntax error, unexpected end of file");
    testError("/* nothing */\n",
              Parser6Context::PARSER_JSON,
              "<string>:2.1: syntax error, unexpected end of file");
    testError("/* no\nthing */\n",
              Parser6Context::PARSER_JSON,
              "<string>:3.1: syntax error, unexpected end of file");
    testError("/* no\nthing */\n\n",
              Parser6Context::PARSER_JSON,
              "<string>:4.1: syntax error, unexpected end of file");
    testError("/* nothing\n",
              Parser6Context::PARSER_JSON,
              "Comment not closed. (/* in line 1");
    testError("\n\n\n/* nothing\n",
              Parser6Context::PARSER_JSON,
              "Comment not closed. (/* in line 4");
    testError("{ /* */*/ }\n",
              Parser6Context::PARSER_JSON,
              "<string>:1.3-8: Invalid character: *");
    testError("{ /* // *// }\n",
              Parser6Context::PARSER_JSON,
              "<string>:1.3-11: Invalid character: /");
    testError("{ /* // *///  }\n",
              Parser6Context::PARSER_JSON,
              "<string>:2.1: syntax error, unexpected end of file, "
              "expecting }");

    // includes
    testError("<?\n",
              Parser6Context::PARSER_JSON,
              "Directive not closed.");
    testError("<?include\n",
              Parser6Context::PARSER_JSON,
              "Directive not closed.");
    string file = string(CFG_EXAMPLES) + "/" + "stateless.json";
    testError("<?include \"" + file + "\"\n",
              Parser6Context::PARSER_JSON,
              "Directive not closed.");
    testError("<?include \"/foo/bar\" ?>/n",
              Parser6Context::PARSER_JSON,
              "Can't open include file /foo/bar");

    // JSON keywords
    testError("{ \"foo\": True }",
              Parser6Context::PARSER_JSON,
              "<string>:1.10-13: JSON true reserved keyword is lower case only");
    testError("{ \"foo\": False }",
              Parser6Context::PARSER_JSON,
              "<string>:1.10-14: JSON false reserved keyword is lower case only");
    testError("{ \"foo\": NULL }",
              Parser6Context::PARSER_JSON,
              "<string>:1.10-13: JSON null reserved keyword is lower case only");
    testError("{ \"foo\": Tru }",
              Parser6Context::PARSER_JSON,
              "<string>:1.10: Invalid character: T");
    testError("{ \"foo\": nul }",
              Parser6Context::PARSER_JSON,
              "<string>:1.10: Invalid character: n");

    // numbers
    testError("123",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1-3: syntax error, unexpected integer, "
              "expecting {");
    testError("-456",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1-4: syntax error, unexpected integer, "
              "expecting {");
    testError("-0001",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1-5: syntax error, unexpected integer, "
              "expecting {");
    testError("1234567890123456789012345678901234567890",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-40: Failed to convert "
              "1234567890123456789012345678901234567890"
              " to an integer.");
    testError("-3.14e+0",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1-8: syntax error, unexpected floating point, "
              "expecting {");
    testError("1e50000",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-7: Failed to convert 1e50000 "
              "to a floating point.");

    // strings
    testError("\"aabb\"",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1-6: syntax error, unexpected constant string, "
              "expecting {");
    testError("{ \"aabb\"err",
              Parser6Context::PARSER_JSON,
              "<string>:1.9: Invalid character: e");
    testError("{ err\"aabb\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.3: Invalid character: e");
    testError("\"a\n\tb\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-6 (near 2): Invalid control in \"a\n\tb\"");
    testError("\"a\n\\u12\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-8 (near 2): Invalid control in \"a\n\\u12\"");
    testError("\"a\\n\\tb\"",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1-8: syntax error, unexpected constant string, "
              "expecting {");
    testError("\"a\\x01b\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-8 (near 3): Bad escape in \"a\\x01b\"");
    testError("\"a\\u0162\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-9 (near 4): Unsupported unicode escape "
              "in \"a\\u0162\"");
    testError("\"a\\u062z\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-9 (near 3): Bad escape in \"a\\u062z\"");
    testError("\"abc\\\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-6 (near 6): Overflow escape in \"abc\\\"");
    testError("\"a\\u006\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-8 (near 3): Overflow unicode escape "
              "in \"a\\u006\"");
    testError("\"\\u\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-4 (near 2): Overflow unicode escape in \"\\u\"");
    testError("\"\\u\x02\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-5 (near 2): Bad escape in \"\\u\x02\"");
    testError("\"\\u\\\"foo\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-5 (near 2): Bad escape in \"\\u\\\"...");
    testError("\"\x02\\u\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1-5 (near 1): Invalid control in \"\x02\\u\"");

    // from data_unittest.c
    testError("\\a",
              Parser6Context::PARSER_JSON,
              "<string>:1.1: Invalid character: \\");
    testError("\\",
              Parser6Context::PARSER_JSON,
              "<string>:1.1: Invalid character: \\");
    testError("\\\"\\\"",
              Parser6Context::PARSER_JSON,
              "<string>:1.1: Invalid character: \\");

    // want a map
    testError("[]\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1: syntax error, unexpected [, "
              "expecting {");
    testError("[]\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.1: syntax error, unexpected [, "
              "expecting {");
    testError("{ 123 }\n",
              Parser6Context::PARSER_JSON,
              "<string>:1.3-5: syntax error, unexpected integer, "
              "expecting }");
    testError("{ 123 }\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.3-5: syntax error, unexpected integer, "
              "expecting Dhcp6");
    testError("{ \"foo\" }\n",
              Parser6Context::PARSER_JSON,
              "<string>:1.9: syntax error, unexpected }, "
              "expecting :");
    testError("{ \"foo\" }\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.3-7: syntax error, unexpected constant string, "
              "expecting Dhcp6");
    testError("{ \"foo\":null }\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.3-7: syntax error, unexpected constant string, "
              "expecting Dhcp6");
    testError("{ \"Logging\":null }\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.3-11: syntax error, unexpected constant string, "
              "expecting Dhcp6");
    testError("{ \"Dhcp6\" }\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:1.11: syntax error, unexpected }, "
              "expecting :");
    testError("{}{}\n",
              Parser6Context::PARSER_JSON,
              "<string>:1.3: syntax error, unexpected {, "
              "expecting end of file");

    // duplicate in map
    testError("{ \"foo\": 1, \"foo\": true }\n",
              Parser6Context::PARSER_JSON,
              "<string>:1:13: duplicate foo entries in "
              "JSON map (previous at <string>:1:10)");

    // bad commas
    testError("{ , }\n",
              Parser6Context::PARSER_JSON,
              "<string>:1.3: syntax error, unexpected \",\", "
              "expecting }");
    testError("{ , \"foo\":true }\n",
              Parser6Context::PARSER_JSON,
              "<string>:1.3: syntax error, unexpected \",\", "
              "expecting }");

    // bad type
    testError("{ \"Dhcp6\":{\n"
              "  \"preferred-lifetime\":false }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:2.24-28: syntax error, unexpected boolean, "
              "expecting integer");

    // unknown keyword
    testError("{ \"Dhcp6\":{\n"
              " \"preferred_lifetime\":600 }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:2.2-21: got unexpected keyword "
              "\"preferred_lifetime\" in Dhcp6 map.");

    // missing parameter
    testError("{ \"name\": \"foo\",\n"
              "  \"code\": 123 }\n",
              Parser6Context::PARSER_OPTION_DEF,
              "missing parameter 'type' (<string>:1:1) "
              "[option-def map between <string>:1:1 and <string>:2:15]");

    // user context and embedded comments
    testError("{ \"Dhcp6\":{\n"
              "  \"comment\": true,\n"
              "  \"preferred-lifetime\": 600 }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:2.14-17: syntax error, unexpected boolean, "
              "expecting constant string");

    testError("{ \"Dhcp6\":{\n"
              "  \"user-context\": \"a comment\",\n"
              "  \"preferred-lifetime\": 600 }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:2.19-29: syntax error, unexpected constant string, "
              "expecting {");

    testError("{ \"Dhcp6\":{\n"
              "  \"comment\": \"a comment\",\n"
              "  \"comment\": \"another one\",\n"
              "  \"preferred-lifetime\": 600 }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:3.3-11: duplicate user-context/comment entries "
              "(previous at <string>:2:3)");

    testError("{ \"Dhcp6\":{\n"
              "  \"user-context\": { \"version\": 1 },\n"
              "  \"user-context\": { \"one\": \"only\" },\n"
              "  \"preferred-lifetime\": 600 }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:3.3-16: duplicate user-context entries "
              "(previous at <string>:2:19)");

    testError("{ \"Dhcp6\":{\n"
              "  \"user-context\": { \"comment\": \"indirect\" },\n"
              "  \"comment\": \"a comment\",\n"
              "  \"preferred-lifetime\": 600 }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:3.3-11: duplicate user-context/comment entries "
              "(previous at <string>:2:19)");

    // duplicate Dhcp6 entries
    testError("{ \"Dhcp6\":{\n"
              "  \"comment\": \"first\" },\n"
              "  \"Dhcp6\":{\n"
              "  \"comment\": \"second\" }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:3.3-9: syntax error, unexpected Dhcp6, expecting \",\" or }");

    // duplicate of not string entries
    testError("{ \"Dhcp6\":{\n"
              " \"subnet6\": [],\n"
              " \"subnet6\": [] }}\n",
              Parser6Context::PARSER_DHCP6,
              "<string>:3:2: duplicate subnet6 entries in "
              "Dhcp6 map (previous at <string>:2:2)");

    // duplicate of string entries
    testError("{ \"data\": \"foo\",\n"
              " \"data\": \"bar\" }\n",
              Parser6Context::PARSER_OPTION_DATA,
              "<string>:2:2: duplicate data entries in "
              "option-data map (previous at <string>:1:11)");
}

// Check unicode escapes
TEST(ParserTest, unicodeEscapes) {
    ConstElementPtr result;
    string json;

    // check we can reread output
    for (char c = -128; c < 127; ++c) {
        string ins(" ");
        ins[1] = c;
        ConstElementPtr e(new StringElement(ins));
        json = e->str();
        ASSERT_NO_THROW(
        try {
            Parser6Context ctx;
            result = ctx.parseString(json, Parser6Context::PARSER_JSON);
        } catch (const std::exception &x) {
            cout << "EXCEPTION: " << x.what() << endl;
            throw;
        });
        ASSERT_EQ(Element::string, result->getType());
        EXPECT_EQ(ins, result->stringValue());
    }
}

// This test checks that all representations of a slash is recognized properly.
TEST(ParserTest, unicodeSlash) {
    // check the 4 possible encodings of solidus '/'
    ConstElementPtr result;
    string json = "\"/\\/\\u002f\\u002F\"";
    ASSERT_NO_THROW(
    try {
        Parser6Context ctx;
        result = ctx.parseString(json, Parser6Context::PARSER_JSON);
    } catch (const std::exception &x) {
        cout << "EXCEPTION: " << x.what() << endl;
        throw;
    });
    ASSERT_EQ(Element::string, result->getType());
    EXPECT_EQ("////", result->stringValue());
}

/// @brief Load a file into a JSON element.
///
/// @param fname The name of the file to load.
/// @param list The JSON element list to add the parsing result to.
void loadFile(const string& fname, ElementPtr list) {
    Parser6Context ctx;
    ElementPtr json;
    EXPECT_NO_THROW(json = ctx.parseFile(fname, Parser6Context::PARSER_DHCP6));
    ASSERT_TRUE(json);
    list->add(json);
}

// This test checks that all map entries are in the example files.
TEST(ParserTest, mapEntries) {
    // Type of keyword set.
    typedef set<string> KeywordSet;

    // Get keywords from the syntax file (dhcp6_parser.yy).
    ifstream syntax_file(SYNTAX_FILE);
    EXPECT_TRUE(syntax_file.is_open());
    string line;
    KeywordSet syntax_keys = { "user-context" };
    // Code setting the map entry.
    const string pattern = "ctx.stack_.back()->set(\"";
    while (getline(syntax_file, line)) {
        // Skip comments.
        size_t comment = line.find("//");
        if (comment <= pattern.size()) {
            continue;
        }
        if (comment != string::npos) {
            line.resize(comment);
        }
        // Search for the code pattern.
        size_t key_begin = line.find(pattern);
        if (key_begin == string::npos) {
            continue;
        }
        // Extract keywords.
        line = line.substr(key_begin + pattern.size());
        size_t key_end = line.find_first_of('"');
        EXPECT_NE(string::npos, key_end);
        string keyword = line.substr(0, key_end);
        // Ignore result when adding the keyword to the syntax keyword set.
        static_cast<void>(syntax_keys.insert(keyword));
    }
    syntax_file.close();

    // Get keywords from the example files.
    string sample_dir(CFG_EXAMPLES);
    sample_dir += "/";
    ElementPtr sample_json = Element::createList();
    loadFile(sample_dir + "advanced.json", sample_json);
    loadFile(sample_dir + "all-keys.json", sample_json);
    loadFile(sample_dir + "duid.json", sample_json);
    loadFile(sample_dir + "reservations.json", sample_json);
    loadFile(sample_dir + "all-keys-netconf.json", sample_json);
    KeywordSet sample_keys = {
        "data-directory",
        "hosts-database"
    };
    // Recursively extract keywords.
    static void (*extract)(ConstElementPtr, KeywordSet&) =
        [] (ConstElementPtr json, KeywordSet& set) {
            if (json->getType() == Element::list) {
                // Handle lists.
                for (auto const& elem : json->listValue()) {
                    extract(elem, set);
                }
            } else if (json->getType() == Element::map) {
                // Handle maps.
                for (auto const& elem : json->mapValue()) {
                    static_cast<void>(set.insert(elem.first));
                    // Skip entries with free content.
                    if ((elem.first != "user-context") &&
                        (elem.first != "parameters")) {
                        extract(elem.second, set);
                    }
                }
            }
        };
    extract(sample_json, sample_keys);

    // Compare.
    auto print_keys = [](const KeywordSet& keys) {
        string s = "{";
        bool first = true;
        for (auto const& key : keys) {
            if (first) {
                first = false;
                s += " ";
            } else {
                s += ", ";
            }
            s += "\"" + key + "\"";
        }
        return (s + " }");
    };
    // Remove deprecated parameters. This needs to be done because even though
    // they are supported, examples are loaded by yang tests also and they don't
    // support both old and new syntax.
    ASSERT_EQ(syntax_keys.erase("client-class"), 1);
    ASSERT_EQ(syntax_keys.erase("only-if-required"), 1);
    ASSERT_EQ(syntax_keys.erase("require-client-classes"), 1);
    EXPECT_EQ(syntax_keys, sample_keys)
        << "syntax has: " << print_keys(syntax_keys) << endl
        << "sample has: " << print_keys(sample_keys) << endl;
}

/// @brief Tests a duplicate entry.
///
/// The entry was duplicated by adding a new <name>DDDD entry.
/// An error is expected, usually it is a duplicate but there are
/// a few syntax errors when the syntax allows only one parameter.
///
/// @param json the JSON configuration with the duplicate entry.
void testDuplicate(ConstElementPtr json) {
    string config = json->str();
    size_t where = config.find("DDDD");
    ASSERT_NE(string::npos, where);
    string before = config.substr(0, where);
    string after = config.substr(where + 4, string::npos);
    Parser6Context ctx;
    EXPECT_THROW(ctx.parseString(before + after,
                                 Parser6Context::PARSER_DHCP6),
                 Dhcp6ParseError) << "config: " << config;
}

// This test checks that duplicate entries make parsing to fail.
TEST(ParserTest, duplicateMapEntries) {
    // Get the config to work with from the all keys file.
    string sample_fname(CFG_EXAMPLES);
    sample_fname += "/all-keys.json";
    Parser6Context ctx;
    ElementPtr sample_json;
    EXPECT_NO_THROW(sample_json =
        ctx.parseFile(sample_fname, Parser6Context::PARSER_DHCP6));
    ASSERT_TRUE(sample_json);

    // Recursively check duplicates.
    static void (*test)(ElementPtr, ElementPtr, size_t&) =
        [] (ElementPtr config, ElementPtr json, size_t& cnt) {
            if (json->getType() == Element::list) {
                // Handle lists.
                for (auto const& elem : json->listValue()) {
                    test(config, elem, cnt);
                }
            } else if (json->getType() == Element::map) {
                // Handle maps.
                for (auto const& elem : json->mapValue()) {
                    // Skip entries with free content.
                    if ((elem.first == "user-context") ||
                        (elem.first == "parameters")) {
                        continue;
                    }

                    // Perform tests.
                    string dup = elem.first + "DDDD";
                    json->set(dup, elem.second);
                    testDuplicate(config);
                    json->remove(dup);
                    ++cnt;

                    // Recursive call.
                    ElementPtr mutable_json =
                        boost::const_pointer_cast<Element>(elem.second);
                    ASSERT_TRUE(mutable_json);
                    test(config, mutable_json, cnt);
                }
            }
        };
    size_t cnt = 0;
    test(sample_json, sample_json, cnt);
    cout << "checked " << cnt << " duplicated map entries\n";
}

/// @brief Test fixture for trailing commas.
class TrailingCommasTest : public isc::dhcp::test::LogContentTest {
public:
    /// @brief Add a log entry.
    ///
    /// @param loc Location of the trailing comma.
    void addLog(const string& loc) {
        string log = "DHCP6_CONFIG_SYNTAX_WARNING configuration syntax ";
        log += "warning: " + loc;
        log += ": Extraneous comma. ";
        log += "A piece of configuration may have been omitted.";
        addString(log);
    }
};

// Test that trailing commas are allowed.
TEST_F(TrailingCommasTest, tests) {
    string txt(R"({
  "Dhcp6": {
    "control-sockets": [
      {
        "socket-type": "http",
        "socket-address": "::1",
        "authentication": {
          "clients": [
            {
              "password-file": "/tmp/pwd",
            }
          ],
          "type": "basic",
        },
        "socket-port": 8000,
      },
    ],
    "hooks-libraries": [
      {
        "library": "/usr/local/lib/kea/hooks/libdhcp_dummy.so",
      },
    ],
    "interfaces-config": {
      "interfaces": [
        "eth0",
      ],
    },
    "lease-database": {
      "name": "/tmp/kea-dhcp6.csv",
      "persist": true,
      "type": "memfile",
    },
    "loggers": [
      {
        "debuglevel": 99,
        "name": "kea-dhcp6",
        "output-options": [
          {
            "output": "stdout",
          },
        ],
        "severity": "DEBUG",
      },
    ],
    "multi-threading": {
      "enable-multi-threading": false,
      "packet-queue-size": 0,
      "thread-pool-size": 0
    },
    "subnet6": [
      {
        "pools": [
          {
            "pool": "2001:db8:1::/64",
          },
        ],
        "subnet": "2001:db8:1::/64",
        "id": 1,
      },
    ],
  },
})");
    testParser(txt, Parser6Context::PARSER_DHCP6, false);

    addLog("<string>:10.42");
    addLog("<string>:13.26");
    addLog("<string>:15.28");
    addLog("<string>:16.8");
    addLog("<string>:20.63");
    addLog("<string>:21.8");
    addLog("<string>:25.15");
    addLog("<string>:26.8");
    addLog("<string>:31.24");
    addLog("<string>:39.31");
    addLog("<string>:40.12");
    addLog("<string>:42.28");
    addLog("<string>:43.8");
    addLog("<string>:54.38");
    addLog("<string>:55.12");
    addLog("<string>:58.16");
    addLog("<string>:59.8");
    addLog("<string>:60.6");
    addLog("<string>:61.4");
    EXPECT_TRUE(checkFile());

    // Test with many consecutive commas.
    boost::replace_all(txt, ",", ",,,,");
    testParser(txt, Parser6Context::PARSER_DHCP6, false);
}

/// @brief Tests control socket config conflicts.
TEST(ParserTest, duplicateControlSocket) {
    // Valid configuration.
    string txt(R"({
    "Dhcp6": {
        "control-socket": {
            "socket-type": "http",
            "socket-address": "::1"
        }
     }
})");
    testParser(txt, Parser6Context::PARSER_DHCP6, false);

    // Invalid configuration: both map and list.
    string bad1(R"({
    "Dhcp6": {
        "control-socket": {
            "socket-type": "http",
            "socket-address": "::1"
        },
        "control-sockets": [ ]
     }
})");

    ASSERT_NO_THROW(Element::fromJSON(bad1, true));
    Parser6Context ctx1;
    EXPECT_THROW(ctx1.parseString(bad1, Parser6Context::PARSER_DHCP6),
                 Dhcp6ParseError);

    // Invalid configuration: both name and address.
    string bad2(R"({
    "Dhcp6": {
        "control-socket": {
            "socket-type": "http",
            "socket-address": "::1",
            "socket-name": "127.0.0.1"
        }
     }
})");

    ASSERT_NO_THROW(Element::fromJSON(bad2, true));
    Parser6Context ctx2;
    EXPECT_THROW(ctx2.parseString(bad2, Parser6Context::PARSER_DHCP6),
                 Dhcp6ParseError);
}

}  // namespace test
}  // namespace dhcp
}  // namespace isc
