/*
 * 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.repair.autorepair;

import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

import com.google.common.collect.ImmutableSet;

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

import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.DurationSpec;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.marshal.UUIDType;
import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType;
import org.apache.cassandra.repair.autorepair.AutoRepairUtils.AutoRepairHistory;
import org.apache.cassandra.repair.autorepair.AutoRepairUtils.CurrentRepairStatus;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.SystemDistributedKeyspace;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.membership.NodeAddresses;
import org.apache.cassandra.tcm.membership.NodeVersion;
import org.apache.cassandra.utils.CassandraVersion;
import org.apache.cassandra.utils.FBUtilities;

import static org.apache.cassandra.Util.setAutoRepairEnabled;
import static org.apache.cassandra.config.CassandraRelevantProperties.SYSTEM_DISTRIBUTED_DEFAULT_RF;
import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.COL_DELETE_HOSTS;
import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.COL_FORCE_REPAIR;
import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.COL_REPAIR_FINISH_TS;
import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.COL_REPAIR_PRIORITY;
import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.COL_REPAIR_START_TS;
import static org.apache.cassandra.repair.autorepair.AutoRepairUtils.COL_REPAIR_TURN;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

/**
 * Unit tests for {@link org.apache.cassandra.repair.autorepair.AutoRepairUtils}
 */
public class AutoRepairUtilsTest extends CQLTester
{
    static final RepairType repairType = RepairType.INCREMENTAL;
    static UUID hostId;

    static InetAddressAndPort localEndpoint;

    @BeforeClass
    public static void setupClass() throws Exception
    {
        SYSTEM_DISTRIBUTED_DEFAULT_RF.setInt(1);
        setAutoRepairEnabled(true);
        requireNetwork();
        localEndpoint = FBUtilities.getBroadcastAddressAndPort();
        hostId = StorageService.instance.getHostIdForEndpoint(localEndpoint);
        StorageService.instance.doAutoRepairSetup();
    }

    @Before
    public void setup()
    {
        SYSTEM_DISTRIBUTED_DEFAULT_RF.setInt(1);
        QueryProcessor.executeInternal(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", "ks"));
        QueryProcessor.executeInternal(String.format("CREATE TABLE %s.%s (k text, s text static, i int, v text, primary key(k,i))", "ks", "tbl"));

        AutoRepair.SLEEP_IF_REPAIR_FINISHES_QUICKLY = new DurationSpec.IntSecondsBound("0s");
        QueryProcessor.executeInternal(String.format(
        "TRUNCATE %s.%s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY));
        QueryProcessor.executeInternal(String.format(
        "TRUNCATE %s.%s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY));
    }

    @Test
    public void testSetForceRepair()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id, force_repair) VALUES ('%s', %s, false)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));

        AutoRepairUtils.setForceRepair(repairType, ImmutableSet.of(localEndpoint));

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT force_repair FROM %s.%s WHERE repair_type = '%s' AND host_id = %s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        assertNotNull(result);
        assertEquals(1, result.size());
        assertTrue(result.one().getBoolean(COL_FORCE_REPAIR));
    }

    @Test
    public void testSetForceRepairNewNode()
    {
        AutoRepairUtils.setForceRepairNewNode(repairType);

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT force_repair FROM %s.%s WHERE repair_type = '%s' AND host_id = %s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        assertNotNull(result);
        assertEquals(1, result.size());
        assertTrue(result.one().getBoolean(COL_FORCE_REPAIR));
    }

    @Test
    public void testClearDeleteHosts()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id, delete_hosts, delete_hosts_update_time) VALUES ('%s', %s, { %s }, toTimestamp(now()))",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId, hostId));

        AutoRepairUtils.clearDeleteHosts(repairType, hostId);

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT delete_hosts FROM %s.%s WHERE repair_type = '%s' AND host_id = %s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        assertNotNull(result);
        assertEquals(1, result.size());
        Set<UUID> deleteHosts = result.one().getSet(COL_DELETE_HOSTS, UUIDType.instance);
        assertNull(deleteHosts);
    }

    @Test
    public void testGetAutoRepairHistoryForLocalGroup()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id, force_repair) VALUES ('%s', %s, false)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));

        List<AutoRepairHistory> history = AutoRepairUtils.getAutoRepairHistory(repairType);
        assertNotNull(history);
        assertEquals(1, history.size());
        assertEquals(hostId, history.get(0).hostId);
    }

    @Test
    public void testGetAutoRepairHistoryForLocalGroup_empty_history()
    {
        List<AutoRepairHistory> history = AutoRepairUtils.getAutoRepairHistory(repairType);

        assertNull(history);
    }

    @Test
    public void testGetCurrentRepairStatus()
    {
        UUID forceRepair = UUID.randomUUID();
        UUID regularRepair = UUID.randomUUID();
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id) VALUES ('%s', %s)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id, force_repair, repair_start_ts) VALUES ('%s', %s, true, toTimestamp(now()))",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), forceRepair));
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id, repair_start_ts) VALUES ('%s', %s, toTimestamp(now()))",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), regularRepair));
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, repair_priority) VALUES ('%s', { %s })",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY,
        repairType.toString(), regularRepair));

        CurrentRepairStatus status = AutoRepairUtils.getCurrentRepairStatus(repairType, AutoRepairUtils.getAutoRepairHistory(repairType), hostId);

        assertNotNull(status);
        assertEquals(1, status.historiesWithoutOnGoingRepair.size());
        assertEquals(hostId, status.historiesWithoutOnGoingRepair.get(0).hostId);
        assertEquals(1, status.hostIdsWithOnGoingRepair.size());
        assertTrue(status.hostIdsWithOnGoingRepair.contains(regularRepair));
        assertEquals(1, status.hostIdsWithOnGoingForceRepair.size());
        assertTrue(status.hostIdsWithOnGoingForceRepair.contains(forceRepair));
        assertEquals(1, status.priority.size());
        assertTrue(status.priority.contains(regularRepair));
        assertEquals(hostId, status.myRepairHistory.hostId);
    }

    @Test
    public void testGetHostIdsInCurrentRing()
    {
        TreeSet<UUID> hosts = AutoRepairUtils.getHostIdsInCurrentRing(repairType);

        assertNotNull(hosts);
        assertEquals(1, hosts.size());
        assertTrue(hosts.contains(hostId));
    }

    @Test
    public void testGetHostIdsInCurrentRing_multiple_nodes()
    {
        InetAddressAndPort ignoredEndpoint = localEndpoint.withPort(localEndpoint.getPort() + 1);
        InetAddressAndPort deadEndpoint = localEndpoint.withPort(localEndpoint.getPort() + 2);
        DatabaseDescriptor.getAutoRepairConfig().setIgnoreDCs(repairType, ImmutableSet.of("dc2"));

        TreeSet<UUID> hosts = AutoRepairUtils.getHostIdsInCurrentRing(repairType, ImmutableSet.of(new NodeAddresses(localEndpoint), new NodeAddresses(ignoredEndpoint), new NodeAddresses(deadEndpoint)));

        assertNotNull(hosts);
        assertEquals(1, hosts.size());
        assertTrue(hosts.contains(hostId));
    }

    @Test
    public void testHasMultipleLiveMajorVersionsWithSingleNode()
    {
        boolean result = AutoRepairUtils.hasMultipleLiveMajorVersions();
        assertFalse(result);
    }

    @Test
    public void testHasMultipleLiveMajorVersionsWithMultipleNodesOfSameVersion()
    {
        ClusterMetadataTestHelper.addEndpoint(2);
        // Test the current behavior with the existing cluster setup
        // In a single-node test environment, this should return false
        boolean result = AutoRepairUtils.hasMultipleLiveMajorVersions();
        assertFalse(result);
    }

    @Test
    public void testHasMultipleLiveMajorVersionsWithMultipleNodesOfSameMajorVersionDifferentMinorVersions()
    {
        // add two nodes with the current cassandra major version, but different minor version
        CassandraVersion differentCassandraVersion = new CassandraVersion(
            String.format("%d.%d",
                          NodeVersion.CURRENT.cassandraVersion.major,
                          NodeVersion.CURRENT.cassandraVersion.minor+1));
        ClusterMetadataTestHelper.addEndpoint(2, new NodeVersion(
             differentCassandraVersion,
             NodeVersion.CURRENT_METADATA_VERSION));
        // With the same major versions, but different minor versions, we should still see this function return true
        boolean result = AutoRepairUtils.hasMultipleLiveMajorVersions();
        assertFalse(result);
    }

    @Test
    public void testHasMultipleLiveMajorVersionsWithMultipleNodesOfDifferentMajorVersions()
    {
        // add two nodes with different cassandra major versions
        CassandraVersion differentCassandraVersion = new CassandraVersion(
            String.format("%d.%d", NodeVersion.CURRENT.cassandraVersion.major - 1, 0));
        ClusterMetadataTestHelper.addEndpoint(2, new NodeVersion(differentCassandraVersion,
                                                                 NodeVersion.CURRENT_METADATA_VERSION));
        // With different major versions, we should see this function return true
        boolean result = AutoRepairUtils.hasMultipleLiveMajorVersions();
        assertTrue(result);
    }

    @Test
    public void testGetHostWithLongestUnrepairTime()
    {
        UUID otherHostId = UUID.randomUUID();
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id) VALUES ('%s', %s)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id, repair_finish_ts) VALUES ('%s', %s, toTimestamp(now()))",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), otherHostId));

        AutoRepairHistory history = AutoRepairUtils.getHostWithLongestUnrepairTime(repairType);

        assertEquals(hostId, history.hostId);
    }

    @Test
    public void testGetMaxNumberOfNodeRunAutoRepairInGroup_0_group_size()
    {
        DatabaseDescriptor.getAutoRepairConfig().setParallelRepairCount(repairType, 2);

        int count = AutoRepairUtils.getMaxNumberOfNodeRunAutoRepair(repairType, 0);

        assertEquals(2, count);
    }

    @Test
    public void testGetMaxNumberOfNodeRunAutoRepairInGroup_percentage()
    {
        DatabaseDescriptor.getAutoRepairConfig().setParallelRepairCount(repairType, 2);
        DatabaseDescriptor.getAutoRepairConfig().setParallelRepairPercentage(repairType, 50);


        int count = AutoRepairUtils.getMaxNumberOfNodeRunAutoRepair(repairType, 10);

        assertEquals(5, count);
    }

    @Test
    public void testDeleteAutoRepairHistory()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id) VALUES ('%s', %s)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));

        AutoRepairUtils.deleteAutoRepairHistory(repairType, hostId);

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT * FROM %s.%s WHERE repair_type = '%s' AND host_id = %s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        assertNotNull(result);
        assertEquals(0, result.size());
    }

    @Test
    public void testUpdateStartAutoRepairHistory()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id) VALUES ('%s', %s)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));

        AutoRepairUtils.updateStartAutoRepairHistory(repairType, hostId, 123, AutoRepairUtils.RepairTurn.MY_TURN);

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT repair_start_ts, repair_turn FROM %s.%s WHERE repair_type = '%s' AND host_id = %s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        assertNotNull(result);
        assertEquals(1, result.size());
        UntypedResultSet.Row row = result.one();
        assertEquals(123, row.getLong(COL_REPAIR_START_TS, 0));
        assertEquals(AutoRepairUtils.RepairTurn.MY_TURN.toString(), row.getString(COL_REPAIR_TURN));
    }

    @Test
    public void testUpdateFinishAutoRepairHistory()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id) VALUES ('%s', %s)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));

        AutoRepairUtils.updateFinishAutoRepairHistory(repairType, hostId, 123);

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT repair_finish_ts FROM %s.%s WHERE repair_type = '%s' AND host_id = %s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), hostId));
        assertNotNull(result);
        assertEquals(1, result.size());
        assertEquals(123, result.one().getLong(COL_REPAIR_FINISH_TS, 0));
    }

    @Test
    public void testAddHostIdToDeleteHosts()
    {
        UUID otherHostId = UUID.randomUUID();
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, host_id) VALUES ('%s', %s)",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), otherHostId));

        AutoRepairUtils.addHostIdToDeleteHosts(repairType, hostId, otherHostId);

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT * FROM %s.%s WHERE repair_type = '%s' AND host_id = %s",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_HISTORY,
        repairType.toString(), otherHostId));
        assertNotNull(result);
        assertEquals(1, result.size());
        Set<UUID> deleteHosts = result.one().getSet(COL_DELETE_HOSTS, UUIDType.instance);
        assertNotNull(deleteHosts);
        assertEquals(1, deleteHosts.size());
        assertTrue(deleteHosts.contains(hostId));
    }

    @Test
    public void testAddPriorityHost()
    {
        AutoRepairUtils.addPriorityHosts(repairType, ImmutableSet.of(localEndpoint));

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT * FROM %s.%s WHERE repair_type = '%s'",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY,
        repairType.toString()));
        assertNotNull(result);
        assertEquals(1, result.size());
        Set<UUID> repairPriority = result.one().getSet(COL_REPAIR_PRIORITY, UUIDType.instance);
        assertNotNull(repairPriority);
        assertEquals(1, repairPriority.size());
        assertTrue(repairPriority.contains(hostId));
    }

    @Test
    public void testRemovePriorityStatus()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, repair_priority) VALUES ('%s', { %s })",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY,
        repairType.toString(), hostId));

        AutoRepairUtils.removePriorityStatus(repairType, hostId);

        UntypedResultSet result = QueryProcessor.executeInternal(String.format(
        "SELECT * FROM %s.%s WHERE repair_type = '%s'",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY,
        repairType.toString()));
        assertNotNull(result);
        assertEquals(1, result.size());
        Set<UUID> repairPriority = result.one().getSet(COL_REPAIR_PRIORITY, UUIDType.instance);
        assertNull(repairPriority);
    }

    @Test
    public void testGetPriorityHosts()
    {
        QueryProcessor.executeInternal(String.format(
        "INSERT INTO %s.%s (repair_type, repair_priority) VALUES ('%s', { %s })",
        SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.AUTO_REPAIR_PRIORITY,
        repairType.toString(), hostId));

        Set<InetAddressAndPort> hosts = AutoRepairUtils.getPriorityHosts(repairType);

        assertNotNull(hosts);
        assertEquals(1, hosts.size());
        assertTrue(hosts.contains(localEndpoint));
    }

    @Test
    public void testCheckNodeContainsKeyspaceReplica()
    {
        Keyspace ks = Keyspace.open("ks");

        assertTrue(AutoRepairUtils.shouldConsiderKeyspace(ks));
    }

    @Test
    public void testTableMaxRepairTimeExceeded()
    {
        DatabaseDescriptor.getAutoRepairConfig().setAutoRepairTableMaxRepairTime(repairType, "0s");

        assertTrue(AutoRepairUtils.tableMaxRepairTimeExceeded(repairType, 0));
    }

    @Test
    public void testKeyspaceMaxRepairTimeExceeded()
    {
        DatabaseDescriptor.getAutoRepairConfig().setAutoRepairTableMaxRepairTime(repairType, "0s");

        assertTrue(AutoRepairUtils.keyspaceMaxRepairTimeExceeded(repairType, 0, 1));
    }

    @Test
    public void testGetLastRepairFinishTime()
    {
        AutoRepairHistory history = new AutoRepairHistory(UUID.randomUUID(), "", 0, 0, null, 0, false);

        assertEquals(0, history.getLastRepairFinishTime());

        history.lastRepairFinishTime = 100;

        assertEquals(100, history.getLastRepairFinishTime());
    }

    @Test
    public void testMyTurnToRunRepairShouldReturnMyTurnWhenRepairOngoing()
    {
        UUID myID = UUID.randomUUID();
        UUID otherID = UUID.randomUUID();
        DatabaseDescriptor.getAutoRepairConfig().setParallelRepairCount(repairType, 5);
        long currentMillis = System.currentTimeMillis();
        // finish time less than start time means that repair is ongoing
        AutoRepairUtils.insertNewRepairHistory(repairType, myID, currentMillis, currentMillis - 100);
        // finish time is larger than start time means that repair for other node is finished
        AutoRepairUtils.insertNewRepairHistory(repairType, otherID, currentMillis, currentMillis + 100);

        assertEquals(AutoRepairUtils.RepairTurn.MY_TURN, AutoRepairUtils.myTurnToRunRepair(repairType, myID));
    }

    @Test
    public void testLocalStrategyAndNetworkKeyspace()
    {
        assertFalse(AutoRepairUtils.shouldConsiderKeyspace(Keyspace.open("system")));
        assertTrue(AutoRepairUtils.shouldConsiderKeyspace(Keyspace.open(KEYSPACE)));
    }

    @Test
    public void testGetLastRepairTimeForNode()
    {
        UUID myID = UUID.randomUUID();
        UUID otherID = UUID.randomUUID();
        long currentMillis = System.currentTimeMillis();
        AutoRepairUtils.insertNewRepairHistory(repairType, myID, currentMillis, currentMillis - 100);
        AutoRepairUtils.insertNewRepairHistory(repairType, otherID, currentMillis, currentMillis + 100);

        assertEquals(currentMillis - 100, AutoRepairUtils.getLastRepairFinishTimeForNode(repairType, myID));
    }

    @Test
    public void testGetLastRepairTimeForNodeWhenHistoryIsEmpty()
    {
        UUID myID = UUID.randomUUID();

        assertEquals(0, AutoRepairUtils.getLastRepairFinishTimeForNode(repairType, myID));
    }

    @Test
    public void testGetLastRepairStartTimeForNode()
    {
        UUID myID = UUID.randomUUID();
        UUID otherID = UUID.randomUUID();
        long currentMillis = System.currentTimeMillis();
        AutoRepairUtils.insertNewRepairHistory(repairType, myID, currentMillis, currentMillis - 100);
        AutoRepairUtils.insertNewRepairHistory(repairType, otherID, currentMillis + 50, currentMillis + 100);

        assertEquals(currentMillis, AutoRepairUtils.getLastRepairStartTimeForNode(repairType, myID));
    }

    @Test
    public void testGetLastRepairStartTimeForNodeWhenHistoryIsEmpty()
    {
        UUID myID = UUID.randomUUID();

        assertEquals(0, AutoRepairUtils.getLastRepairStartTimeForNode(repairType, myID));
    }

    @Test
    public void testSkipSystemTraces()
    {
        assertFalse(AutoRepairUtils.shouldConsiderKeyspace(Keyspace.open(SchemaConstants.TRACE_KEYSPACE_NAME)));
    }

    /**
     * Regression test for CASSANDRA-21426: exercises the getMostEligibleHostToRepair code path through
     * hasReplicaWithOngoingRepair -> getRangeAddresses -> calculateNaturalReplicas when another node
     * has an ongoing repair. In cassandra-5.0 this triggered an AssertionError in getTopology() when
     * called on the live TokenMetadata singleton; in trunk (TCM) the code uses ClusterMetadata which
     * is safe, but this test ensures the code path remains exercised.
     */
    @Test
    public void testGetMostEligibleHostToRepairWithOngoingParallelRepair() throws UnknownHostException
    {
        // Register a second endpoint via TCM
        InetAddressAndPort otherEndpoint = InetAddressAndPort.getByName("127.0.0.2");
        ClusterMetadataTestHelper.addEndpoint(otherEndpoint,
                                              new org.apache.cassandra.dht.Murmur3Partitioner.LongToken(1000));
        UUID otherHostId = StorageService.instance.getHostIdForEndpoint(otherEndpoint);
        assertNotNull(otherHostId);

        // Create an NTS keyspace so hasReplicaWithOngoingRepair exercises getRangeAddresses
        QueryProcessor.executeInternal(
        "CREATE KEYSPACE IF NOT EXISTS ks_nts WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': '2'}");

        try
        {
            long currentMillis = System.currentTimeMillis();

            // Build repair histories: other node has ongoing repair, local node is finished
            AutoRepairHistory otherHistory = new AutoRepairHistory(otherHostId, null, currentMillis, currentMillis - 100,
                                                                   null, 0, false);
            AutoRepairHistory myHistory = new AutoRepairHistory(hostId, null, currentMillis - 200, currentMillis - 100,
                                                                null, 0, false);

            List<AutoRepairHistory> allHistories = new ArrayList<>();
            allHistories.add(otherHistory);
            allHistories.add(myHistory);
            CurrentRepairStatus currentRepairStatus = new CurrentRepairStatus(allHistories, null, hostId);

            // Verify preconditions: otherHostId is repairing, local node is not
            assertTrue(currentRepairStatus.hostIdsWithOnGoingRepair.contains(otherHostId));
            assertFalse(currentRepairStatus.hostIdsWithOnGoingRepair.contains(hostId));

            // Exercises: getMostEligibleHostToRepair -> hasReplicaWithOngoingRepair
            //   -> getRangeAddresses(ClusterMetadata.current()) -> calculateNaturalReplicas
            AutoRepairHistory result = AutoRepairUtils.getMostEligibleHostToRepair(repairType, currentRepairStatus, hostId);
            assertNotNull(result);
            assertEquals(hostId, result.hostId);
        }
        finally
        {
            QueryProcessor.executeInternal("DROP KEYSPACE IF EXISTS ks_nts");
            ClusterMetadataTestHelper.removeEndpoint(otherEndpoint, true);
        }
    }
}
