// Copyright (C) 2012-2024 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 <exceptions/exceptions.h>

#include <dns/master_lexer.h>
#include <dns/master_lexer_state.h>

#include <gtest/gtest.h>

#include <boost/lexical_cast.hpp>
#include <boost/scoped_ptr.hpp>

#include <string>
#include <sstream>

using namespace isc::dns;
using std::string;
using std::stringstream;
using boost::lexical_cast;
using boost::scoped_ptr;
using master_lexer_internal::State;

namespace {

class MasterLexerTest : public ::testing::Test {
protected:
    MasterLexerTest() :
        expected_stream_name("stream-" + lexical_cast<string>(&ss))
    {}

    MasterLexer lexer;
    stringstream ss;
    const string expected_stream_name;
};

// Commonly used check case where the input sources stack is empty.
void
checkEmptySource(const MasterLexer& lexer) {
    EXPECT_TRUE(lexer.getSourceName().empty());
    EXPECT_EQ(0, lexer.getSourceLine());
    EXPECT_EQ(0, lexer.getPosition());
}

TEST_F(MasterLexerTest, preOpen) {
    // Initially sources stack is empty.
    checkEmptySource(lexer);
}

TEST_F(MasterLexerTest, pushStream) {
    EXPECT_EQ(0, lexer.getSourceCount());
    ss << "test";
    lexer.pushSource(ss);
    EXPECT_EQ(expected_stream_name, lexer.getSourceName());
    EXPECT_EQ(1, lexer.getSourceCount());
    EXPECT_EQ(4, lexer.getTotalSourceSize()); // 4 = len("test")

    // From the point of view of this test, we only have to check (though
    // indirectly) getSourceLine calls InputSource::getCurrentLine.  It should
    // return 1 initially.
    EXPECT_EQ(1, lexer.getSourceLine());

    // By popping it the stack will be empty again.
    lexer.popSource();
    EXPECT_EQ(0, lexer.getSourceCount());
    checkEmptySource(lexer);
    EXPECT_EQ(4, lexer.getTotalSourceSize()); // this shouldn't change
}

TEST_F(MasterLexerTest, pushStreamFail) {
    // Pretend a "bad" thing happened in the stream.  This will make the
    // initialization throw an exception.
    ss << "test";
    ss.setstate(std::ios_base::badbit);

    EXPECT_THROW(lexer.pushSource(ss), isc::Unexpected);
}

TEST_F(MasterLexerTest, pushFile) {
    // We use zone file (-like) data, but in this test that actually doesn't
    // matter.
    EXPECT_EQ(0, lexer.getSourceCount());
    EXPECT_TRUE(lexer.pushSource(TEST_DATA_SRCDIR "/masterload.txt"));
    EXPECT_EQ(1, lexer.getSourceCount());
    EXPECT_EQ(TEST_DATA_SRCDIR "/masterload.txt", lexer.getSourceName());
    EXPECT_EQ(1, lexer.getSourceLine());

    // 143 = size of the test zone file.  hardcode it assuming it won't change
    // too often.
    EXPECT_EQ(143, lexer.getTotalSourceSize());

    lexer.popSource();
    checkEmptySource(lexer);
    EXPECT_EQ(0, lexer.getSourceCount());
    EXPECT_EQ(143, lexer.getTotalSourceSize()); // this shouldn't change

    // If we give a non null string pointer, its content will be intact
    // if pushSource succeeds.
    std::string error_txt = "dummy";
    EXPECT_TRUE(lexer.pushSource(TEST_DATA_SRCDIR "/masterload.txt",
                                 &error_txt));
    EXPECT_EQ("dummy", error_txt);
}

TEST_F(MasterLexerTest, pushBadFileName) {
    EXPECT_THROW(lexer.pushSource(0), isc::InvalidParameter);
}

TEST_F(MasterLexerTest, pushFileFail) {
    // The file to be pushed doesn't exist.  pushSource() fails and
    // some non empty error string should be set.
    std::string error_txt;
    EXPECT_TRUE(error_txt.empty());
    EXPECT_FALSE(lexer.pushSource("no-such-file", &error_txt));
    EXPECT_FALSE(error_txt.empty());

    // It's safe to pass null error_txt (either explicitly or implicitly as
    // the default)
    EXPECT_FALSE(lexer.pushSource("no-such-file", 0));
    EXPECT_FALSE(lexer.pushSource("no-such-file"));
}

TEST_F(MasterLexerTest, nestedPush) {
    const string test_txt = "test";
    ss << test_txt;
    lexer.pushSource(ss);

    EXPECT_EQ(test_txt.size(), lexer.getTotalSourceSize());
    EXPECT_EQ(0, lexer.getPosition());

    EXPECT_EQ(expected_stream_name, lexer.getSourceName());

    // Read the string; getPosition() should reflect that.
    EXPECT_EQ(MasterToken::STRING, lexer.getNextToken().getType());
    EXPECT_EQ(test_txt.size(), lexer.getPosition());

    // We can push another source without popping the previous one.
    lexer.pushSource(TEST_DATA_SRCDIR "/masterload.txt");
    EXPECT_EQ(TEST_DATA_SRCDIR "/masterload.txt", lexer.getSourceName());
    EXPECT_EQ(143 + test_txt.size(),
              lexer.getTotalSourceSize()); // see above for magic nums

    // the next token should be the EOL (skipping a comment line), its
    // position in the file is 35 (hardcoded).
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    EXPECT_EQ(test_txt.size() + 35, lexer.getPosition());

    // popSource() works on the "topmost" (last-pushed) source
    lexer.popSource();
    EXPECT_EQ(expected_stream_name, lexer.getSourceName());

    // pop shouldn't change the total size and the current position
    EXPECT_EQ(143 + test_txt.size(), lexer.getTotalSourceSize());
    EXPECT_EQ(test_txt.size() + 35, lexer.getPosition());

    lexer.popSource();
    EXPECT_TRUE(lexer.getSourceName().empty());

    // size and position still shouldn't change
    EXPECT_EQ(143 + test_txt.size(), lexer.getTotalSourceSize());
    EXPECT_EQ(test_txt.size() + 35, lexer.getPosition());
}

TEST_F(MasterLexerTest, unknownSourceSize) {
    // Similar to the previous case, but the size of the second source
    // will be considered "unknown" (by emulating an error).
    ss << "test";
    lexer.pushSource(ss);
    EXPECT_EQ(4, lexer.getTotalSourceSize());

    stringstream ss2;
    ss2.setstate(std::ios_base::failbit); // this will make the size unknown
    lexer.pushSource(ss2);
    // Then the total size is also unknown.
    EXPECT_EQ(MasterLexer::SOURCE_SIZE_UNKNOWN, lexer.getTotalSourceSize());

    // Even if we pop that source, the size is still unknown.
    lexer.popSource();
    EXPECT_EQ(MasterLexer::SOURCE_SIZE_UNKNOWN, lexer.getTotalSourceSize());
}

TEST_F(MasterLexerTest, invalidPop) {
    // popSource() cannot be called if the sources stack is empty.
    EXPECT_THROW(lexer.popSource(), isc::InvalidOperation);
}

// Test it is not possible to get token when no source is available.
TEST_F(MasterLexerTest, noSource) {
    EXPECT_THROW(lexer.getNextToken(), isc::InvalidOperation);
}

// Test getting some tokens.  It also check basic behavior of getPosition().
TEST_F(MasterLexerTest, getNextToken) {
    ss << "\n   \n\"STRING\"\n";
    lexer.pushSource(ss);

    // First, the newline should get out.
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    EXPECT_EQ(1, lexer.getPosition());
    // Then the whitespace, if we specify the option.
    EXPECT_EQ(MasterToken::INITIAL_WS,
              lexer.getNextToken(MasterLexer::INITIAL_WS).getType());
    EXPECT_EQ(2, lexer.getPosition());
    // The newline
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    EXPECT_EQ(5, lexer.getPosition()); // 1st \n + 3 spaces, then 2nd \n
    // The (quoted) string
    EXPECT_EQ(MasterToken::QSTRING,
              lexer.getNextToken(MasterLexer::QSTRING).getType());
    EXPECT_EQ(5 + 8, lexer.getPosition()); // 8 = len("STRING') + quotes

    // And the end of line and file
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    EXPECT_EQ(5 + 8 + 1, lexer.getPosition()); // previous + 3rd \n
    EXPECT_EQ(MasterToken::END_OF_FILE, lexer.getNextToken().getType());
    EXPECT_EQ(5 + 8 + 1, lexer.getPosition()); // position doesn't change
}

// Test we correctly find end of file.
TEST_F(MasterLexerTest, eof) {
    // Let the ss empty.
    lexer.pushSource(ss);

    // The first one is found to be EOF
    EXPECT_EQ(MasterToken::END_OF_FILE, lexer.getNextToken().getType());
    // And it stays on EOF for any following attempts
    EXPECT_EQ(MasterToken::END_OF_FILE, lexer.getNextToken().getType());
    // And we can step back one token, but that is the EOF too.
    lexer.ungetToken();
    EXPECT_EQ(MasterToken::END_OF_FILE, lexer.getNextToken().getType());
}

// Check we properly return error when there's an opened parentheses and no
// closing one
TEST_F(MasterLexerTest, getUnbalancedParen) {
    ss << "(string";
    lexer.pushSource(ss);

    // The string gets out first
    EXPECT_EQ(MasterToken::STRING, lexer.getNextToken().getType());
    // Then an unbalanced parenthesis
    EXPECT_EQ(MasterToken::UNBALANCED_PAREN,
              lexer.getNextToken().getErrorCode());
    // And then EOF
    EXPECT_EQ(MasterToken::END_OF_FILE, lexer.getNextToken().getType());
}

// Check we properly return error when there's an opened quoted string and no
// closing one
TEST_F(MasterLexerTest, getUnbalancedString) {
    ss << "\"string";
    lexer.pushSource(ss);

    // Then an unbalanced qstring (reported as an unexpected end)
    EXPECT_EQ(MasterToken::UNEXPECTED_END,
              lexer.getNextToken(MasterLexer::QSTRING).getErrorCode());
    // And then EOF
    EXPECT_EQ(MasterToken::END_OF_FILE, lexer.getNextToken().getType());
}

// Test ungetting tokens works.  Also check getPosition() is adjusted
TEST_F(MasterLexerTest, ungetToken) {
    ss << "\n (\"string\"\n) more";
    lexer.pushSource(ss);

    // Try getting the newline
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    EXPECT_EQ(1, lexer.getPosition());
    // Return it and get again
    lexer.ungetToken();
    EXPECT_EQ(0, lexer.getPosition());
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    EXPECT_EQ(1, lexer.getPosition());
    // Get the string and return it back
    EXPECT_EQ(MasterToken::QSTRING,
              lexer.getNextToken(MasterLexer::QSTRING).getType());
    EXPECT_EQ(string("\n (\"string\"").size(), lexer.getPosition());
    lexer.ungetToken();
    EXPECT_EQ(1, lexer.getPosition()); // back to just after 1st \n
    // But if we change the options, it honors them
    EXPECT_EQ(MasterToken::INITIAL_WS,
              lexer.getNextToken(MasterLexer::QSTRING |
                                 MasterLexer::INITIAL_WS).getType());
    // Get to the "more" string
    EXPECT_EQ(MasterToken::QSTRING,
              lexer.getNextToken(MasterLexer::QSTRING).getType());
    EXPECT_EQ(MasterToken::STRING,
              lexer.getNextToken(MasterLexer::QSTRING).getType());
    // Return it back. It should get inside the parentheses.
    // Upon next attempt to get it again, the newline inside the parentheses
    // should be still ignored.
    lexer.ungetToken();
    EXPECT_EQ(MasterToken::STRING,
              lexer.getNextToken(MasterLexer::QSTRING).getType());
}

// Check ungetting token without overriding the start method. We also
// check it works well with changing options between the calls.
TEST_F(MasterLexerTest, ungetRealOptions) {
    ss << "    \n";
    lexer.pushSource(ss);

    // If we call it the usual way, it skips up to the newline and returns
    // it
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());

    // Now we return it. If we call it again, but with different options,
    // we get the initial whitespace.
    lexer.ungetToken();
    EXPECT_EQ(MasterToken::INITIAL_WS,
              lexer.getNextToken(MasterLexer::INITIAL_WS).getType());
}

// Check the initial whitespace is found even in the first line of included
// file.  It also confirms getPosition() works for multiple sources, each
// of which is partially parsed.
TEST_F(MasterLexerTest, includeAndInitialWS) {
    ss << "    \n";
    lexer.pushSource(ss);

    stringstream ss2;
    ss2 << "    \n";

    EXPECT_EQ(MasterToken::INITIAL_WS,
              lexer.getNextToken(MasterLexer::INITIAL_WS).getType());
    EXPECT_EQ(1, lexer.getPosition());
    lexer.pushSource(ss2);
    EXPECT_EQ(MasterToken::INITIAL_WS,
              lexer.getNextToken(MasterLexer::INITIAL_WS).getType());
    EXPECT_EQ(2, lexer.getPosition()); // should be sum of pushed positions.
}

// Test only one token can be ungotten
TEST_F(MasterLexerTest, ungetTwice) {
    ss << "\n";
    lexer.pushSource(ss);

    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    // Unget the token. It can be done once
    lexer.ungetToken();
    // But not twice
    EXPECT_THROW(lexer.ungetToken(), isc::InvalidOperation);
}

// Test we can't unget a token before we get one
TEST_F(MasterLexerTest, ungetBeforeGet) {
    lexer.pushSource(ss); // Just to eliminate the missing source problem
    EXPECT_THROW(lexer.ungetToken(), isc::InvalidOperation);
}

// Test we can't unget a token after a source switch, even when we got
// something before.
TEST_F(MasterLexerTest, ungetAfterSwitch) {
    ss << "\n\n";
    lexer.pushSource(ss);
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    // Switch the source
    std::stringstream ss2;
    ss2 << "\n\n";
    lexer.pushSource(ss2);
    EXPECT_THROW(lexer.ungetToken(), isc::InvalidOperation);
    // We can get from the new source
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
    // And when we drop the current source, we can't unget again
    lexer.popSource();
    EXPECT_THROW(lexer.ungetToken(), isc::InvalidOperation);
}

// Common checks for the case when getNextToken() should result in LexerError
void
lexerErrorCheck(MasterLexer& lexer, MasterToken::Type expect,
                MasterToken::ErrorCode expected_error)
{
    bool thrown = false;
    try {
        lexer.getNextToken(expect);
    } catch (const MasterLexer::LexerError& error) {
        EXPECT_EQ(expected_error, error.token_.getErrorCode());
        thrown = true;
    }
    EXPECT_TRUE(thrown);
}

// Common checks regarding expected/unexpected end-of-line
//
// The 'lexer' should be at a position before two consecutive '\n's.
// The first one will be recognized, and the second one will be considered an
// unexpected token.  Then this helper consumes the second '\n', so the caller
// can continue the test after these '\n's.
void
eolCheck(MasterLexer& lexer, MasterToken::Type expect) {
    // If EOL is found and eol_ok is true, we get it.
    EXPECT_EQ(MasterToken::END_OF_LINE,
              lexer.getNextToken(expect, true).getType());
    // We'll see the second '\n'; by default it will fail.
    EXPECT_THROW(lexer.getNextToken(expect), MasterLexer::LexerError);
    // Same if eol_ok is explicitly set to false.  This also checks the
    // offending '\n' was "ungotten".
    EXPECT_THROW(lexer.getNextToken(expect, false), MasterLexer::LexerError);

    // And also check the error token set in the exception object.
    lexerErrorCheck(lexer, expect, MasterToken::UNEXPECTED_END);

    // Then skip the 2nd '\n'
    EXPECT_EQ(MasterToken::END_OF_LINE, lexer.getNextToken().getType());
}

// Common checks regarding expected/unexpected end-of-file
//
// The 'lexer' should be at a position just before an end-of-file.
void
eofCheck(MasterLexer& lexer, MasterToken::Type expect) {
    EXPECT_EQ(MasterToken::END_OF_FILE,
              lexer.getNextToken(expect, true).getType());
    EXPECT_THROW(lexer.getNextToken(expect), MasterLexer::LexerError);
    EXPECT_THROW(lexer.getNextToken(expect, false), MasterLexer::LexerError);
}

TEST_F(MasterLexerTest, getNextTokenString) {
    ss << "normal-string\n";
    ss << "\n";
    ss << "another-string";
    lexer.pushSource(ss);

    // Normal successful case: Expecting a string and get one.
    EXPECT_EQ("normal-string",
              lexer.getNextToken(MasterToken::STRING).getString());
    eolCheck(lexer, MasterToken::STRING);

    // Same set of tests but for end-of-file
    EXPECT_EQ("another-string",
              lexer.getNextToken(MasterToken::STRING, true).getString());
    eofCheck(lexer, MasterToken::STRING);
}

TEST_F(MasterLexerTest, getNextTokenQString) {
    ss << "\"quoted-string\"\n";
    ss << "\n";
    ss << "normal-string";
    lexer.pushSource(ss);

    // Expecting a quoted string and get one.
    EXPECT_EQ("quoted-string",
              lexer.getNextToken(MasterToken::QSTRING).getString());
    eolCheck(lexer, MasterToken::QSTRING);

    // Expecting a quoted string but see a normal string.  It's okay.
    EXPECT_EQ("normal-string",
              lexer.getNextToken(MasterToken::QSTRING).getString());
    eofCheck(lexer, MasterToken::QSTRING);
}

TEST_F(MasterLexerTest, getNextTokenNumber) {
    ss << "3600\n";
    ss << "\n";
    ss << "4294967296 ";        // =2^32, out of range
    ss << "not-a-number ";
    ss << "123abc "; // starting with digits, but resulting in a string
    ss << "86400";
    lexer.pushSource(ss);

    // Expecting a number string and get one.
    EXPECT_EQ(3600,
              lexer.getNextToken(MasterToken::NUMBER).getNumber());
    eolCheck(lexer, MasterToken::NUMBER);

    // Expecting a number, but it's too big for uint32.
    lexerErrorCheck(lexer, MasterToken::NUMBER,
                    MasterToken::NUMBER_OUT_OF_RANGE);
    // The token should have been "ungotten".  Re-read and skip it.
    EXPECT_EQ(MasterToken::STRING, lexer.getNextToken().getType());

    // Expecting a number, but see a string.
    lexerErrorCheck(lexer, MasterToken::NUMBER, MasterToken::BAD_NUMBER);
    // The unexpected string should have been "ungotten".  Re-read and skip it.
    EXPECT_EQ(MasterToken::STRING, lexer.getNextToken().getType());

    // Expecting a number, but see a string.
    lexerErrorCheck(lexer, MasterToken::NUMBER, MasterToken::BAD_NUMBER);
    // The unexpected string should have been "ungotten".  Re-read and skip it.
    EXPECT_EQ(MasterToken::STRING, lexer.getNextToken().getType());

    // Unless we specify NUMBER, decimal number string should be recognized
    // as a string.
    EXPECT_EQ("86400",
              lexer.getNextToken(MasterToken::STRING).getString());
    eofCheck(lexer, MasterToken::NUMBER);
}

TEST_F(MasterLexerTest, getNextTokenErrors) {
    // Check miscellaneous error cases

    ss << ") ";                 // unbalanced parenthesis
    ss << "string-after-error ";
    lexer.pushSource(ss);

    // Only string/qstring/number can be "expected".
    EXPECT_THROW(lexer.getNextToken(MasterToken::END_OF_LINE),
                 isc::InvalidParameter);
    EXPECT_THROW(lexer.getNextToken(MasterToken::END_OF_FILE),
                 isc::InvalidParameter);
    EXPECT_THROW(lexer.getNextToken(MasterToken::INITIAL_WS),
                 isc::InvalidParameter);
    EXPECT_THROW(lexer.getNextToken(MasterToken::ERROR),
                 isc::InvalidParameter);

    // If it encounters a syntax error, it results in LexerError exception.
    lexerErrorCheck(lexer, MasterToken::STRING, MasterToken::UNBALANCED_PAREN);

    // Unlike the NUMBER_OUT_OF_RANGE case, the error part has been skipped
    // within getNextToken().  We should be able to get the next token.
    EXPECT_EQ("string-after-error",
              lexer.getNextToken(MasterToken::STRING).getString());
}

}
