/*
 * 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.hadoop.hbase;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.apache.hadoop.hbase.testclassification.ClientTests;
import org.apache.hadoop.hbase.testclassification.SmallTests;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag(ClientTests.TAG)
@Tag(SmallTests.TAG)
public class TestRegionLocations {

  ServerName sn0 = ServerName.valueOf("host0", 10, 10);
  ServerName sn1 = ServerName.valueOf("host1", 10, 10);
  ServerName sn2 = ServerName.valueOf("host2", 10, 10);
  ServerName sn3 = ServerName.valueOf("host3", 10, 10);

  HRegionInfo info0 = hri(0);
  HRegionInfo info1 = hri(1);
  HRegionInfo info2 = hri(2);
  HRegionInfo info9 = hri(9);

  long regionId1 = 1000;
  long regionId2 = 2000;

  @Test
  public void testSizeMethods() {
    RegionLocations list = new RegionLocations();
    assertTrue(list.isEmpty());
    assertEquals(0, list.size());
    assertEquals(0, list.numNonNullElements());

    list = hrll((HRegionLocation) null);
    assertTrue(list.isEmpty());
    assertEquals(1, list.size());
    assertEquals(0, list.numNonNullElements());

    HRegionInfo info0 = hri(0);
    list = hrll(hrl(info0, null));
    assertTrue(list.isEmpty());
    assertEquals(1, list.size());
    assertEquals(0, list.numNonNullElements());

    HRegionInfo info9 = hri(9);
    list = hrll(hrl(info9, null));
    assertTrue(list.isEmpty());
    assertEquals(10, list.size());
    assertEquals(0, list.numNonNullElements());

    list = hrll(hrl(info0, null), hrl(info9, null));
    assertTrue(list.isEmpty());
    assertEquals(10, list.size());
    assertEquals(0, list.numNonNullElements());
  }

  private HRegionInfo hri(int replicaId) {
    return hri(regionId1, replicaId);
  }

  private HRegionInfo hri(long regionId, int replicaId) {
    TableName table = TableName.valueOf("table");
    byte[] startKey = HConstants.EMPTY_START_ROW;
    byte[] endKey = HConstants.EMPTY_END_ROW;
    HRegionInfo info = new HRegionInfo(table, startKey, endKey, false, regionId, replicaId);
    return info;
  }

  private HRegionLocation hrl(HRegionInfo hri, ServerName sn) {
    return new HRegionLocation(hri, sn);
  }

  private HRegionLocation hrl(HRegionInfo hri, ServerName sn, long seqNum) {
    return new HRegionLocation(hri, sn, seqNum);
  }

  private RegionLocations hrll(HRegionLocation... locations) {
    return new RegionLocations(locations);
  }

  @Test
  public void testRemoveByServer() {
    RegionLocations list;

    // test remove from empty list
    list = new RegionLocations();
    assertTrue(list == list.removeByServer(sn0));

    // test remove from single element list
    list = hrll(hrl(info0, sn0));
    assertTrue(list == list.removeByServer(sn1));
    list = list.removeByServer(sn0);
    assertEquals(0, list.numNonNullElements());

    // test remove from multi element list
    list = hrll(hrl(info0, sn0), hrl(info1, sn1), hrl(info2, sn2), hrl(info9, sn2));
    assertTrue(list == list.removeByServer(sn3)); // no region is mapped to sn3
    list = list.removeByServer(sn0);
    assertNull(list.getRegionLocation(0));
    assertEquals(sn1, list.getRegionLocation(1).getServerName());
    assertEquals(sn2, list.getRegionLocation(2).getServerName());
    assertNull(list.getRegionLocation(5));
    assertEquals(sn2, list.getRegionLocation(9).getServerName());

    // test multi-element remove from multi element list
    list = hrll(hrl(info0, sn1), hrl(info1, sn1), hrl(info2, sn0), hrl(info9, sn0));
    list = list.removeByServer(sn0);
    assertEquals(sn1, list.getRegionLocation(0).getServerName());
    assertEquals(sn1, list.getRegionLocation(1).getServerName());
    assertNull(list.getRegionLocation(2));
    assertNull(list.getRegionLocation(5));
    assertNull(list.getRegionLocation(9));
  }

  @Test
  public void testRemove() {
    RegionLocations list;

    // test remove from empty list
    list = new RegionLocations();
    assertTrue(list == list.remove(hrl(info0, sn0)));

    // test remove from single element list
    list = hrll(hrl(info0, sn0));
    assertTrue(list == list.remove(hrl(info0, sn1)));
    list = list.remove(hrl(info0, sn0));
    assertTrue(list.isEmpty());

    // test remove from multi element list
    list = hrll(hrl(info0, sn0), hrl(info1, sn1), hrl(info2, sn2), hrl(info9, sn2));
    assertTrue(list == list.remove(hrl(info1, sn3))); // no region is mapped to sn3
    list = list.remove(hrl(info0, sn0));
    assertNull(list.getRegionLocation(0));
    assertEquals(sn1, list.getRegionLocation(1).getServerName());
    assertEquals(sn2, list.getRegionLocation(2).getServerName());
    assertNull(list.getRegionLocation(5));
    assertEquals(sn2, list.getRegionLocation(9).getServerName());

    list = list.remove(hrl(info9, sn2));
    assertNull(list.getRegionLocation(0));
    assertEquals(sn1, list.getRegionLocation(1).getServerName());
    assertEquals(sn2, list.getRegionLocation(2).getServerName());
    assertNull(list.getRegionLocation(5));
    assertNull(list.getRegionLocation(9));

    // test multi-element remove from multi element list
    list = hrll(hrl(info0, sn1), hrl(info1, sn1), hrl(info2, sn0), hrl(info9, sn0));
    list = list.remove(hrl(info9, sn0));
    assertEquals(sn1, list.getRegionLocation(0).getServerName());
    assertEquals(sn1, list.getRegionLocation(1).getServerName());
    assertEquals(sn0, list.getRegionLocation(2).getServerName());
    assertNull(list.getRegionLocation(5));
    assertNull(list.getRegionLocation(9));
  }

  @Test
  public void testUpdateLocation() {
    RegionLocations list;

    // test add to empty list
    list = new RegionLocations();
    list = list.updateLocation(hrl(info0, sn1), false, false);
    assertEquals(sn1, list.getRegionLocation(0).getServerName());

    // test add to non-empty list
    list = list.updateLocation(hrl(info9, sn3, 10), false, false);
    assertEquals(sn3, list.getRegionLocation(9).getServerName());
    assertEquals(10, list.size());
    list = list.updateLocation(hrl(info2, sn2, 10), false, false);
    assertEquals(sn2, list.getRegionLocation(2).getServerName());
    assertEquals(10, list.size());

    // test update greater SeqNum
    list = list.updateLocation(hrl(info2, sn3, 11), false, false);
    assertEquals(sn3, list.getRegionLocation(2).getServerName());
    assertEquals(sn3, list.getRegionLocation(9).getServerName());

    // test update equal SeqNum
    list = list.updateLocation(hrl(info2, sn1, 11), false, false); // should not update
    assertEquals(sn3, list.getRegionLocation(2).getServerName());
    assertEquals(sn3, list.getRegionLocation(9).getServerName());
    list = list.updateLocation(hrl(info2, sn1, 11), true, false); // should update
    assertEquals(sn1, list.getRegionLocation(2).getServerName());
    assertEquals(sn3, list.getRegionLocation(9).getServerName());

    // test force update
    list = list.updateLocation(hrl(info2, sn2, 9), false, true); // should update
    assertEquals(sn2, list.getRegionLocation(2).getServerName());
    assertEquals(sn3, list.getRegionLocation(9).getServerName());
  }

  @Test
  public void testMergeLocations() {
    RegionLocations list1, list2;

    // test merge empty lists
    list1 = new RegionLocations();
    list2 = new RegionLocations();

    assertTrue(list1 == list1.mergeLocations(list2));

    // test merge non-empty and empty
    list2 = hrll(hrl(info0, sn0));
    list1 = list1.mergeLocations(list2);
    assertEquals(sn0, list1.getRegionLocation(0).getServerName());

    // test merge empty and non empty
    list1 = hrll();
    list1 = list2.mergeLocations(list1);
    assertEquals(sn0, list1.getRegionLocation(0).getServerName());

    // test merge non intersecting
    list1 = hrll(hrl(info0, sn0), hrl(info1, sn1));
    list2 = hrll(hrl(info2, sn2));
    list1 = list2.mergeLocations(list1);
    assertEquals(sn0, list1.getRegionLocation(0).getServerName());
    assertEquals(sn1, list1.getRegionLocation(1).getServerName());
    assertEquals(2, list1.size()); // the size is taken from the argument list to merge

    // do the other way merge as well
    list1 = hrll(hrl(info0, sn0), hrl(info1, sn1));
    list2 = hrll(hrl(info2, sn2));
    list1 = list1.mergeLocations(list2);
    assertEquals(sn0, list1.getRegionLocation(0).getServerName());
    assertEquals(sn1, list1.getRegionLocation(1).getServerName());
    assertEquals(sn2, list1.getRegionLocation(2).getServerName());

    // test intersecting lists same seqNum
    list1 = hrll(hrl(info0, sn0), hrl(info1, sn1));
    list2 = hrll(hrl(info0, sn2), hrl(info1, sn2), hrl(info9, sn3));
    list1 = list2.mergeLocations(list1); // list1 should override
    assertEquals(2, list1.size());
    assertEquals(sn0, list1.getRegionLocation(0).getServerName());
    assertEquals(sn1, list1.getRegionLocation(1).getServerName());

    // do the other way
    list1 = hrll(hrl(info0, sn0), hrl(info1, sn1));
    list2 = hrll(hrl(info0, sn2), hrl(info1, sn2), hrl(info9, sn3));
    list1 = list1.mergeLocations(list2); // list2 should override
    assertEquals(10, list1.size());
    assertEquals(sn2, list1.getRegionLocation(0).getServerName());
    assertEquals(sn2, list1.getRegionLocation(1).getServerName());
    assertEquals(sn3, list1.getRegionLocation(9).getServerName());

    // test intersecting lists different seqNum
    list1 = hrll(hrl(info0, sn0, 10), hrl(info1, sn1, 10));
    list2 = hrll(hrl(info0, sn2, 11), hrl(info1, sn2, 11), hrl(info9, sn3, 11));
    list1 = list1.mergeLocations(list2); // list2 should override because of seqNum
    assertEquals(10, list1.size());
    assertEquals(sn2, list1.getRegionLocation(0).getServerName());
    assertEquals(sn2, list1.getRegionLocation(1).getServerName());
    assertEquals(sn3, list1.getRegionLocation(9).getServerName());

    // do the other way
    list1 = hrll(hrl(info0, sn0, 10), hrl(info1, sn1, 10));
    list2 = hrll(hrl(info0, sn2, 11), hrl(info1, sn2, 11), hrl(info9, sn3, 11));
    list1 = list1.mergeLocations(list2); // list2 should override
    assertEquals(10, list1.size());
    assertEquals(sn2, list1.getRegionLocation(0).getServerName());
    assertEquals(sn2, list1.getRegionLocation(1).getServerName());
    assertEquals(sn3, list1.getRegionLocation(9).getServerName());
  }

  @Test
  public void testMergeLocationsWithDifferentRegionId() {
    RegionLocations list1, list2;

    // test merging two lists. But the list2 contains region replicas with a different region id
    HRegionInfo info0 = hri(regionId1, 0);
    HRegionInfo info1 = hri(regionId1, 1);
    HRegionInfo info2 = hri(regionId2, 2);

    list1 = hrll(hrl(info2, sn1));
    list2 = hrll(hrl(info0, sn2), hrl(info1, sn2));
    list1 = list2.mergeLocations(list1);
    assertNull(list1.getRegionLocation(0));
    assertNull(list1.getRegionLocation(1));
    assertNotNull(list1.getRegionLocation(2));
    assertEquals(sn1, list1.getRegionLocation(2).getServerName());
    assertEquals(3, list1.size());

    // try the other way merge
    list1 = hrll(hrl(info2, sn1));
    list2 = hrll(hrl(info0, sn2), hrl(info1, sn2));
    list2 = list1.mergeLocations(list2);
    assertNotNull(list2.getRegionLocation(0));
    assertNotNull(list2.getRegionLocation(1));
    assertNull(list2.getRegionLocation(2));
  }

  @Test
  public void testUpdateLocationWithDifferentRegionId() {
    RegionLocations list;

    HRegionInfo info0 = hri(regionId1, 0);
    HRegionInfo info1 = hri(regionId2, 1);
    HRegionInfo info2 = hri(regionId1, 2);

    list = new RegionLocations(hrl(info0, sn1), hrl(info2, sn1));

    list = list.updateLocation(hrl(info1, sn2), false, true); // force update

    // the other locations should be removed now
    assertNull(list.getRegionLocation(0));
    assertNotNull(list.getRegionLocation(1));
    assertNull(list.getRegionLocation(2));
    assertEquals(sn2, list.getRegionLocation(1).getServerName());
    assertEquals(3, list.size());
  }

  @Test
  public void testConstructWithNullElements() {
    // RegionLocations can contain null elements as well. These null elements can

    RegionLocations list = new RegionLocations((HRegionLocation) null);
    assertTrue(list.isEmpty());
    assertEquals(1, list.size());
    assertEquals(0, list.numNonNullElements());

    list = new RegionLocations(null, hrl(info1, sn0));
    assertFalse(list.isEmpty());
    assertEquals(2, list.size());
    assertEquals(1, list.numNonNullElements());

    list = new RegionLocations(hrl(info0, sn0), null);
    assertEquals(2, list.size());
    assertEquals(1, list.numNonNullElements());

    list = new RegionLocations(null, hrl(info2, sn0), null, hrl(info9, sn0));
    assertEquals(10, list.size());
    assertEquals(2, list.numNonNullElements());

    list = new RegionLocations(null, hrl(info2, sn0), null, hrl(info9, sn0), null);
    assertEquals(11, list.size());
    assertEquals(2, list.numNonNullElements());

    list = new RegionLocations(null, hrl(info2, sn0), null, hrl(info9, sn0), null, null);
    assertEquals(12, list.size());
    assertEquals(2, list.numNonNullElements());
  }
}
