/*
 * 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
 *
 *     https://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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Test class to test the handling of list structures in XMLConfiguration.
 */
public class TestXMLListHandling {

    /** XML to be loaded by the configuration. */
    private static final String SOURCE = "<config><values>a,b,c</values><split><value>1</value><value>2</value></split>"
            + "<mixed><values>foo,blah</values><values>bar,baz</values></mixed></config>";

    /** Key for the string property with multiple values. */
    private static final String KEY_VALUES = "values";

    /** Key for the split list property. */
    private static final String KEY_SPLIT = "split.value";

    /** The XML element name for the single values of the split list. */
    private static final String ELEM_SPLIT = "value";

    /**
     * Checks whether the specified XML contains a list of values as a single, comma-separated string.
     *
     * @param xml the XML
     * @param key the key
     * @param values the expected values
     */
    private static void checkCommaSeparated(final String xml, final String key, final String... values) {
        final String strValues = StringUtils.join(values, ',');
        final String element = element(key, strValues);
        assertTrue(xml.contains(element));
    }

    /**
     * Checks whether the specified XML contains a list of values as multiple XML elements.
     *
     * @param xml the XML
     * @param key the key
     * @param values the expected values
     */
    private static void checkSplit(final String xml, final String key, final String... values) {
        for (final String v : values) {
            assertTrue(xml.contains(element(key, v)));
        }
    }

    /**
     * Generates an XML element with the specified value as body.
     *
     * @param key the key
     * @param value the value
     * @return the string representation of this element
     */
    private static String element(final String key, final String value) {
        return "<" + key + '>' + value + "</" + key + '>';
    }

    /**
     * Parses the specified string into an XML configuration.
     *
     * @param xml the XML to be parsed
     * @return the resulting configuration
     */
    private static XMLConfiguration readFromString(final String xml) throws ConfigurationException {
        final XMLConfiguration config = new XMLConfiguration();
        config.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
        final FileHandler handler = new FileHandler(config);
        handler.load(new StringReader(xml));
        return config;
    }

    /** Configuration to be tested. */
    private XMLConfiguration config;

    /**
     * Saves the test configuration into a string.
     *
     * @return the resulting string
     */
    private String saveToString() throws ConfigurationException {
        final StringWriter writer = new StringWriter(4096);
        final FileHandler handler = new FileHandler(config);
        handler.save(writer);
        return writer.toString();
    }

    @BeforeEach
    public void setUp() throws Exception {
        config = readFromString(SOURCE);
    }

    /**
     * Tests that a list item can be added without affecting the format.
     */
    @Test
    void testAddListItem() throws ConfigurationException {
        config.addProperty(KEY_VALUES, "d");
        config.addProperty(KEY_SPLIT, "3");
        final String xml = saveToString();

        checkSplit(xml, ELEM_SPLIT, "1", "2", "3");
        checkCommaSeparated(xml, KEY_VALUES, "a", "b", "c", "d");
    }

    /**
     * Tries to save the configuration with a different list delimiter handler which does not support escaping of lists.
     * This should fail with a meaningful exception message.
     */
    @Test
    void testIncompatibleListDelimiterOnSaving() {
        config.setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE);
        assertThrows(ConfigurationRuntimeException.class, this::saveToString);
    }

    /**
     * Tests whether a list consisting of multiple elements where some elements define multiple values is handled correctly.
     */
    @Test
    void testMixedList() throws ConfigurationException {
        final List<String> expected = Arrays.asList("foo", "blah", "bar", "baz");
        assertEquals(expected, config.getList("mixed.values"));
        final String xml = saveToString();

        final XMLConfiguration c2 = readFromString(xml);
        assertEquals(expected, c2.getList("mixed.values"));
    }

    /**
     * Tests that a list item can be removed without affecting the format.
     */
    @Test
    void testRemoveListItem() throws ConfigurationException {
        config.clearProperty(KEY_VALUES + "(2)");
        config.clearProperty(KEY_SPLIT + "(1)");
        final String xml = saveToString();

        checkSplit(xml, ELEM_SPLIT, "1");
        checkCommaSeparated(xml, KEY_VALUES, "a", "b");
    }

    /**
     * Tests that the list format is kept if properties are not touched,
     */
    @Test
    void testSaveNoChanges() throws ConfigurationException {
        final String xml = saveToString();

        checkSplit(xml, ELEM_SPLIT, "1", "2");
        checkCommaSeparated(xml, KEY_VALUES, "a", "b", "c");
    }
}
