/*
 * 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.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

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.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Test class for PropertiesConfigurationLayout.
 */
public class TestPropertiesConfigurationLayout {

    /**
     * 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. */
        private 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, final 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;
        }
    }

    /**
     * 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 comment line.
         *
         * @param s the comment (can be <b>null</b>, then a blank 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);
        }

        /**
         * 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);
        }

        /**
         * 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();
        }
    }

    /** Constant for the line break character. */
    private static final String CR = System.lineSeparator();

    /** 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;

    /**
     * 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(expected, getLayoutString());
    }

    /**
     * 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");
        assertDoesNotThrow(() -> layout.load(config, builder.getReader()));
    }

    /**
     * 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();
    }

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

    /**
     * Tests whether blank lines before a property are correctly detected.
     */
    @Test
    public void testBlankLines() 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(2, layout.getBlankLinesBefore(TEST_KEY));
        assertEquals(TEST_COMMENT + CRNORM, layout.getCanonicalComment(TEST_KEY, false));
        assertEquals(TEST_VALUE, config.getString(TEST_KEY));
    }

    /**
     * Tests whether blank lines before a property are correctly detected if a header comment is present
     */
    @Test
    public void testBlankLinesWithHeaderComment() throws ConfigurationException {
        builder.addComment(TEST_COMMENT);
        builder.addComment(null);
        builder.addComment(null);
        builder.addComment(TEST_COMMENT);
        builder.addProperty(TEST_KEY, TEST_VALUE);
        layout.load(config, builder.getReader());
        assertEquals(2, layout.getBlankLinesBefore(TEST_KEY));
        assertEquals(TEST_COMMENT, layout.getCanonicalComment(TEST_KEY, false));
        assertEquals(TEST_VALUE, config.getString(TEST_KEY));
    }

    /**
     * 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(TEST_COMMENT + CRNORM + TEST_COMMENT, layout.getCanonicalComment(TEST_KEY, false));
        assertEquals(0, layout.getBlankLinesBefore(TEST_KEY));
    }

    /**
     * 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(layout.getKeys().contains(TEST_KEY));
        assertEquals(0, layout.getBlankLinesBefore(TEST_KEY));
        assertTrue(layout.isSingleLine(TEST_KEY));
        assertEquals(" = ", layout.getSeparator(TEST_KEY));
    }

    /**
     * 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(layout.getKeys().contains(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(layout.isSingleLine(TEST_KEY));
        assertEquals(TEST_COMMENT, layout.getCanonicalComment(TEST_KEY, false));
    }

    /**
     * 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(layout.isSingleLine(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(layout.getKeys().isEmpty());
        assertNull(layout.getHeaderComment());
    }

    /**
     * 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(layout.getKeys().contains(TEST_KEY));
    }

    /**
     * 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(layout.getKeys().contains(TEST_KEY));
    }

    /**
     * Tests accessing data for a property, which is not stored.
     */
    @Test
    public void testGetNonExistingLayouData() {
        assertNull(layout.getComment("unknown"));
        assertTrue(layout.isSingleLine("unknown"));
        assertEquals(0, layout.getBlankLinesBefore("unknown"));
    }

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

    /**
     * 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(TEST_COMMENT, layout.getCanonicalHeaderComment(false));
        assertNull(layout.getCanonicalComment(TEST_KEY, false));
    }

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

    /**
     * Tests if a header comment containing blank lines is correctly detected.
     */
    @Test
    public void testHeaderCommentWithBlanks() 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(TEST_COMMENT + CRNORM + CRNORM + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
        assertNull(layout.getComment(TEST_KEY));
    }

    /**
     * Tests if a header comment containing blank 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 testHeaderCommentWithBlanksAndPresetHeaderComment() 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(presetHeaderComment, layout.getCanonicalHeaderComment(false));
        assertNull(layout.getComment(TEST_KEY));
    }

    /**
     * Tests if a header comment is correctly detected when it contains blank lines and the first property has a comment,
     * too.
     */
    @Test
    public void testHeaderCommentWithBlanksAndPropComment() 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(TEST_COMMENT + CRNORM + CRNORM + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
        assertEquals(TEST_COMMENT, layout.getCanonicalComment(TEST_KEY, false));
    }

    /**
     * Tests a newly created instance.
     */
    @Test
    public void testInit() {
        assertTrue(layout.getKeys().isEmpty());
        assertNull(layout.getHeaderComment());
        final Iterator<EventListener<? super ConfigurationEvent>> it = config.getEventListeners(ConfigurationEvent.ANY).iterator();
        assertTrue(it.hasNext());
        assertSame(layout, it.next());
        assertFalse(it.hasNext());
        assertFalse(layout.isForceSingleLine());
        assertNull(layout.getGlobalSeparator());
    }

    /**
     * Tests the copy constructor.
     */
    @Test
    public void testInitCopy() {
        fillLayout();
        final PropertiesConfigurationLayout l2 = new PropertiesConfigurationLayout(layout);
        assertEquals(l2.getKeys(), layout.getKeys());
        assertEquals(layout.getHeaderComment(), l2.getHeaderComment());
        assertEquals(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(layout.getComment(TEST_KEY), l2.getComment(TEST_KEY));
        layout.setComment(TEST_KEY, "A new comment");
        assertEquals(TEST_COMMENT, l2.getCanonicalComment(TEST_KEY, false));
        l2.setBlankLinesBefore(TEST_KEY, l2.getBlankLinesBefore(TEST_KEY) + 1);
        assertNotEquals(layout.getBlankLinesBefore(TEST_KEY), l2.getBlankLinesBefore(TEST_KEY));
    }

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

    /**
     * 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(layout.isSingleLine(TEST_KEY));
        assertEquals(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(layout.isSingleLine(TEST_KEY));
        assertEquals(2, config.getList(TEST_KEY).size());
    }

    /**
     * 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(TEST_COMMENT + CRNORM + " ", 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(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 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(layout.getHeaderComment());
        assertEquals(1, layout.getKeys().size());
        assertTrue(layout.getKeys().contains(TEST_KEY));
        assertEquals(TEST_COMMENT, layout.getCanonicalComment(TEST_KEY, false));
        assertEquals(0, layout.getBlankLinesBefore(TEST_KEY));
        assertTrue(layout.isSingleLine(TEST_KEY));
        assertEquals(TEST_VALUE, config.getString(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("Header comment", layout.getCanonicalHeaderComment(false));
        assertFalse(layout.getKeys().contains(PropertiesConfiguration.getInclude()));
        assertEquals(TEST_COMMENT + CRNORM + "A nested header comment." + CRNORM + "With multiple lines" + CRNORM + CRNORM + "Second comment",
                layout.getCanonicalComment(TEST_KEY, false));
    }

    /**
     * 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.setBlankLinesBefore("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 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();
        assertFalse(output.contains("NonExistingKey"));
        assertFalse(output.contains("NonExistingComment"));
    }

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

    /**
     * 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 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.setBlankLinesBefore(TEST_KEY, 2);
        layout.setComment(TEST_KEY, TEST_COMMENT);
        layout.setHeaderComment("Header comment");
        layout.setLineSeparator(lf);
        checkLayoutString("# Header comment" + 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 resetting a comment.
     */
    @Test
    public void testSetNullComment() {
        fillLayout();
        layout.setComment(TEST_KEY, null);
        assertNull(layout.getComment(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 the trimComment method.
     */
    @Test
    public void testTrimComment() {
        assertEquals("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 enforcing comment characters in a comment.
     */
    @Test
    public void testTrimCommentFalse() {
        assertEquals("# Comment with" + CR + " ! some mixed " + CR + "#comment" + CR + "# lines",
                PropertiesConfigurationLayout.trimComment("Comment with" + CR + " ! some mixed " + CR + "#comment" + CR + "lines", true));
    }

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