/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "../logunit.h"
#include <log4cxx/logger.h>
#include <log4cxx/xml/xmllayout.h>
#include <log4cxx/htmllayout.h>
#include <log4cxx/fileappender.h>
#include <log4cxx/mdc.h>

#include "../util/transformer.h"
#include "../util/compare.h"
#include "../util/xmltimestampfilter.h"
#include "../util/xmllineattributefilter.h"
#include "../util/xmlthreadfilter.h"
#include "../util/filenamefilter.h"
#include <iostream>
#include <log4cxx/helpers/stringhelper.h>
#include "../testchar.h"
#include <log4cxx/spi/loggerrepository.h>
#include <apr_xml.h>
#include <log4cxx/ndc.h>
#include <log4cxx/mdc.h>
#include "../xml/xlevel.h"
#include <log4cxx/helpers/bytebuffer.h>
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/helpers/loglog.h>

using namespace log4cxx;
using namespace log4cxx::helpers;
using namespace log4cxx::xml;
using namespace log4cxx::spi;

#if defined(__LOG4CXX_FUNC__)
	#undef __LOG4CXX_FUNC__
	#define __LOG4CXX_FUNC__ "X::X()"
#else
	#error __LOG4CXX_FUNC__ expected to be defined
#endif
/**
 * Test for XMLLayout.
 *
 */
LOGUNIT_CLASS(XMLLayoutTest)
{
	LOGUNIT_TEST_SUITE(XMLLayoutTest);
	LOGUNIT_TEST(testGetContentType);
	LOGUNIT_TEST(testIgnoresThrowable);
	LOGUNIT_TEST(testGetHeader);
	LOGUNIT_TEST(testGetFooter);
	LOGUNIT_TEST(testFormat);
	LOGUNIT_TEST(testFormatWithNDC);
	LOGUNIT_TEST(testGetSetLocationInfo);
	LOGUNIT_TEST(testActivateOptions);
	LOGUNIT_TEST(testProblemCharacters);
	LOGUNIT_TEST(testNDCWithCDATA);
	LOGUNIT_TEST(testHTMLLayout);
	LOGUNIT_TEST_SUITE_END();


public:
	/**
	 * Clear MDC and NDC before test.
	 */
	void setUp()
	{
		NDC::clear();
		MDC::clear();
	}

	/**
	 * Clear MDC and NDC after test.
	 */
	void tearDown()
	{
		setUp();
	}


public:
	/**
	 * Tests getContentType.
	 */
	void testGetContentType()
	{
		LogString expected(LOG4CXX_STR("text/plain"));
		LogString actual(XMLLayout().getContentType());
		LOGUNIT_ASSERT(expected == actual);
	}

	/**
	 * Tests ignoresThrowable.
	 */
	void testIgnoresThrowable()
	{
		LOGUNIT_ASSERT_EQUAL(false, XMLLayout().ignoresThrowable());
	}

	/**
	 * Tests getHeader.
	 */
	void testGetHeader()
	{
		LogString header;
		XMLLayout().appendHeader(header);
		LOGUNIT_ASSERT_EQUAL((size_t) 0, header.size());
	}

	/**
	 * Tests getFooter.
	 */
	void testGetFooter()
	{
		LogString footer;
		XMLLayout().appendFooter(footer);
		LOGUNIT_ASSERT_EQUAL((size_t) 0, footer.size());
	}

private:
	/**
	 * Parses the string as the body of an XML document and returns the document element.
	 * @param source source string.
	 * @return document element.
	 * @throws Exception if parser can not be constructed or source is not a valid XML document.
	 */
	static apr_xml_elem* parse(const LogString & source, Pool & p)
	{
		char backing[3000];
		ByteBuffer buf(backing, sizeof(backing));
		CharsetEncoderPtr encoder(CharsetEncoder::getUTF8Encoder());
		LogString header(LOG4CXX_STR("<log4j:eventSet xmlns:log4j='http://jakarta.apache.org/log4j/'>"));
		LogString::const_iterator iter(header.begin());
		encoder->encode(header, iter, buf);
		LOGUNIT_ASSERT(iter == header.end());
		iter = source.begin();
		encoder->encode(source, iter, buf);
		LOGUNIT_ASSERT(iter == source.end());
		LogString footer(LOG4CXX_STR("</log4j:eventSet>"));
		iter = footer.begin();
		encoder->encode(footer, iter, buf);
		buf.flip();
		apr_pool_t* apr_pool = p.getAPRPool();
		apr_xml_parser* parser = apr_xml_parser_create(apr_pool);
		LOGUNIT_ASSERT(parser != 0);
		apr_status_t stat = apr_xml_parser_feed(parser, buf.data(), buf.remaining());
		LOGUNIT_ASSERT(stat == APR_SUCCESS);
		apr_xml_doc* doc = 0;
		stat = apr_xml_parser_done(parser, &doc);
		LOGUNIT_ASSERT(doc != 0);
		apr_xml_elem* eventSet = doc->root;
		LOGUNIT_ASSERT(eventSet != 0);
		apr_xml_elem* event = eventSet->first_child;
		LOGUNIT_ASSERT(event != 0);
		return event;
	}

	std::string getAttribute(apr_xml_elem * elem, const char* attrName)
	{
		for (apr_xml_attr* attr = elem->attr;
			attr != NULL;
			attr = attr->next)
		{
			if (strcmp(attr->name, attrName) == 0)
			{
				return attr->value;
			}
		}

		return "";
	}

	std::string getText(apr_xml_elem * elem)
	{
		std::string dMessage;

		for (apr_text* t = elem->first_cdata.first;
			t != NULL;
			t = t->next)
		{
			dMessage.append(t->text);
		}

		return dMessage;
	}
	/**
	 * Checks a log4j:event element against expectations.
	 * @param element element, may not be null.
	 * @param event event, may not be null.
	 */
	void checkEventElement(
		apr_xml_elem * element, LoggingEventPtr & event)
	{
		std::string tagName("event");
		LOGUNIT_ASSERT_EQUAL(tagName, (std::string) element->name);
		LOG4CXX_ENCODE_CHAR(cLoggerName, event->getLoggerName());
		LOGUNIT_ASSERT_EQUAL(cLoggerName, getAttribute(element, "logger"));
		LOG4CXX_ENCODE_CHAR(cLevelName, event->getLevel()->toString());
		LOGUNIT_ASSERT_EQUAL(cLevelName, getAttribute(element, "level"));
	}

	/**
	 * Checks a log4j:message element against expectations.
	 * @param element element, may not be null.
	 * @param message expected message.
	 */
	void checkMessageElement(
		apr_xml_elem * element, std::string message)
	{
		std::string tagName = "message";
		LOGUNIT_ASSERT_EQUAL(tagName, (std::string) element->name);
		LOGUNIT_ASSERT_EQUAL(message, getText(element));
	}

	/**
	 * Checks a log4j:message element against expectations.
	 * @param element element, may not be null.
	 * @param message expected message.
	 */
	void checkNDCElement(apr_xml_elem * element, std::string message)
	{
		std::string tagName = "NDC";
		LOGUNIT_ASSERT_EQUAL(tagName, (std::string) element->name);
		std::string dMessage = getText(element);
		LOGUNIT_ASSERT_EQUAL(message, dMessage);
	}


	/**
	 * Checks a log4j:properties element against expectations.
	 * @param element element, may not be null.
	 * @param key key.
	 * @param value value.
	 */
	void checkPropertiesElement(
		apr_xml_elem * element, std::string key, std::string value)
	{
		std::string tagName = "properties";
		std::string dataTag = "data";
		int childNodeCount = 0;
		LOGUNIT_ASSERT_EQUAL(tagName, (std::string) element->name);

		for (apr_xml_elem* child = element->first_child;
			child != NULL;
			child = child->next)
		{
			LOGUNIT_ASSERT_EQUAL(dataTag, (std::string) child->name);
			LOGUNIT_ASSERT_EQUAL(key, getAttribute(child, "name"));
			LOGUNIT_ASSERT_EQUAL(value, getAttribute(child, "value"));
			childNodeCount++;
		}

		LOGUNIT_ASSERT_EQUAL(1, childNodeCount);
	}

public:
	/**
	 * Tests formatted results.
	 * @throws Exception if parser can not be constructed or source is not a valid XML document.
	 */
	void testFormat()
	{
		LogString logger = LOG4CXX_STR("org.apache.log4j.xml.XMLLayoutTest");
		LoggingEventPtr event = LoggingEventPtr(
				new LoggingEvent(
					logger, Level::getInfo(), LOG4CXX_STR("Hello, World"), LOG4CXX_LOCATION));
		Pool p;
		XMLLayout layout;
		LogString result;
		layout.format(result, event);
		apr_xml_elem* parsedResult = parse(result, p);
		checkEventElement(parsedResult, event);

		int childElementCount = 0;

		for (
			apr_xml_elem* node = parsedResult->first_child;
			node != NULL;
			node = node->next)
		{
			childElementCount++;
			checkMessageElement(node, "Hello, World");
		}

		LOGUNIT_ASSERT_EQUAL(1, childElementCount);
	}


	/**
	 * Tests formatted results with an exception.
	 * @throws Exception if parser can not be constructed or source is not a valid XML document.
	 */
	void testFormatWithNDC()
	{
		LogString logger = LOG4CXX_STR("org.apache.log4j.xml.XMLLayoutTest");
		NDC::push("\001NDC goes here\004");

		LoggingEventPtr event = LoggingEventPtr(
				new LoggingEvent(
					logger, Level::getInfo(), LOG4CXX_STR("Hello, World"), LOG4CXX_LOCATION));
		Pool p;
		XMLLayout layout;
		LogString result;
		layout.format(result, event);
		NDC::pop();

		apr_xml_elem* parsedResult = parse(result, p);
		checkEventElement(parsedResult, event);

		int childElementCount = 0;

		for (
			apr_xml_elem* node = parsedResult->first_child; node != NULL;
			node = node->next)
		{
			childElementCount++;

			if (childElementCount == 1)
			{
				checkMessageElement(node, "Hello, World");
			}
			else
			{
				checkNDCElement(node, "&#x1;NDC goes here&#x4;");
			}
		}

		LOGUNIT_ASSERT_EQUAL(2, childElementCount);
	}

	/**
	 * Tests getLocationInfo and setLocationInfo.
	 */
	void testGetSetLocationInfo()
	{
		XMLLayout layout;
		LOGUNIT_ASSERT_EQUAL(false, layout.getLocationInfo());
		layout.setLocationInfo(true);
		LOGUNIT_ASSERT_EQUAL(true, layout.getLocationInfo());
		layout.setLocationInfo(false);
		LOGUNIT_ASSERT_EQUAL(false, layout.getLocationInfo());
	}

	/**
	 * Tests activateOptions().
	 */
	void testActivateOptions()
	{
		XMLLayout layout;
		layout.activateOptions();
	}

	/**
	 * Tests problematic characters in multiple fields.
	 * @throws Exception if parser can not be constructed or source is not a valid XML document.
	 */
	void testProblemCharacters()
	{
		// '\"<räksmörgås.josefsson.org>&\"'
		std::string problemName = "'\"<\162\303\244\153\163\155\303\266\162\147\303\245\163\056\152\157\163\145\146\163\163\157\156\056\157\162\147>&\"'";
		LOG4CXX_DECODE_CHAR(problemNameLS, problemName);
		auto loggerNameLS = problemNameLS;
		auto levelNameLS = problemNameLS;
		Transcoder::encode(0xD822, problemNameLS); // Add an invalid character that should be stripped from attribute values
		auto keyLS = problemNameLS;
		auto expectedKeyValue = problemName;
		// UTF-8 encodes the Unicode replacement character (0xFFFD) as the following:
		expectedKeyValue += "\xEF\xBF\xBD";
		std::string problemMessage = "'\001\"<Hello >\"\004'";
		std::string expectedCdataValue = "'&#x1;\"<Hello >\"&#x4;'";
		std::string expectedAttributeValue = "'\"<Hello >\"'"; // Invalid characters stripped
		LOG4CXX_DECODE_CHAR(problemMessageLS, problemMessage);
		LevelPtr level = LevelPtr(new XLevel(6000, levelNameLS, 7));
		NDC::push(problemName);
		MDC::clear();
		MDC::put(keyLS, problemMessageLS);
		auto event = std::make_shared<LoggingEvent>
				(loggerNameLS, level, problemMessageLS, LOG4CXX_LOCATION);
		XMLLayout layout;
		layout.setProperties(true);
		Pool p;
		LogString result;
		layout.format(result, event);
		MDC::clear();

		apr_xml_elem* parsedResult = parse(result, p);
		checkEventElement(parsedResult, event);

		int childElementCount = 0;

		for (
			apr_xml_elem* node = parsedResult->first_child; node != NULL;
			node = node->next)
		{
			childElementCount++;

			switch (childElementCount)
			{
				case 1:
					checkMessageElement(node, expectedCdataValue);
					break;

				case 2:
					checkNDCElement(node, problemName);
					break;

				case 3:
					checkPropertiesElement(node, expectedKeyValue.c_str(), expectedAttributeValue.c_str());
					break;

				default:
					break;
			}

		}

		LOGUNIT_ASSERT_EQUAL(3, childElementCount);
	}

	/**
	  * Tests CDATA element within NDC content.  See bug 37560.
	  */
	void testNDCWithCDATA()
	{
		LogString logger = LOG4CXX_STR("com.example.bar");
		LevelPtr level = Level::getInfo();
		std::string ndcMessage = "<envelope><faultstring><![CDATA[The EffectiveDate]]></faultstring><envelope>";
		NDC::push(ndcMessage);
		LoggingEventPtr event = LoggingEventPtr(
				new LoggingEvent(
					logger, level, LOG4CXX_STR("Hello, World"), LOG4CXX_LOCATION));
		XMLLayout layout;
		Pool p;
		LogString result;
		layout.format(result, event);
		NDC::clear();
		apr_xml_elem* parsedResult = parse(result, p);
		int ndcCount = 0;

		for (apr_xml_elem* node = parsedResult->first_child;
			node != NULL;
			node = node->next)
		{
			if (strcmp(node->name, "NDC") == 0)
			{
				ndcCount++;
				LOGUNIT_ASSERT_EQUAL(ndcMessage, getText(node));
			}
		}

		LOGUNIT_ASSERT_EQUAL(1, ndcCount);
	}

	/**
	 * Tests problematic characters in multiple fields.
	 * @throws Exception if parser can not be constructed or source is not a valid XML document.
	 */
	void testHTMLLayout()
	{
		LogString problemName = LOG4CXX_STR("com.example.bar<>&\"'\n\t");
		auto level = std::make_shared<XLevel>(6000, problemName, 7);
		NDC context(problemName);
		auto event = std::make_shared<LoggingEvent>(problemName, level, problemName, LOG4CXX_LOCATION);

		HTMLLayout layout;
		LogString html(LOG4CXX_STR("<body>"));
		layout.format(html, event);
		html += LOG4CXX_STR("</body>");

		LogLog::debug(html);
		char backing[3000];
		ByteBuffer buf(backing, sizeof(backing));
		CharsetEncoderPtr encoder(CharsetEncoder::getUTF8Encoder());
		LogString::const_iterator iter{ html.begin() };
		encoder->encode(html, iter, buf);
		LOGUNIT_ASSERT(iter == html.end());
		buf.flip();

		LOGUNIT_ASSERT(html.find(LOG4CXX_STR(
			"title=\"com.example.bar&lt;&gt;&amp;&quot;&#x27;&#xa;&#x9; logger\"")) != LogString::npos);

		Pool p;
		auto parser = apr_xml_parser_create(p.getAPRPool());
		LOGUNIT_ASSERT(parser != 0);
		auto stat = apr_xml_parser_feed(parser, buf.data(), buf.remaining());
		LOGUNIT_ASSERT(stat == APR_SUCCESS);
		apr_xml_doc* doc = 0;
		stat = apr_xml_parser_done(parser, &doc);
		LOGUNIT_ASSERT(doc != 0);
		auto parsedResult = doc->root;
		LOGUNIT_ASSERT(parsedResult != 0);

		int childElementCount = 0;
		for ( auto node = parsedResult->first_child
		    ; node != NULL
		    ; node = node->next)
		{
			++childElementCount;
			LOGUNIT_ASSERT_EQUAL(std::string("tr"), std::string(node->name));
		}
		LOGUNIT_ASSERT(1 < childElementCount);
	}

};


LOGUNIT_TEST_SUITE_REGISTRATION(XMLLayoutTest);

