/*
 * 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.kafka.connect.util;

import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.connect.runtime.WorkerConfig;
import org.apache.kafka.connect.runtime.distributed.DistributedConfig;
import org.apache.kafka.connect.runtime.standalone.StandaloneConfig;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static org.apache.kafka.clients.CommonClientConfigs.CLIENT_ID_CONFIG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.STRICT_STUBS)
public class ConnectUtilsTest {

    @Test
    public void testAddMetricsContextPropertiesDistributed() {
        Map<String, String> props = new HashMap<>();
        props.put(DistributedConfig.GROUP_ID_CONFIG, "connect-cluster");
        props.put(DistributedConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(DistributedConfig.CONFIG_TOPIC_CONFIG, "connect-configs");
        props.put(DistributedConfig.OFFSET_STORAGE_TOPIC_CONFIG, "connect-offsets");
        props.put(DistributedConfig.STATUS_STORAGE_TOPIC_CONFIG, "connect-status");
        props.put(DistributedConfig.KEY_CONVERTER_CLASS_CONFIG, "org.apache.kafka.connect.json.JsonConverter");
        props.put(DistributedConfig.VALUE_CONVERTER_CLASS_CONFIG, "org.apache.kafka.connect.json.JsonConverter");
        DistributedConfig config = new DistributedConfig(props);

        Map<String, Object> prop = new HashMap<>();
        ConnectUtils.addMetricsContextProperties(prop, config, "cluster-1");
        assertEquals("connect-cluster", prop.get(CommonClientConfigs.METRICS_CONTEXT_PREFIX + WorkerConfig.CONNECT_GROUP_ID));
        assertEquals("cluster-1", prop.get(CommonClientConfigs.METRICS_CONTEXT_PREFIX + WorkerConfig.CONNECT_KAFKA_CLUSTER_ID));
    }

    @Test
    public void testAddMetricsContextPropertiesStandalone() {
        Map<String, String> props = new HashMap<>();
        props.put(StandaloneConfig.OFFSET_STORAGE_FILE_FILENAME_CONFIG, "offsetStorageFile");
        props.put(StandaloneConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(StandaloneConfig.KEY_CONVERTER_CLASS_CONFIG, "org.apache.kafka.connect.json.JsonConverter");
        props.put(StandaloneConfig.VALUE_CONVERTER_CLASS_CONFIG, "org.apache.kafka.connect.json.JsonConverter");
        StandaloneConfig config = new StandaloneConfig(props);

        Map<String, Object> prop = new HashMap<>();
        ConnectUtils.addMetricsContextProperties(prop, config, "cluster-1");
        assertNull(prop.get(CommonClientConfigs.METRICS_CONTEXT_PREFIX + WorkerConfig.CONNECT_GROUP_ID));
        assertEquals("cluster-1", prop.get(CommonClientConfigs.METRICS_CONTEXT_PREFIX + WorkerConfig.CONNECT_KAFKA_CLUSTER_ID));

    }

    @Test
    public void testNoOverrideWarning() {
        Map<String, ? super String> props = new HashMap<>();
        assertEquals(
                Optional.empty(),
                ConnectUtils.ensurePropertyAndGetWarning(props, "key", "value", "because i say so", true)
        );
        assertEquals("value", props.get("key"));

        props.clear();
        assertEquals(
                Optional.empty(),
                ConnectUtils.ensurePropertyAndGetWarning(props, "key", "value", "because i say so", false)
        );
        assertEquals("value", props.get("key"));

        props.clear();
        props.put("key", "value");
        assertEquals(
                Optional.empty(),
                ConnectUtils.ensurePropertyAndGetWarning(props, "key", "value", "because i say so", true)
        );
        assertEquals("value", props.get("key"));

        props.clear();
        props.put("key", "VALUE");
        assertEquals(
                Optional.empty(),
                ConnectUtils.ensurePropertyAndGetWarning(props, "key", "value", "because i say so", false)
        );
        assertEquals("VALUE", props.get("key"));
    }

    @Test
    public void testOverrideWarning() {
        Map<String, ? super String> props = new HashMap<>();
        props.put("\u1984", "little brother");
        String expectedWarning = "The value 'little brother' for the '\u1984' property will be ignored as it cannot be overridden "
                + "thanks to newly-introduced federal legislation. "
                + "The value 'big brother' will be used instead.";
        assertEquals(
                Optional.of(expectedWarning),
                ConnectUtils.ensurePropertyAndGetWarning(
                        props,
                        "\u1984",
                        "big brother",
                        "thanks to newly-introduced federal legislation",
                        false)
        );
        assertEquals(Map.of("\u1984", "big brother"), props);

        props.clear();
        props.put("\u1984", "BIG BROTHER");
        expectedWarning = "The value 'BIG BROTHER' for the '\u1984' property will be ignored as it cannot be overridden "
                + "thanks to newly-introduced federal legislation. "
                + "The value 'big brother' will be used instead.";
        assertEquals(
                Optional.of(expectedWarning),
                ConnectUtils.ensurePropertyAndGetWarning(
                        props,
                        "\u1984",
                        "big brother",
                        "thanks to newly-introduced federal legislation",
                        true)
        );
        assertEquals(Map.of("\u1984", "big brother"), props);
    }

    @Test
    public void testClientIdBase() {
        String groupId = "connect-cluster";
        String userSpecifiedClientId = "worker-57";

        String expectedClientIdBase = groupId + "-" + userSpecifiedClientId + "-";
        assertClientIdBase(groupId, userSpecifiedClientId, expectedClientIdBase);

        expectedClientIdBase = groupId + "-";
        assertClientIdBase(groupId, null, expectedClientIdBase);

        expectedClientIdBase = "connect-";
        assertClientIdBase(null, null, expectedClientIdBase);

        expectedClientIdBase = "connect-" + userSpecifiedClientId + "-";
        assertClientIdBase(null, userSpecifiedClientId, expectedClientIdBase);

        expectedClientIdBase = "connect-";
        assertClientIdBase(null, "", expectedClientIdBase);
    }

    private void assertClientIdBase(String groupId, String userSpecifiedClientId, String expectedClientIdBase) {
        WorkerConfig config = mock(WorkerConfig.class);
        when(config.groupId()).thenReturn(groupId);
        when(config.getString(CLIENT_ID_CONFIG)).thenReturn(userSpecifiedClientId);
        String actualClientIdBase = ConnectUtils.clientIdBase(config);
        assertEquals(expectedClientIdBase, actualClientIdBase);
    }

    @Test
    public void testPatchConfig() {
        HashMap<String, String> config = new HashMap<>();
        config.put("unaffected-key", "unaffected-value");
        config.put("to-be-changed-key", "to-be-changed-value-old");
        config.put("to-be-deleted-key", "to-be-deleted-value");

        HashMap<String, String> patch = new HashMap<>();
        patch.put("to-be-changed-key", "to-be-changed-value-new");
        patch.put("to-be-deleted-key", null);
        patch.put("to-be-added-key", "to-be-added-value");

        HashMap<String, String> expectedResult = new HashMap<>();
        expectedResult.put("unaffected-key", "unaffected-value");
        expectedResult.put("to-be-changed-key", "to-be-changed-value-new");
        expectedResult.put("to-be-added-key", "to-be-added-value");

        Map<String, String> result = ConnectUtils.patchConfig(config, patch);
        assertEquals(expectedResult, result);
    }
}
