/*
 * 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.
 */
package org.apache.commons.configuration2;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.Deque;
import java.util.Iterator;

import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.convert.LegacyListDelimiterHandler;
import org.apache.commons.configuration2.event.ConfigurationEvent;
import org.apache.commons.configuration2.event.EventListener;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.junit.Before;
import org.junit.Test;

/**
 * Test class for PropertiesConfigurationLayout.
 */
public class TestPropertiesConfigurationLayout
{
    /** Constant for the line break character. */
    private static final String CR = System.getProperty("line.separator");

    /** Constant for the normalized line break character. */
    private static final String CRNORM = "\n";

    /** Constant for a test property key. */
    private static final String TEST_KEY = "myProperty";

    /** Constant for a test comment. */
    private static final String TEST_COMMENT = "A comment for my test property";

    /** Constant for a test property value. */
    private static final String TEST_VALUE = "myPropertyValue";

    /** The layout object under test. */
    private PropertiesConfigurationLayout layout;

    /** The associated configuration object. */
    private LayoutTestConfiguration config;

    /** A properties builder that can be used for testing. */
    private PropertiesBuilder builder;

    @Before
    public void setUp() throws Exception
    {
        config = new LayoutTestConfiguration();
        config.setListDelimiterHandler(new LegacyListDelimiterHandler(','));
        layout = new PropertiesConfigurationLayout();
        config.setLayout(layout);
        builder = new PropertiesBuilder();
    }

    /**
     * Tests a newly created instance.
     */
    @Test
    public void testInit()
    {
        assertTrue("Object contains keys", layout.getKeys().isEmpty());
        assertNull("Header comment not null", layout.getHeaderComment());
        final Iterator<EventListener<? super ConfigurationEvent>> it =
                config.getEventListeners(ConfigurationEvent.ANY).iterator();
        assertTrue("No event listener registered", it.hasNext());
        assertSame("Layout not registered as event listener", layout, it.next());
        assertFalse("Multiple event listeners registered", it.hasNext());
        assertFalse("Force single line flag set", layout.isForceSingleLine());
        assertNull("Got a global separator", layout.getGlobalSeparator());
    }

    /**
     * Tests the copy constructor if no other layout object is passed.
     */
    @Test
    public void testInitNull()
    {
        layout = new PropertiesConfigurationLayout(null);
        assertTrue("Object contains keys", layout.getKeys().isEmpty());
    }

    /**
     * Tests reading a simple properties file.
     */
    @Test
    public void testReadSimple() throws ConfigurationException
    {
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        assertNull("A header comment was found", layout.getHeaderComment());
        assertEquals("Wrong number of properties", 1, layout.getKeys().size());
        assertTrue("Property not found", layout.getKeys().contains(TEST_KEY));
        assertEquals("Comment not found", TEST_COMMENT, layout
                .getCanonicalComment(TEST_KEY, false));
        assertEquals("Wrong number of blanc lines", 0, layout
                .getBlancLinesBefore(TEST_KEY));
        assertTrue("Wrong single line flag", layout.isSingleLine(TEST_KEY));
        assertEquals("Property not stored in config", TEST_VALUE, config
                .getString(TEST_KEY));
    }

    /**
     * Tests whether blanc lines before a property are correctly detected.
     */
    @Test
    public void testBlancLines() throws ConfigurationException
    {
        builder.addProperty("prop", "value");
        builder.addComment(null);
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        assertEquals("Wrong number of blanc lines", 2, layout
                .getBlancLinesBefore(TEST_KEY));
        assertEquals("Wrong comment", TEST_COMMENT + CRNORM, layout
                .getCanonicalComment(TEST_KEY, false));
        assertEquals("Wrong property value", TEST_VALUE, config
                .getString(TEST_KEY));
    }

    /**
     * Tests the single line flag for a simple property definition.
     */
    @Test
    public void testIsSingleLine() throws ConfigurationException
    {
        builder.addProperty(TEST_KEY, TEST_VALUE + "," + TEST_VALUE + "2");
        layout.load(config, builder.getReader());
        assertTrue("Wrong single line flag", layout.isSingleLine(TEST_KEY));
        assertEquals("Wrong number of values", 2, config.getList(TEST_KEY)
                .size());
    }

    /**
     * Tests the single line flag if there are multiple property definitions.
     */
    @Test
    public void testIsSingleLineMulti() throws ConfigurationException
    {
        builder.addProperty(TEST_KEY, TEST_VALUE);
        builder.addProperty("anotherProp", "a value");
        builder.addProperty(TEST_KEY, TEST_VALUE + "2");
        layout.load(config, builder.getReader());
        assertFalse("Wrong single line flag", layout.isSingleLine(TEST_KEY));
        assertEquals("Wrong number of values", 2, config.getList(TEST_KEY)
                .size());
    }

    /**
     * Tests whether comments are combined for multiple occurrences.
     */
    @Test
    public void testCombineComments() throws ConfigurationException
    {
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE + "2");
        layout.load(config, builder.getReader());
        assertEquals("Wrong combined comment",
                TEST_COMMENT + CRNORM + TEST_COMMENT, layout.getCanonicalComment(
                        TEST_KEY, false));
        assertEquals("Wrong combined blanc numbers", 0, layout
                .getBlancLinesBefore(TEST_KEY));
    }

    /**
     * Tests if a header comment is detected.
     */
    @Test
    public void testHeaderComment() throws ConfigurationException
    {
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        assertEquals("Wrong header comment", TEST_COMMENT, layout
                .getCanonicalHeaderComment(false));
        assertNull("Wrong comment for property", layout.getCanonicalComment(
                TEST_KEY, false));
    }

    /**
     * Tests if a header comment containing blanc lines is correctly detected.
     */
    @Test
    public void testHeaderCommentWithBlancs() throws ConfigurationException
    {
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        assertEquals("Wrong header comment", TEST_COMMENT + CRNORM + CRNORM
                + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
        assertNull("Wrong comment for property", layout.getComment(TEST_KEY));
    }

    /**
     * Tests if a header comment containing blanc lines is correctly detected and doesn't overflow into the property
     * comment in the case that the header comment is already set
     */
    @Test
    public void testHeaderCommentWithBlancsAndPresetHeaderComment() throws ConfigurationException
    {
        final String presetHeaderComment = "preset" + TEST_COMMENT + CRNORM + CRNORM + TEST_COMMENT;
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.setHeaderComment(presetHeaderComment);
        layout.load(config, builder.getReader());
        assertEquals("Wrong header comment", presetHeaderComment,
                     layout.getCanonicalHeaderComment(false));
        assertNull("Wrong comment for property", layout.getComment(TEST_KEY));
    }

    /**
     * Tests if a header comment is correctly detected when it contains blanc
     * lines and the first property has a comment, too.
     */
    @Test
    public void testHeaderCommentWithBlancsAndPropComment()
            throws ConfigurationException
    {
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        assertEquals("Wrong header comment", TEST_COMMENT + CRNORM + CRNORM
                + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
        assertEquals("Wrong comment for property", TEST_COMMENT, layout
                .getCanonicalComment(TEST_KEY, false));
    }

    /**
     * Tests fetching a canonical header comment when no comment is set.
     */
    @Test
    public void testHeaderCommentNull()
    {
        assertNull("No null comment with comment chars", layout
                .getCanonicalHeaderComment(true));
        assertNull("No null comment without comment chars", layout
                .getCanonicalHeaderComment(false));
    }

    /**
     * Tests if a property add event is correctly processed.
     */
    @Test
    public void testEventAdd()
    {
        final ConfigurationEvent event = new ConfigurationEvent(this,
                ConfigurationEvent.ADD_PROPERTY, TEST_KEY, TEST_VALUE,
                false);
        layout.onEvent(event);
        assertTrue("Property not stored", layout.getKeys().contains(TEST_KEY));
        assertEquals("Blanc lines before new property", 0, layout
                .getBlancLinesBefore(TEST_KEY));
        assertTrue("No single line property", layout.isSingleLine(TEST_KEY));
        assertEquals("Wrong separator", " = ", layout.getSeparator(TEST_KEY));
    }

    /**
     * Tests adding a property multiple time through an event. The property
     * should then be a multi-line property.
     */
    @Test
    public void testEventAddMultiple()
    {
        final ConfigurationEvent event = new ConfigurationEvent(this,
                ConfigurationEvent.ADD_PROPERTY, TEST_KEY, TEST_VALUE,
                false);
        layout.onEvent(event);
        layout.onEvent(event);
        assertFalse("No multi-line property", layout.isSingleLine(TEST_KEY));
    }

    /**
     * Tests if an add event is correctly processed if the affected property is
     * already stored in the layout object.
     */
    @Test
    public void testEventAddExisting() throws ConfigurationException
    {
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        final ConfigurationEvent event = new ConfigurationEvent(this,
                ConfigurationEvent.ADD_PROPERTY, TEST_KEY, TEST_VALUE,
                false);
        layout.onEvent(event);
        assertFalse("No multi-line property", layout.isSingleLine(TEST_KEY));
        assertEquals("Comment was modified", TEST_COMMENT, layout
                .getCanonicalComment(TEST_KEY, false));
    }

    /**
     * Tests if a set property event for a non existing property is correctly
     * handled.
     */
    @Test
    public void testEventSetNonExisting()
    {
        final ConfigurationEvent event = new ConfigurationEvent(this,
                ConfigurationEvent.SET_PROPERTY, TEST_KEY, TEST_VALUE,
                false);
        layout.onEvent(event);
        assertTrue("New property was not found", layout.getKeys().contains(
                TEST_KEY));
    }

    /**
     * Tests if a property delete event is correctly processed.
     */
    @Test
    public void testEventDelete()
    {
        ConfigurationEvent event = new ConfigurationEvent(this,
                ConfigurationEvent.ADD_PROPERTY, TEST_KEY, TEST_VALUE,
                false);
        layout.onEvent(event);
        event = new ConfigurationEvent(this,
                ConfigurationEvent.CLEAR_PROPERTY, TEST_KEY, null,
                false);
        layout.onEvent(event);
        assertFalse("Property still existing", layout.getKeys().contains(
                TEST_KEY));
    }

    /**
     * Tests if a clear event is correctly processed.
     */
    @Test
    public void testEventClearConfig() throws Exception
    {
        fillLayout();
        final ConfigurationEvent event = new ConfigurationEvent(this,
                ConfigurationEvent.CLEAR, null, null, false);
        layout.onEvent(event);
        assertTrue("Keys not empty", layout.getKeys().isEmpty());
        assertNull("Header comment was not reset", layout.getHeaderComment());
    }

    /**
     * Tests if a before update event is correctly ignored.
     */
    @Test
    public void testEventAddBefore()
    {
        final ConfigurationEvent event = new ConfigurationEvent(this,
                ConfigurationEvent.ADD_PROPERTY, TEST_KEY, TEST_VALUE,
                true);
        layout.onEvent(event);
        assertFalse("Property already stored", layout.getKeys().contains(
                TEST_KEY));
    }

    /**
     * Tests a recursive load call.
     */
    @Test
    public void testRecursiveLoadCall() throws ConfigurationException
    {
        final PropertiesBuilder b = new PropertiesBuilder();
        b.addComment("A nested header comment.");
        b.addComment("With multiple lines");
        b.addComment(null);
        b.addComment("Second comment");
        b.addProperty(TEST_KEY, TEST_VALUE);
        b.addProperty(TEST_KEY + "2", TEST_VALUE + "2");
        config.builder = b;

        builder.addComment("Header comment");
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        builder.addComment("Include file");
        builder.addProperty(PropertiesConfiguration.getInclude(), "test");

        layout.load(config, builder.getReader());

        assertEquals("Wrong header comment", "Header comment", layout
                .getCanonicalHeaderComment(false));
        assertFalse("Include property was stored", layout.getKeys().contains(
                PropertiesConfiguration.getInclude()));
        assertEquals("Wrong comment for property", TEST_COMMENT + CRNORM
                + "A nested header comment." + CRNORM + "With multiple lines" + CRNORM
                + CRNORM + "Second comment", layout.getCanonicalComment(TEST_KEY,
                false));
    }

    /**
     * Tests whether the output of the layout object is identical to the source
     * file (at least for simple properties files).
     */
    @Test
    public void testReadAndWrite() throws ConfigurationException
    {
        builder.addComment("This is my test properties file,");
        builder.addComment("which contains a header comment.");
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_COMMENT);
        builder.addComment(null);
        builder.addComment(null);
        builder.addComment("Another comment");
        builder.addProperty("property", "and a value");
        layout.load(config, builder.getReader());
        checkLayoutString(builder.toString());
    }

    /**
     * Tests if the content of the layout object is correctly written.
     */
    @Test
    public void testSave() throws ConfigurationException
    {
        config.addProperty(TEST_KEY, TEST_VALUE);
        layout.setComment(TEST_KEY, TEST_COMMENT);
        config.addProperty(TEST_KEY, TEST_VALUE + "2");
        config.addProperty("AnotherProperty", "AnotherValue");
        config.addProperty("AnotherProperty", "3rdValue");
        layout.setComment("AnotherProperty", "AnotherComment");
        layout.setBlancLinesBefore("AnotherProperty", 2);
        layout.setSingleLine("AnotherProperty", true);
        layout.setHeaderComment("A header comment" + CRNORM + "for my properties");
        checkLayoutString("# A header comment" + CR + "# for my properties"
                + CR + CR + "# " + TEST_COMMENT + CR + TEST_KEY + " = "
                + TEST_VALUE + CR + TEST_KEY + " = " + TEST_VALUE + "2" + CR
                + CR + CR + "# AnotherComment" + CR
                + "AnotherProperty = AnotherValue,3rdValue" + CR);
    }

    /**
     * Tests the force single line flag.
     */
    @Test
    public void testSaveForceSingleLine() throws ConfigurationException
    {
        config.setListDelimiterHandler(new DefaultListDelimiterHandler(';'));
        config.addProperty(TEST_KEY, TEST_VALUE);
        config.addProperty(TEST_KEY, TEST_VALUE + "2");
        config.addProperty("AnotherProperty", "value1;value2;value3");
        layout.setComment(TEST_KEY, TEST_COMMENT);
        layout.setForceSingleLine(true);
        checkLayoutString("# " + TEST_COMMENT + CR + TEST_KEY + " = "
                + TEST_VALUE + ';' + TEST_VALUE + "2" + CR
                + "AnotherProperty = value1;value2;value3" + CR);
    }

    /**
     * Tests the trimComment method.
     */
    @Test
    public void testTrimComment()
    {
        assertEquals("Wrong trimmed comment", "This is a comment" + CR
                + "that spans multiple" + CR + "lines in a" + CR
                + " complex way.", PropertiesConfigurationLayout.trimComment(
                "   # This is a comment" + CR + "that spans multiple" + CR
                        + "!lines in a" + CR + " complex way.", false));
    }

    /**
     * Tests trimming a comment with trailing CRs.
     */
    @Test
    public void testTrimCommentTrainlingCR()
    {
        assertEquals("Wrong trimmed comment", "Comment with" + CR
                + "trailing CR" + CR, PropertiesConfigurationLayout
                .trimComment("Comment with" + CR + "! trailing CR" + CR, false));
    }

    /**
     * Tests enforcing comment characters in a comment.
     */
    @Test
    public void testTrimCommentFalse()
    {
        assertEquals("Wrong trimmed comment", "# Comment with" + CR
                + " ! some mixed " + CR + "#comment" + CR + "# lines",
                PropertiesConfigurationLayout.trimComment("Comment with" + CR
                        + " ! some mixed " + CR + "#comment" + CR + "lines",
                        true));
    }

    /**
     * Tests accessing data for a property, which is not stored.
     */
    @Test
    public void testGetNonExistingLayouData()
    {
        assertNull("A comment was found", layout.getComment("unknown"));
        assertTrue("A multi-line property", layout.isSingleLine("unknown"));
        assertEquals("Leading blanc lines", 0, layout
                .getBlancLinesBefore("unknown"));
    }

    /**
     * Tests accessing a property with a null key. This should throw an
     * exception.
     */
    @Test(expected = IllegalArgumentException.class)
    public void testGetNullLayouttData()
    {
        layout.setComment(null, TEST_COMMENT);
    }

    /**
     * Tests resetting a comment.
     */
    @Test
    public void testSetNullComment()
    {
        fillLayout();
        layout.setComment(TEST_KEY, null);
        assertNull("Comment was not reset", layout.getComment(TEST_KEY));
    }

    /**
     * Tests saving when a comment for a non existing property is contained in
     * the layout object. This comment should be ignored.
     */
    @Test
    public void testSaveCommentForUnexistingProperty()
            throws ConfigurationException
    {
        fillLayout();
        layout.setComment("NonExistingKey", "NonExistingComment");
        final String output = getLayoutString();
        assertTrue("Non existing key was found", !output.contains("NonExistingKey"));
        assertTrue("Non existing comment was found", !output.contains("NonExistingComment"));
    }

    /**
     * Tests saving an empty layout object.
     */
    @Test
    public void testSaveEmptyLayout() throws ConfigurationException
    {
        checkLayoutString("");
    }

    /**
     * Tests the copy constructor.
     */
    @Test
    public void testInitCopy()
    {
        fillLayout();
        final PropertiesConfigurationLayout l2 = new PropertiesConfigurationLayout(layout);
        assertEquals("Wrong number of keys", layout.getKeys().size(), l2
                .getKeys().size());
        for (final String key : layout.getKeys())
        {
            assertTrue("Key was not found: " + key, l2.getKeys().contains(key));
        }
        assertEquals("Wrong header comment", layout.getHeaderComment(),
                l2.getHeaderComment());
        assertEquals("Wrong footer comment", layout.getFooterComment(),
                l2.getFooterComment());
    }

    /**
     * Tests if the copy and the original are independent from each other.
     */
    @Test
    public void testInitCopyModify()
    {
        fillLayout();
        final PropertiesConfigurationLayout l2 = new PropertiesConfigurationLayout(layout);
        assertEquals("Comments are not equal", layout.getComment(TEST_KEY), l2
                .getComment(TEST_KEY));
        layout.setComment(TEST_KEY, "A new comment");
        assertEquals("Comment was changed", TEST_COMMENT, l2
                .getCanonicalComment(TEST_KEY, false));
        l2.setBlancLinesBefore(TEST_KEY, l2.getBlancLinesBefore(TEST_KEY) + 1);
        assertFalse("Blanc lines do not differ", layout
                .getBlancLinesBefore(TEST_KEY) == l2
                .getBlancLinesBefore(TEST_KEY));
    }

    /**
     * Tests changing the separator for a property.
     */
    @Test
    public void testSetSeparator() throws ConfigurationException
    {
        config.addProperty(TEST_KEY, TEST_VALUE);
        layout.setSeparator(TEST_KEY, ":");
        checkLayoutString(TEST_KEY + ":" + TEST_VALUE + CR);
    }

    /**
     * Tests setting the global separator. This separator should override the
     * separators for all properties.
     */
    @Test
    public void testSetGlobalSeparator() throws ConfigurationException
    {
        final String sep = "=";
        config.addProperty(TEST_KEY, TEST_VALUE);
        config.addProperty("key2", "value2");
        layout.setSeparator(TEST_KEY, " : ");
        layout.setGlobalSeparator(sep);
        checkLayoutString(TEST_KEY + sep + TEST_VALUE + CR + "key2" + sep
                + "value2" + CR);
    }

    /**
     * Tests setting the line separator.
     */
    @Test
    public void testSetLineSeparator() throws ConfigurationException
    {
        final String lf = CR + CR;
        config.addProperty(TEST_KEY, TEST_VALUE);
        layout.setBlancLinesBefore(TEST_KEY, 2);
        layout.setComment(TEST_KEY, TEST_COMMENT);
        layout.setHeaderComment("Header comment");
        layout.setLineSeparator(lf);
        checkLayoutString("# Header comment" + lf + lf + lf + lf + "# "
                + TEST_COMMENT + lf + TEST_KEY + " = " + TEST_VALUE + lf);
    }

    /**
     * Tests whether the line separator is also taken into account within
     * comments.
     */
    @Test
    public void testSetLineSeparatorInComments() throws ConfigurationException
    {
        final String lf = "<-\n";
        config.addProperty(TEST_KEY, TEST_VALUE);
        layout.setComment(TEST_KEY, TEST_COMMENT + "\nMore comment");
        layout.setHeaderComment("Header\ncomment");
        layout.setLineSeparator(lf);
        checkLayoutString("# Header" + lf + "# comment" + lf + lf + "# "
                + TEST_COMMENT + lf + "# More comment" + lf + TEST_KEY + " = "
                + TEST_VALUE + lf);
    }

    /**
     * Tests whether a line with whitespace is handled correctly. This is
     * related to CONFIGURATION-582.
     */
    @Test
    public void testLineWithBlank() throws ConfigurationException
    {
        builder.addComment(TEST_COMMENT);
        builder.addLine(" ");
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        assertEquals("Wrong comment", TEST_COMMENT + CRNORM + " ",
                layout.getCanonicalComment(TEST_KEY, false));
    }

    /**
     * Helper method for filling the layout object with some properties.
     */
    private void fillLayout()
    {
        builder.addComment("A header comment");
        builder.addComment(null);
        builder.addProperty("prop", "value");
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        builder.addProperty("anotherProp", "anotherValue");
        builder.addComment("A footer comment");
        try
        {
            layout.load(config, builder.getReader());
        }
        catch (final ConfigurationException cex)
        {
            // should not happen
            fail("Exception was thrown: " + cex);
        }
    }

    /**
     * Writes the layout's data into a string.
     *
     * @return the layout file's content as string
     * @throws ConfigurationException if an error occurs
     */
    private String getLayoutString() throws ConfigurationException
    {
        final StringWriter out = new StringWriter();
        layout.save(config, out);
        return out.toString();
    }

    /**
     * Checks if the layout's output is correct.
     *
     * @param expected the expected result
     * @throws ConfigurationException if an error occurs
     */
    private void checkLayoutString(final String expected)
            throws ConfigurationException
    {
        assertEquals("Wrong layout file content", expected, getLayoutString());
    }

    /**
     * A helper class used for constructing test properties files.
     */
    static class PropertiesBuilder
    {
        /** A buffer for storing the data. */
        private final StringBuilder buf = new StringBuilder();

        /** A counter for varying the comment character. */
        private int commentCounter;

        /**
         * Adds a line verbatim to the simulated file.
         *
         * @param s the content of the line
         */
        public void addLine(final String s)
        {
            buf.append(s).append(CR);
        }

        /**
         * Adds a property to the simulated file.
         *
         * @param key the property key
         * @param value the value
         */
        public void addProperty(final String key, final String value)
        {
            buf.append(key).append(" = ").append(value).append(CR);
        }

        /**
         * Adds a comment line.
         *
         * @param s the comment (can be <b>null</b>, then a blanc line is
         * added)
         */
        public void addComment(final String s)
        {
            if (s != null)
            {
                if (commentCounter % 2 == 0)
                {
                    buf.append("# ");
                }
                else
                {
                    buf.append("! ");
                }
                buf.append(s);
                commentCounter++;
            }
            buf.append(CR);
        }

        /**
         * Returns a reader for the simulated properties.
         *
         * @return a reader
         */
        public Reader getReader()
        {
            return new StringReader(buf.toString());
        }

        /**
         * Returns a string representation of the buffer's content.
         *
         * @return the buffer as string
         */
        @Override
        public String toString()
        {
            return buf.toString();
        }
    }

    /**
     * A mock properties configuration implementation that is used to check
     * whether some expected methods are called.
     */
    static class LayoutTestConfiguration extends PropertiesConfiguration
    {
        /** Stores a builder object. */
        public PropertiesBuilder builder;

        /**
         * Simulates the propertyLoaded() callback. If a builder was set, a
         * load() call on the layout is invoked.
         */
        @Override
        boolean propertyLoaded(final String key, final String value, Deque<URL> seenStack)
                throws ConfigurationException
        {
            if (builder == null)
            {
                return super.propertyLoaded(key, value, seenStack);
            }
            if (PropertiesConfiguration.getInclude().equals(key))
            {
                getLayout().load(this, builder.getReader());
                return false;
            }
            return true;
        }
    }
}
