/*
 * Copyright 2018 GoDataDriven B.V.
 *
 * Licensed 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 io.divolte.server.config;

import static org.junit.Assert.*;

import com.google.common.collect.ImmutableMap;
import org.junit.Test;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

import java.time.Duration;

public class ValidatedConfigurationTest {
    @Test
    public void shouldNotThrowExceptionsOnInvalidConfiguration() {
        final Config empty = ConfigFactory.parseString("");
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> empty);

        assertFalse(vc.isValid());
        assertFalse(vc.errors().isEmpty());
    }

    @Test
    public void shouldValidateJavaScriptName() {
        final String propertyName = "divolte.sources.browser.javascript.name";
        final String invalidValue = "404.exe";
        final Config config = ConfigFactory.parseMap(ImmutableMap.of(propertyName, invalidValue))
                                           .withFallback(ConfigFactory.parseResources("base-test-server.conf"))
                                           .withFallback(ConfigFactory.parseResources("reference-test.conf"));

        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> config);
        assertFalse(vc.errors().isEmpty());
        final String reportedPropertyName = propertyName.replace(".sources.browser.", ".sources[browser].");
        assertEquals("Property '" + reportedPropertyName + "' must match \"^[A-Za-z0-9_-]+\\.js$\". Found: '" + invalidValue + "'.",
                     vc.errors().get(0));
    }

    @Test(expected = IllegalStateException.class)
    public void shouldNotAllowAccessToInvalidConfiguration() {
        final Config empty = ConfigFactory.parseString("");
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> empty);

        assertFalse(vc.isValid());

        // This throws IllegalArgumentException in case of invalid configuration
        vc.configuration();
    }

    @Test
    public void shouldNotBreakOnConfigSyntaxErrorsDuringLoad() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseString("not = //allowed"));
        assertFalse(vc.errors().isEmpty());
        assertEquals("String: 1: Expecting a value but got wrong token: end of file", vc.errors().get(0));
    }

    @Test
    public void shouldMapReferenceConfig() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(ConfigFactory::load);
        assertTrue(vc.errors().isEmpty());
    }

    @Test
    public void shouldReportMissingSourcesAndSinks() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("missing-sources-sinks.conf"));

        assertFalse(vc.isValid());
        assertEquals(1, vc.errors().size());
        assertTrue(
                vc.errors()
                  .get(0)
                  .startsWith("Property 'divolte.' The following sources and/or sinks were used in a mapping but never defined: [missing-sink, missing-source].."));
    }

    @Test
    public void sourceAndSinkNamesCannotCollide() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("source-sink-collisions.conf"));

        assertFalse(vc.isValid());
        assertEquals(1, vc.errors().size());
        assertTrue(
                vc.errors()
                  .get(0)
                  .startsWith("Property 'divolte.' Source and sink names cannot collide (must be globally unique). The following names were both used as source and as sink: [foo, bar].."));
    }

    @Test
    public void sharedSinksAllowedWithSameSchema() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("multiple-mappings-same-schema-shared-sink.conf"));
        assertTrue(vc.isValid());
    }

    @Test
    public void sharedSinksCannotHaveDifferentSchemas() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("multiple-mappings-different-schema-shared-sink.conf"));

        assertFalse(vc.isValid());
        assertEquals(1, vc.errors().size());
        assertTrue(
                vc.errors()
                  .get(0)
                  .startsWith("Property 'divolte.' Any sink can only use one schema. The following sinks have multiple mappings with different schema's linked to them: [kafka].."));
    }

    @Test
    public void kafkaSinksSupportsConfluentMode() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("kafka-sink-confluent.conf"));
        assertTrue(vc.isValid());
    }

    @Test
    public void mappingsCanContainConfluentId() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("mapping-configuration-confluent-id.conf"));
        assertTrue(vc.isValid());
    }

    @Test
    public void mappingsForConfluentSinksMustHaveConfluentId() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("kafka-sink-confluent-without-confluent-id.conf"));
        assertFalse(vc.isValid());
        assertEquals(1, vc.errors().size());
        assertTrue(
            vc.errors()
              .get(0)
              .startsWith("Property 'divolte.' Mappings used by sinks in Confluent-mode must have their 'confluent_id' attribute set. The following mappings are missing this: [test]..")
        );
    }

    @Test
    public void allMappingsForConfluentSinksMustHaveConfluentId() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("kafka-sink-confluent-partially-without-confluent-id.conf"));
        assertFalse(vc.isValid());
        assertEquals(1, vc.errors().size());
        assertTrue(
            vc.errors()
              .get(0)
              .startsWith("Property 'divolte.' Mappings used by sinks in Confluent-mode must have their 'confluent_id' attribute set. The following mappings are missing this: [test-2]..")
        );
    }

    @Test
    public void mappingsForConfluentSinksMustHaveSameConfluentId() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("kafka-sink-confluent-with-confluent-id-conflict.conf"));
        assertFalse(vc.isValid());
        assertEquals(1, vc.errors().size());
        assertTrue(
            vc.errors()
                .get(0)
                .startsWith("Property 'divolte.' Any sink can only use one confluent identifier. The following sinks have multiple mappings with different 'confluent_id' attributes: [kafka]..")
        );
    }

    @Test
    public void shouldSetShutdownDelay() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("reference-test-shutdown.conf"));

        assertTrue(vc.isValid());
        assertEquals(Duration.ofMillis(2200), vc.configuration().global.server.shutdownDelay);
    }

    @Test
    public void shouldSetShutdownTimeout() {
        final ValidatedConfiguration vc = new ValidatedConfiguration(() -> ConfigFactory.parseResources("reference-test-shutdown.conf"));

        assertTrue(vc.isValid());
        assertEquals(Duration.ofMinutes(3), vc.configuration().global.server.shutdownTimeout);
    }
}