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

import org.apache.kafka.common.utils.Exit;

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Optional;
import java.util.Properties;

import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class CommandLineUtilsTest {
    @Test
    public void testParseEmptyArg() {
        List<String> argArray = List.of("my.empty.property=");

        assertThrows(IllegalArgumentException.class, () -> CommandLineUtils.parseKeyValueArgs(argArray, false));
    }

    @Test
    public void testParseEmptyArgWithNoDelimiter() {
        List<String> argArray = List.of("my.empty.property");

        assertThrows(IllegalArgumentException.class, () -> CommandLineUtils.parseKeyValueArgs(argArray, false));
    }

    @Test
    public void testParseEmptyArgAsValid() {
        List<String> argArray = List.of("my.empty.property=", "my.empty.property1");
        Properties props = CommandLineUtils.parseKeyValueArgs(argArray);

        assertEquals("", props.getProperty("my.empty.property"), "Value of a key with missing value should be an empty string");
        assertEquals("", props.getProperty("my.empty.property1"), "Value of a key with missing value with no delimiter should be an empty string");
    }

    @Test
    public void testParseSingleArg() {
        List<String> argArray = List.of("my.property=value");
        Properties props = CommandLineUtils.parseKeyValueArgs(argArray);

        assertEquals("value", props.getProperty("my.property"), "Value of a single property should be 'value'");
    }

    @Test
    public void testParseArgs() {
        List<String> argArray = List.of("first.property=first", "second.property=second");
        Properties props = CommandLineUtils.parseKeyValueArgs(argArray);

        assertEquals("first", props.getProperty("first.property"), "Value of first property should be 'first'");
        assertEquals("second", props.getProperty("second.property"), "Value of second property should be 'second'");
    }

    @Test
    public void testParseArgsWithMultipleDelimiters() {
        List<String> argArray = List.of("first.property==first", "second.property=second=", "third.property=thi=rd");
        Properties props = CommandLineUtils.parseKeyValueArgs(argArray);

        assertEquals("=first", props.getProperty("first.property"), "Value of first property should be '=first'");
        assertEquals("second=", props.getProperty("second.property"), "Value of second property should be 'second='");
        assertEquals("thi=rd", props.getProperty("third.property"), "Value of third property should be 'thi=rd'");
    }

    Properties props = new Properties();
    OptionParser parser = new OptionParser(false);
    OptionSpec<String> stringOpt;
    OptionSpec<Integer> intOpt;
    OptionSpec<String> stringOptOptionalArg;
    OptionSpec<Integer> intOptOptionalArg;
    OptionSpec<String> stringOptOptionalArgNoDefault;
    OptionSpec<Integer> intOptOptionalArgNoDefault;

    private void setUpOptions() {
        stringOpt = parser.accepts("str")
                .withRequiredArg()
                .ofType(String.class)
                .defaultsTo("default-string");
        intOpt = parser.accepts("int")
                .withRequiredArg()
                .ofType(Integer.class)
                .defaultsTo(100);
        stringOptOptionalArg = parser.accepts("str-opt")
                .withOptionalArg()
                .ofType(String.class)
                .defaultsTo("default-string-2");
        intOptOptionalArg = parser.accepts("int-opt")
                .withOptionalArg()
                .ofType(Integer.class)
                .defaultsTo(200);
        stringOptOptionalArgNoDefault = parser.accepts("str-opt-nodef")
                .withOptionalArg()
                .ofType(String.class);
        intOptOptionalArgNoDefault = parser.accepts("int-opt-nodef")
                .withOptionalArg()
                .ofType(Integer.class);
    }

    @Test
    public void testMaybeMergeOptionsOverwriteExisting() {
        setUpOptions();

        props.put("skey", "existing-string");
        props.put("ikey", "300");
        props.put("sokey", "existing-string-2");
        props.put("iokey", "400");
        props.put("sondkey", "existing-string-3");
        props.put("iondkey", "500");

        OptionSet options = parser.parse(
                "--str", "some-string",
                "--int", "600",
                "--str-opt", "some-string-2",
                "--int-opt", "700",
                "--str-opt-nodef", "some-string-3",
                "--int-opt-nodef", "800"
        );

        CommandLineUtils.maybeMergeOptions(props, "skey", options, stringOpt);
        CommandLineUtils.maybeMergeOptions(props, "ikey", options, intOpt);
        CommandLineUtils.maybeMergeOptions(props, "sokey", options, stringOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "iokey", options, intOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "sondkey", options, stringOptOptionalArgNoDefault);
        CommandLineUtils.maybeMergeOptions(props, "iondkey", options, intOptOptionalArgNoDefault);

        assertEquals("some-string", props.get("skey"));
        assertEquals("600", props.get("ikey"));
        assertEquals("some-string-2", props.get("sokey"));
        assertEquals("700", props.get("iokey"));
        assertEquals("some-string-3", props.get("sondkey"));
        assertEquals("800", props.get("iondkey"));
    }

    @Test
    public void testMaybeMergeOptionsDefaultOverwriteExisting() {
        setUpOptions();

        props.put("sokey", "existing-string");
        props.put("iokey", "300");
        props.put("sondkey", "existing-string-2");
        props.put("iondkey", "400");

        OptionSet options = parser.parse(
                "--str-opt",
                "--int-opt",
                "--str-opt-nodef",
                "--int-opt-nodef"
        );

        CommandLineUtils.maybeMergeOptions(props, "sokey", options, stringOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "iokey", options, intOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "sondkey", options, stringOptOptionalArgNoDefault);
        CommandLineUtils.maybeMergeOptions(props, "iondkey", options, intOptOptionalArgNoDefault);

        assertEquals("default-string-2", props.get("sokey"));
        assertEquals("200", props.get("iokey"));
        assertNull(props.get("sondkey"));
        assertNull(props.get("iondkey"));
    }

    @Test
    public void testMaybeMergeOptionsDefaultValueIfNotExist() {
        setUpOptions();

        OptionSet options = parser.parse();

        CommandLineUtils.maybeMergeOptions(props, "skey", options, stringOpt);
        CommandLineUtils.maybeMergeOptions(props, "ikey", options, intOpt);
        CommandLineUtils.maybeMergeOptions(props, "sokey", options, stringOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "iokey", options, intOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "sondkey", options, stringOptOptionalArgNoDefault);
        CommandLineUtils.maybeMergeOptions(props, "iondkey", options, intOptOptionalArgNoDefault);

        assertEquals("default-string", props.get("skey"));
        assertEquals("100", props.get("ikey"));
        assertEquals("default-string-2", props.get("sokey"));
        assertEquals("200", props.get("iokey"));
        assertNull(props.get("sondkey"));
        assertNull(props.get("iondkey"));
    }

    @Test
    public void testMaybeMergeOptionsNotOverwriteExisting() {
        setUpOptions();

        props.put("skey", "existing-string");
        props.put("ikey", "300");
        props.put("sokey", "existing-string-2");
        props.put("iokey", "400");
        props.put("sondkey", "existing-string-3");
        props.put("iondkey", "500");

        OptionSet options = parser.parse();

        CommandLineUtils.maybeMergeOptions(props, "skey", options, stringOpt);
        CommandLineUtils.maybeMergeOptions(props, "ikey", options, intOpt);
        CommandLineUtils.maybeMergeOptions(props, "sokey", options, stringOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "iokey", options, intOptOptionalArg);
        CommandLineUtils.maybeMergeOptions(props, "sondkey", options, stringOptOptionalArgNoDefault);
        CommandLineUtils.maybeMergeOptions(props, "iondkey", options, intOptOptionalArgNoDefault);

        assertEquals("existing-string", props.get("skey"));
        assertEquals("300", props.get("ikey"));
        assertEquals("existing-string-2", props.get("sokey"));
        assertEquals("400", props.get("iokey"));
        assertEquals("existing-string-3", props.get("sondkey"));
        assertEquals("500", props.get("iondkey"));
    }

    private static Properties createTestProps() {
        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "this");
        props.setProperty("bootstrap.controllers", "that");
        return props;
    }

    @Test
    public void testInitializeBootstrapPropertiesWithNoBootstraps() {
        assertEquals("You must specify either --bootstrap-controller or --bootstrap-server.",
            assertThrows(CommandLineUtils.InitializeBootstrapException.class,
                () -> CommandLineUtils.initializeBootstrapProperties(createTestProps(),
                    Optional.empty(), Optional.empty())).getMessage());
    }

    @Test
    public void testInitializeBootstrapPropertiesWithBrokerBootstrap() {
        Properties props = createTestProps();
        CommandLineUtils.initializeBootstrapProperties(props,
            Optional.of("127.0.0.2:9094"), Optional.empty());
        assertEquals("127.0.0.2:9094", props.getProperty("bootstrap.servers"));
        assertNull(props.getProperty("bootstrap.controllers"));
    }

    @Test
    public void testInitializeBootstrapPropertiesWithControllerBootstrap() {
        Properties props = createTestProps();
        CommandLineUtils.initializeBootstrapProperties(props,
            Optional.empty(), Optional.of("127.0.0.2:9094"));
        assertNull(props.getProperty("bootstrap.servers"));
        assertEquals("127.0.0.2:9094", props.getProperty("bootstrap.controllers"));
    }

    @Test
    public void testInitializeBootstrapPropertiesWithBothBootstraps() {
        assertEquals("You cannot specify both --bootstrap-controller and --bootstrap-server.",
            assertThrows(CommandLineUtils.InitializeBootstrapException.class,
                () -> CommandLineUtils.initializeBootstrapProperties(createTestProps(),
                    Optional.of("127.0.0.2:9094"), Optional.of("127.0.0.3:9095"))).getMessage());
    }

    @SuppressWarnings("unchecked")
    private OptionSpec<String> createMockOptionSpec(String name) {
        OptionSpec<String> spec = mock(OptionSpec.class);
        when(spec.toString()).thenReturn("[" + name.replaceAll("--", "") + "]");
        return spec;
    }

    @Test
    void testCheckOneOfArgsNoOptions() {
        OptionParser parser = mock(OptionParser.class);
        OptionSet options = mock(OptionSet.class);

        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () ->
                CommandLineUtils.checkOneOfArgs(parser, options)
        );

        assertEquals("At least one option must be provided", e.getMessage());
    }

    @Test
    void testCheckOneOfArgsOnePresent() {
        OptionParser parser = mock(OptionParser.class);
        OptionSet options = mock(OptionSet.class);
        OptionSpec<String> opt1 = createMockOptionSpec("--first-option");
        OptionSpec<String> opt2 = createMockOptionSpec("--second-option");
        OptionSpec<String> opt3 = createMockOptionSpec("--third-option");

        when(options.has(opt1)).thenReturn(true);
        when(options.has(opt2)).thenReturn(false);
        when(options.has(opt3)).thenReturn(false);

        assertDoesNotThrow(() ->
                CommandLineUtils.checkOneOfArgs(parser, options, opt1, opt2, opt3)
        );

        when(options.has(opt1)).thenReturn(false);
        when(options.has(opt2)).thenReturn(true);

        assertDoesNotThrow(() ->
                CommandLineUtils.checkOneOfArgs(parser, options, opt1, opt2, opt3)
        );

        when(options.has(opt2)).thenReturn(false);
        when(options.has(opt3)).thenReturn(true);

        assertDoesNotThrow(() ->
                CommandLineUtils.checkOneOfArgs(parser, options, opt1, opt2, opt3)
        );
    }

    @Test
    void testCheckOneOfArgsNonePresent() {
        Exit.setExitProcedure((code, message) -> {
            throw new IllegalArgumentException(message);
        });

        OptionParser parser = mock(OptionParser.class);
        OptionSet options = mock(OptionSet.class);
        OptionSpec<String> opt1 = createMockOptionSpec("--first-option");
        OptionSpec<String> opt2 = createMockOptionSpec("--second-option");
        OptionSpec<String> opt3 = createMockOptionSpec("--third-option");

        when(options.has(opt1)).thenReturn(false);
        when(options.has(opt2)).thenReturn(false);
        when(options.has(opt3)).thenReturn(false);

        try {
            IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
                    () -> CommandLineUtils.checkOneOfArgs(parser, options, opt1, opt2, opt3));
            assertEquals("Exactly one of the following arguments is required: " +
                    "[first-option], [second-option], [third-option]", e.getMessage());
        } finally {
            Exit.resetExitProcedure();
        }
    }

    @Test
    void testCheckOneOfArgsMultiplePresent() {
        Exit.setExitProcedure((code, message) -> {
            throw new IllegalArgumentException(message);
        });

        OptionParser parser = mock(OptionParser.class);
        OptionSet options = mock(OptionSet.class);
        OptionSpec<String> opt1 = createMockOptionSpec("--first-option");
        OptionSpec<String> opt2 = createMockOptionSpec("--second-option");
        OptionSpec<String> opt3 = createMockOptionSpec("--third-option");

        when(options.has(opt1)).thenReturn(true);
        when(options.has(opt2)).thenReturn(true);
        when(options.has(opt3)).thenReturn(false);

        try {
            IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
                    () -> CommandLineUtils.checkOneOfArgs(parser, options, opt1, opt2, opt3));
            assertEquals("Exactly one of the following arguments is required: " +
                    "[first-option], [second-option], [third-option]", e.getMessage());
        } finally {
            Exit.resetExitProcedure();
        }
    }
}
