/*
 * 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.tools.nodetool;

import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Suite;

import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.repair.autorepair.AutoRepairConfig;
import org.apache.cassandra.tools.NodeProbe;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.Assert.fail;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * Unit tests for {@link org.apache.cassandra.tools.nodetool.SetAutoRepairConfig}
 */
@RunWith(Suite.class)
@Suite.SuiteClasses({ SetAutoRepairConfigTest.NoParamTests.class, SetAutoRepairConfigTest.RepairTypeParamTests.class,
                      SetAutoRepairConfigTest.RepairTypeAndArgsParamsTests.class })
public class SetAutoRepairConfigTest
{
    protected static SetAutoRepairConfig cmd;

    public static void before(NodeProbe probeMock, PrintStream outMock)
    {
        when(probeMock.isAutoRepairDisabled()).thenReturn(false);
        cmd = new SetAutoRepairConfig();
        cmd.out = outMock;
    }

    public static class NoParamTests
    {
        @Mock
        private static NodeProbe probe;

        @Mock
        private static PrintStream out;

        @Before
        public void setUp()
        {
            MockitoAnnotations.initMocks(this);
            before(probe, out);
        }

        @Test
        public void testHistoryDeleteHostsClearBufferInSec()
        {
            cmd.args = ImmutableList.of("history_clear_delete_hosts_buffer_interval", "1s");

            cmd.execute(probe);

            verify(probe, times(1)).setAutoRepairHistoryClearDeleteHostsBufferDuration("1s");

            // test scenario when auto repair is disabled
            when(probe.isAutoRepairDisabled()).thenReturn(true);

            cmd.execute(probe);

            // test new calls are not made when auto repair is disabled
            verify(probe, times(1)).setAutoRepairHistoryClearDeleteHostsBufferDuration("1s");
        }


        @Test
        public void testMinRepairDuration()
        {
            cmd.args = ImmutableList.of("min_repair_task_duration", "4s");

            cmd.execute(probe);

            verify(probe, times(1)).setAutoRepairMinRepairTaskDuration("4s");
        }

        @Test
        public void testStartScheduler()
        {
            cmd.args = ImmutableList.of("start_scheduler", "false");

            cmd.execute(probe);

            verify(probe, times(0)).startAutoRepairScheduler();

            cmd.args = ImmutableList.of("start_scheduler", "true");

            cmd.execute(probe);

            verify(probe, times(1)).startAutoRepairScheduler();
        }
    }

    @RunWith(Parameterized.class)
    public static class RepairTypeParamTests
    {
        @Mock
        private static NodeProbe probe;

        @Mock
        private static PrintStream out;

        @Parameterized.Parameter
        public AutoRepairConfig.RepairType repairType;

        @Parameterized.Parameters(name = "repairType={0}")
        public static Object[] data()
        {
            return AutoRepairConfig.RepairType.values();
        }

        private static InetAddressAndPort localEndpoint;
        private static InetAddressAndPort otherEndpoint;

        @Before
        public void setUp() throws Exception
        {
            MockitoAnnotations.initMocks(this);
            before(probe, out);
            localEndpoint = InetAddressAndPort.getByName("127.0.0.1:7000");
            otherEndpoint = localEndpoint.withPort(localEndpoint.getPort() + 1);
        }

        @Test(expected = IllegalArgumentException.class)
        public void testNoArgs()
        {
            cmd.repairTypeStr = repairType.name();
            cmd.execute(probe);
        }

        @Test
        public void testRepairSchedulingDisabled()
        {
            when(probe.isAutoRepairDisabled()).thenReturn(true);
            cmd.repairTypeStr = repairType.name();
            cmd.args = ImmutableList.of("threads", "1");

            cmd.execute(probe);

            verify(out, times(1)).println("Auto-repair is not enabled");
            verify(probe, times(0)).setAutoRepairThreads(repairType.name(), 1);
        }

        @Test
        public void testRepairTypeDisabled()
        {
            cmd.repairTypeStr = repairType.name();
            cmd.args = ImmutableList.of("number_of_repair_threads", "1");

            cmd.execute(probe);

            verify(probe, times(1)).setAutoRepairThreads(repairType.name(), 1);
        }

        @Test
        public void testV2FlagMissing()
        {
            cmd.repairTypeStr = repairType.name();
            cmd.args = ImmutableList.of("threads", "1");

            try
            {
                cmd.execute(probe);

                fail("expected IllegalArgumentException");
            }
            catch (IllegalArgumentException e)
            {
                // expected
            }

            verify(probe, times(0)).setAutoRepairThreads(repairType.name(), 0);
        }

        @Test(expected = IllegalArgumentException.class)
        public void testInvalidParamType()
        {
            cmd.repairTypeStr = repairType.name();
            cmd.args = ImmutableList.of("unknown_type", "1");

            cmd.execute(probe);
        }

        @Test
        public void testPriorityHosts()
        {
            String commaSeparatedHostSet = String.join(",", localEndpoint.toString().substring(1), otherEndpoint.toString().substring(1));
            cmd.repairTypeStr = repairType.name();
            cmd.args = ImmutableList.of("priority_hosts", commaSeparatedHostSet);

            cmd.execute(probe);

            verify(probe, times(1)).setAutoRepairPriorityForHosts(repairType.name(), commaSeparatedHostSet);
        }

        @Test
        public void testForceRepairHosts()
        {
            String commaSeparatedHostSet = String.join(",", localEndpoint.toString().substring(1), otherEndpoint.toString().substring(1));
            cmd.repairTypeStr = repairType.name();
            cmd.args = ImmutableList.of("forcerepair_hosts", commaSeparatedHostSet);

            cmd.execute(probe);

            verify(probe, times(1)).setAutoRepairForceRepairForHosts(repairType.name(), commaSeparatedHostSet);
        }
    }

    @RunWith(Parameterized.class)
    public static class RepairTypeAndArgsParamsTests
    {
        @Parameterized.Parameter
        public AutoRepairConfig.RepairType repairType;

        @Parameterized.Parameter(1)
        public String paramType;

        @Parameterized.Parameter(2)
        public String paramVal;

        @Parameterized.Parameter(3)
        public Consumer<AutoRepairConfig.RepairType> verifyFunc;

        @Parameterized.Parameters(name = "repairType={0},paramType={1}")
        public static Collection<Object[]> testCases()
        {
            return Stream.of(
            forEachRepairType("enabled", "true", (type) -> verify(probe, times(1)).setAutoRepairEnabled(type.name(), true)),
            forEachRepairType("number_of_repair_threads", "1", (type) -> verify(probe, times(1)).setAutoRepairThreads(type.name(), 1)),
            forEachRepairType("min_repair_interval", "3h", (type) -> verify(probe, times(1)).setAutoRepairMinInterval(type.name(), "3h")),
            forEachRepairType("sstable_upper_threshold", "4", (type) -> verify(probe, times(1)).setAutoRepairSSTableCountHigherThreshold(type.name(), 4)),
            forEachRepairType("table_max_repair_time", "5s", (type) -> verify(probe, times(1)).setAutoRepairTableMaxRepairTime(type.name(), "5s")),
            forEachRepairType("repair_primary_token_range_only", "true", (type) -> verify(probe, times(1)).setAutoRepairPrimaryTokenRangeOnly(type.name(), true)),
            forEachRepairType("parallel_repair_count", "6", (type) -> verify(probe, times(1)).setAutoRepairParallelRepairCount(type.name(), 6)),
            forEachRepairType("parallel_repair_percentage", "7", (type) -> verify(probe, times(1)).setAutoRepairParallelRepairPercentage(type.name(), 7)),
            forEachRepairType("materialized_view_repair_enabled", "true", (type) -> verify(probe, times(1)).setAutoRepairMaterializedViewRepairEnabled(type.name(), true)),
            forEachRepairType("ignore_dcs", "dc1,dc2", (type) -> verify(probe, times(1)).setAutoRepairIgnoreDCs(type.name(), ImmutableSet.of("dc1", "dc2"))),
            forEachRepairType("token_range_splitter.max_bytes_per_schedule", "500GiB", (type) -> verify(probe, times(1)).setAutoRepairTokenRangeSplitterParameter(type.name(), "max_bytes_per_schedule", "500GiB")),
            forEachRepairType("repair_max_retries", "3", (type) -> verify(probe, times(1)).setAutoRepairMaxRetriesCount(type.name(), 3)),
            forEachRepairType("repair_retry_backoff", "60s", (type) -> verify(probe, times(1)).setAutoRepairRetryBackoff(type.name(), "60s")),
            forEachRepairType("mixed_major_version_repair_enabled", "false", (type) -> verify(probe, times(1)).setMixedMajorVersionRepairEnabled(false))
            ).flatMap(Function.identity()).collect(Collectors.toList());
        }

        private static Stream<Object[]> forEachRepairType(String paramType, String paramVal, Consumer<AutoRepairConfig.RepairType> verifyFunc)
        {
            Object[][] testCases = new Object[AutoRepairConfig.RepairType.values().length][4];
            for (AutoRepairConfig.RepairType repairType : AutoRepairConfig.RepairType.values())
            {
                testCases[repairType.ordinal()] = new Object[]{ repairType, paramType, paramVal, verifyFunc };
            }

            return Arrays.stream(testCases);
        }

        @Mock
        private static NodeProbe probe;

        @Mock
        private static PrintStream out;

        @Before
        public void setUp()
        {
            MockitoAnnotations.initMocks(this);
            before(probe, out);
        }

        @Test
        public void test()
        {
            cmd.repairTypeStr = repairType.name();
            cmd.args = ImmutableList.of(paramType, paramVal);

            cmd.execute(probe);

            verifyFunc.accept(repairType);

            // test scenario when auto repair is disabled
            when(probe.isAutoRepairDisabled()).thenReturn(true);

            cmd.execute(probe);

            // test new calls are not made when auto repair is disabled
            verifyFunc.accept(repairType);
        }
    }
}
