/*
 * 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.cassandra.io.sstable;

import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import java.util.function.BiFunction;

import com.google.common.collect.ImmutableMap;

import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;

import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.Config.SSTableConfig;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.YamlConfigurationLoader;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.io.sstable.format.SSTableFormat;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.SSTableWriter;
import org.apache.cassandra.io.sstable.format.Version;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileOutputStreamPlus;
import org.apache.cassandra.io.util.FileUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.when;

public class SSTableFormatTest
{
    public static abstract class AbstractFormat implements SSTableFormat<SSTableReader, SSTableWriter>
    {
        public Map<String, String> options;
        public String name;
        public String latestVersion;

        public AbstractFormat(String latestVersion)
        {
            this.latestVersion = latestVersion;
        }

        @Override
        public Version getVersion(String version)
        {
            Version v = Mockito.mock(Version.class);
            when(v.toString()).thenReturn(version);
            when(v.isCompatible()).thenReturn(version.charAt(0) == latestVersion.charAt(0));
            return v;
        }

        @Override
        public Version getLatestVersion()
        {
            return getVersion(latestVersion);
        }

        @Override
        public String name()
        {
            return name;
        }

        static class Factory implements SSTableFormat.Factory
        {
            public String name;
            public BiFunction<Map<String, String>, String, SSTableFormat<?, ?>> provider;

            public Factory(String name, BiFunction<Map<String, String>, String, SSTableFormat<?, ?>> provider)
            {
                this.name = name;
                this.provider = provider;
            }

            @Override
            public String name()
            {
                return name;
            }

            @Override
            public SSTableFormat<?, ?> getInstance(Map<String, String> options)
            {
                return provider.apply(options, name);
            }
        }
    }

    public static AbstractFormat.Factory factory(String name, Class<? extends AbstractFormat> clazz)
    {
        return new AbstractFormat.Factory(name, (options, version) -> {
            AbstractFormat format = Mockito.spy(clazz);
            format.name = name;
            format.options = options;
            return format;
        });
    }

    public static abstract class Format1 extends AbstractFormat
    {
        public Format1()
        {
            super("xx");
        }
    }

    public static abstract class Format2 extends AbstractFormat
    {
        public Format2()
        {
            super("yy");
        }
    }

    public static abstract class Format3 extends AbstractFormat
    {
        public Format3()
        {
            super("zz");
        }
    }

    @BeforeClass
    public static void beforeClass()
    {
        DatabaseDescriptor.clientInitialization();
    }

    private static final String yamlContent0 = "";
    private static final SSTableConfig expected0 = new Config.SSTableConfig();

    private static final String yamlContent1 = "sstable:\n" +
                                               "   selected_format: aaa\n";
    private static final SSTableConfig expected1 = new Config.SSTableConfig()
    {
        {
            selected_format = "aaa";
        }
    };

    private static final String yamlContent2 = "sstable:\n" +
                                               "   selected_format: aaa\n" +
                                               "   format:\n" +
                                               "       aaa:\n" +
                                               "           param1: value1\n" +
                                               "           param2: value2\n" +
                                               "       bbb:\n" +
                                               "           param3: value3\n" +
                                               "           param4: value4\n";

    private static final Config.SSTableConfig expected2 = new SSTableConfig()
    {
        {
            selected_format = "aaa";
            format = ImmutableMap.of("aaa", ImmutableMap.of("param1", "value1", "param2", "value2"),
                                     "bbb", ImmutableMap.of("param3", "value3", "param4", "value4"));
        }
    };

    private static final SSTableConfig unexpected = new Config.SSTableConfig()
    {
        {
            selected_format = "aaa";
        }
    };


    @Test
    public void testParsingYamlConfig() throws IOException
    {
        YamlConfigurationLoader loader = new YamlConfigurationLoader();
        File f = FileUtils.createTempFile("sstable_format_test_config", ".yaml");
        URL url = f.toPath().toUri().toURL();

        ImmutableMap.of(yamlContent0, expected0, yamlContent1, expected1, yamlContent2, expected2).forEach((yamlContent, expected) -> {
            try (FileOutputStreamPlus out = f.newOutputStream(File.WriteMode.OVERWRITE))
            {
                out.write(yamlContent.getBytes());
            }
            catch (IOException e)
            {
                throw new RuntimeException(e);
            }
            Config config = loader.loadConfig(url);
            assertThat(config.sstable).describedAs("Yaml: \n%s\n", yamlContent).isEqualToComparingFieldByField(expected);
        });
    }

    @Before
    public void before()
    {
    }

    public static void configure(SSTableConfig config, SSTableFormat.Factory... factories)
    {
        DatabaseDescriptor.resetSSTableFormats(Arrays.asList(factories), config);
    }

    private void verifyFormat(String name, Map<String, String> options)
    {
        AbstractFormat format = (AbstractFormat) DatabaseDescriptor.getSSTableFormats().get(name);
        assertThat(format.name).isEqualTo(name);
        assertThat(format.options).isEqualTo(options);
    }

    private void verifySelectedFormat(String name)
    {
        assertThat(DatabaseDescriptor.getSelectedSSTableFormat().name()).isEqualTo(name);
    }

    @Test
    public void testValidConfig()
    {
        configure(expected1, factory("aaa", Format1.class));
        assertThat(DatabaseDescriptor.getSSTableFormats()).hasSize(1);
        verifyFormat("aaa", ImmutableMap.of());
        verifySelectedFormat("aaa");

        configure(expected2, factory("aaa", Format1.class), factory("bbb", Format2.class), factory("ccc", Format3.class));
        assertThat(DatabaseDescriptor.getSSTableFormats()).hasSize(3);
        verifyFormat("aaa", ImmutableMap.of("param1", "value1", "param2", "value2"));
        verifyFormat("bbb", ImmutableMap.of("param3", "value3", "param4", "value4"));
        verifyFormat("ccc", ImmutableMap.of());
        verifySelectedFormat("aaa");
    }

    @Test
    public void testConfigValidation()
    {
        // invalid name
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected1, factory("Aa", Format1.class)))
                                                               .withMessageContainingAll("SSTable format name", "must be non-empty, lower-case letters only string");
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected1, factory("a-a", Format1.class)))
                                                               .withMessageContainingAll("SSTable format name", "must be non-empty, lower-case letters only string");
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected1, factory("a1", Format1.class)))
                                                               .withMessageContainingAll("SSTable format name", "must be non-empty, lower-case letters only string");
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected1, factory("", Format1.class)))
                                                               .withMessageContainingAll("SSTable format name", "must be non-empty, lower-case letters only string");

        // duplicate name
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected1, factory("aaa", Format1.class), factory("aaa", Format2.class)))
                                                               .withMessageContainingAll("Multiple sstable format implementations with the same name", "aaa");

        // missing name
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected1, factory(null, Format1.class)))
                                                               .withMessageContainingAll("SSTable format name", "cannot be null");

        // Configuration contains options of unknown sstable formats
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected2, factory("aaa", Format1.class)))
                                                               .withMessageContainingAll("Configuration contains options of unknown sstable formats", "bbb");

        // Selected sstable format '%s' is not available
        assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> configure(expected1, factory("bbb", Format1.class)))
                                                               .withMessageContainingAll("Selected sstable format", "aaa", "is not available");
    }
}
