/*
 * 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.geode.distributed;

import static org.apache.geode.distributed.ConfigurationProperties.ENABLE_NETWORK_PARTITION_DETECTION;
import static org.apache.geode.distributed.ConfigurationProperties.GROUPS;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.distributed.ConfigurationProperties.NAME;
import static org.apache.geode.distributed.ConfigurationProperties.ROLES;
import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.apache.geode.test.dunit.Assert.assertEquals;
import static org.apache.geode.test.dunit.Assert.assertFalse;
import static org.apache.geode.test.dunit.Assert.assertNotNull;
import static org.apache.geode.test.dunit.Assert.assertNull;
import static org.apache.geode.test.dunit.Assert.assertTrue;
import static org.apache.geode.test.dunit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.IncompatibleSystemException;
import org.apache.geode.distributed.internal.Distribution;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.DistributionManager;
import org.apache.geode.distributed.internal.HighPriorityAckedMessage;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.distributed.internal.membership.api.MembershipManagerHelper;
import org.apache.geode.distributed.internal.membership.gms.GMSMembership;
import org.apache.geode.internal.HeapDataOutputStream;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.SerializableCallable;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.MembershipTest;

/**
 * Tests the functionality of the {@link DistributedMember} class.
 *
 */
@Category({MembershipTest.class})
public class DistributedMemberDUnitTest extends JUnit4DistributedTestCase {

  /**
   * Tests default settings.
   */
  @Test
  public void testDefaults() {
    Properties config = new Properties();
    config.setProperty(MCAST_PORT, "0");
    config.setProperty(LOCATORS, "");
    config.setProperty(ROLES, "");
    config.setProperty(GROUPS, "");
    config.setProperty(NAME, "");

    InternalDistributedSystem system = getSystem(config);
    try {
      assertTrue(system.getConfig().getRoles().equals(DistributionConfig.DEFAULT_ROLES));
      assertTrue(system.getConfig().getGroups().equals(DistributionConfig.DEFAULT_ROLES));
      assertTrue(system.getConfig().getName().equals(DistributionConfig.DEFAULT_NAME));

      DistributionManager dm = system.getDistributionManager();
      InternalDistributedMember member = dm.getDistributionManagerId();

      Set roles = member.getRoles();
      assertEquals(0, roles.size());
      assertEquals("", member.getName());
      assertEquals(Collections.emptyList(), member.getGroups());
    } finally {
      system.disconnect();
    }
  }

  @Test
  public void testNonDefaultName() {
    Properties config = new Properties();
    config.setProperty(MCAST_PORT, "0");
    config.setProperty(LOCATORS, "");
    config.setProperty(NAME, "nondefault");

    InternalDistributedSystem system = getSystem(config);
    try {
      assertEquals("nondefault", system.getConfig().getName());

      DistributionManager dm = system.getDistributionManager();
      InternalDistributedMember member = dm.getDistributionManagerId();

      assertEquals("nondefault", member.getName());
    } finally {
      system.disconnect();
    }
  }

  /**
   * Tests the configuration of many Roles and groups in one vm. Confirms no runtime distinction
   * between roles and groups.
   */
  @Test
  public void testRolesInOneVM() {
    final String rolesProp = "A,B,C";
    final String groupsProp = "D,E,F,G";
    final List bothList = Arrays.asList("A", "B", "C", "D", "E", "F", "G");

    Properties config = new Properties();
    config.setProperty(MCAST_PORT, "0");
    config.setProperty(LOCATORS, "");
    config.setProperty(ROLES, rolesProp);
    config.setProperty(GROUPS, groupsProp);

    InternalDistributedSystem system = getSystem(config);
    try {
      assertEquals(rolesProp, system.getConfig().getRoles());
      assertEquals(groupsProp, system.getConfig().getGroups());

      DistributionManager dm = system.getDistributionManager();
      InternalDistributedMember member = dm.getDistributionManagerId();

      Set roles = member.getRoles();
      assertEquals(bothList.size(), roles.size());

      for (final Object o : roles) {
        Role role = (Role) o;
        assertTrue(bothList.contains(role.getName()));
      }

      assertEquals(bothList, member.getGroups());
    } finally {
      system.disconnect();
    }
  }

  @Test
  public void testTwoMembersSameName() {
    disconnectFromDS(); // or assertion on # members fails when run-dunit-tests
    Host.getHost(0).getVM(0).invoke(new SerializableRunnable() {
      @Override
      public void run() {
        Properties config = new Properties();
        config.setProperty(NAME, "name0");
        getSystem(config);
      }
    });
    Host.getHost(0).getVM(1).invoke(new SerializableRunnable() {
      @Override
      public void run() {
        Properties config = new Properties();
        config.setProperty(NAME, "name1");
        getSystem(config);
      }
    });
    Host.getHost(0).getVM(2).invoke(new SerializableRunnable() {
      @Override
      public void run() {
        Properties config = new Properties();
        config.setProperty(NAME, "name0");
        try {
          getSystem(config);
          fail("expected IncompatibleSystemException");
        } catch (IncompatibleSystemException ignored) {
        }
      }
    });
  }

  /**
   * Tests the configuration of one unique Role in each of four vms. Verifies that each vm is aware
   * of the other vms' Roles.
   */
  @Test
  public void testRolesInAllVMs() {
    disconnectAllFromDS(); // or assertion on # members fails when run-dunit-tests

    // connect all four vms...
    final String[] vmRoles = new String[] {"VM_A", "VM_B", "VM_C", "VM_D"};
    for (int i = 0; i < vmRoles.length; i++) {
      final int vm = i;
      Host.getHost(0).getVM(vm).invoke(new SerializableRunnable() {
        @Override
        public void run() {
          // disconnectFromDS();
          Properties config = new Properties();
          config.setProperty(ROLES, vmRoles[vm]);
          config.setProperty(ENABLE_NETWORK_PARTITION_DETECTION, "false");
          getSystem(config);
        }
      });
    }

    // validate roles from each vm...
    for (int i = 0; i < vmRoles.length; i++) {
      final int vm = i;
      Host.getHost(0).getVM(vm).invoke(new SerializableRunnable() {
        @Override
        public void run() {
          InternalDistributedSystem sys = getSystem();
          assertNotNull(sys.getConfig().getRoles());
          assertTrue(sys.getConfig().getRoles().equals(vmRoles[vm]));

          DistributionManager dm = sys.getDistributionManager();
          InternalDistributedMember self = dm.getDistributionManagerId();

          Set myRoles = self.getRoles();
          assertEquals(1, myRoles.size());

          Role myRole = (Role) myRoles.iterator().next();
          assertTrue(vmRoles[vm].equals(myRole.getName()));

          await()
              .until(() -> dm.getOtherNormalDistributionManagerIds().size() == 3);
          Set<InternalDistributedMember> members = dm.getOtherNormalDistributionManagerIds();

          for (InternalDistributedMember member : members) {
            Set roles = member.getRoles();
            assertEquals(1, roles.size());
            for (final Object o : roles) {
              Role role = (Role) o;
              assertTrue(!role.getName().equals(myRole.getName()));
              boolean foundRole = false;
              for (final String vmRole : vmRoles) {
                if (vmRole.equals(role.getName())) {
                  foundRole = true;
                  break;
                }
              }
              assertTrue(foundRole);
            }
          }
        }
      });
    }
  }


  private InternalDistributedMember connectAndSetUpPartialID() throws Exception {
    Properties properties = new Properties();
    properties.put("name", "myName");
    InternalDistributedSystem system = getSystem(properties);
    assertTrue(system == basicGetSystem()); // senders will use basicGetSystem()
    InternalDistributedMember internalDistributedMember = system.getDistributedMember();

    internalDistributedMember.setName(null);
    HeapDataOutputStream outputStream = new HeapDataOutputStream(100);
    internalDistributedMember.writeEssentialData(outputStream);
    DataInputStream dataInputStream =
        new DataInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
    InternalDistributedMember partialID =
        InternalDistributedMember.readEssentialData(dataInputStream);
    return partialID;
  }

  /**
   * GEODE-3043 surprise member added when member is already in the cluster
   *
   * This test ensures that a partial ID (with no "name") is equal to its equivalent non-partial ID.
   *
   */
  @Test
  public void testMemberNameIgnoredInPartialID() throws Exception {
    InternalDistributedMember partialID = connectAndSetUpPartialID();

    InternalDistributedMember internalDistributedMember = basicGetSystem().getDistributedMember();
    assertTrue(partialID.isPartial());
    assertFalse(internalDistributedMember.isPartial());

    assertEquals(internalDistributedMember, partialID);
    assertEquals(partialID, internalDistributedMember);
  }

  /**
   * GEODE-3043 surprise member added when member is already in the cluster
   *
   * This test ensures that the membership manager can detect and replace a partial ID with one that
   * is not partial
   *
   */
  @Test
  public void testPartialIDInMessageReplacedWithFullID() throws Exception {
    InternalDistributedMember partialID = connectAndSetUpPartialID();

    HighPriorityAckedMessage message = new HighPriorityAckedMessage();
    message.setSender(partialID);

    Distribution manager =
        MembershipManagerHelper.getDistribution(basicGetSystem());
    ((GMSMembership) manager.getMembership()).replacePartialIdentifierInMessage(message);

    assertFalse(message.getSender().isPartial());
  }

  private static String makeOddEvenString(int vm) {
    return ((vm % 2) == 0) ? "EVENS" : "ODDS";
  }

  private static String makeGroupsString(int vm) {
    return "" + vm + ", " + makeOddEvenString(vm);
  }

  /**
   * Tests the configuration of one unique group in each of four vms. Verifies that each vm is aware
   * of the other vms' groups.
   */
  @Test
  public void testGroupsInAllVMs() {
    disconnectFromDS(); // or assertion on # members fails when run-dunit-tests

    // connect all four vms...
    for (int i = 0; i < 4; i++) {
      final int vm = i;
      Host.getHost(0).getVM(vm).invoke(new SerializableRunnable() {
        @Override
        public void run() {
          // disconnectFromDS();
          Properties config = new Properties();
          config.setProperty(GROUPS, makeGroupsString(vm));
          getSystem(config);
        }
      });
    }

    // validate group from each vm...
    for (int i = 0; i < 4; i++) {
      final int vm = i;
      Host.getHost(0).getVM(vm).invoke(new SerializableRunnable() {
        @Override
        public void run() {
          InternalDistributedSystem sys = getSystem();
          final String expectedMyGroup = makeGroupsString(vm);
          assertEquals(expectedMyGroup, sys.getConfig().getGroups());

          DistributionManager dm = sys.getDistributionManager();
          DistributedMember self = sys.getDistributedMember();

          List<String> myGroups = self.getGroups();

          assertEquals(Arrays.asList("" + vm, makeOddEvenString(vm)), myGroups);

          await()
              .until(() -> dm.getOtherNormalDistributionManagerIds().size() == 3);
          Set<InternalDistributedMember> members = dm.getOtherNormalDistributionManagerIds();

          // Make sure getAllOtherMembers returns a set
          // containing our three peers plus an admin member.
          Set<DistributedMember> others = sys.getAllOtherMembers();
          assertEquals(4, others.size());
          others.removeAll(dm.getOtherNormalDistributionManagerIds());
          assertEquals(1, others.size());
          // test getGroupMembers
          HashSet<DistributedMember> evens = new HashSet<>();
          HashSet<DistributedMember> odds = new HashSet<>();
          boolean isEvens = true;
          for (String groupName : Arrays.asList("0", "1", "2", "3")) {
            Set<DistributedMember> gm = sys.getGroupMembers(groupName);
            if (isEvens) {
              evens.addAll(gm);
            } else {
              odds.addAll(gm);
            }
            isEvens = !isEvens;
            if (groupName.equals("" + vm)) {
              assertEquals(Collections.singleton(self), gm);
            } else {
              assertEquals(1, gm.size());
              assertEquals("members=" + members + " gm=" + gm, true, members.removeAll(gm));
            }
          }
          assertEquals(Collections.emptySet(), members);
          assertEquals(evens, sys.getGroupMembers("EVENS"));
          assertEquals(odds, sys.getGroupMembers("ODDS"));
        }
      });
    }
  }

  /**
   * The method getId() returns a string that is used as a key identifier in the JMX and Admin APIs.
   * This test asserts that it matches the expected format. If you change DistributedMember.getId()
   * or DistributedSystem. getMemberId() you will need to look closely at the format, decide if it
   * is appropriate for JMX as the id for SystemMember mbeans and then adjust this test as needed.
   *
   * Changing the id can result in bad keys in JMX and can result in numerous errors in Admin/JMX
   * tests.
   */
  @Test
  public void testGetId() {
    Properties config = new Properties();
    config.setProperty(MCAST_PORT, "0");
    config.setProperty(LOCATORS, "");
    config.setProperty(NAME, "foobar");

    InternalDistributedSystem system = getSystem(config);
    try {

      DistributionManager dm = system.getDistributionManager();
      DistributedMember member = dm.getDistributionManagerId();

      assertEquals(member.getId(), system.getMemberId());
      assertTrue(member.getId().contains("foobar"));
    } finally {
      system.disconnect();
    }
  }

  @Test
  public void testFindMemberByName() {
    disconnectAllFromDS(); // or assertion on # members fails when run-dunit-tests
    VM vm0 = Host.getHost(0).getVM(0);
    VM vm1 = Host.getHost(0).getVM(1);
    VM vm2 = Host.getHost(0).getVM(2);
    final DistributedMember member0 = createSystemAndGetId(vm0, "name0");
    final DistributedMember member1 = createSystemAndGetId(vm1, "name1");
    final DistributedMember member2 = createSystemAndGetId(vm2, "name2");
    vm0.invoke(new SerializableRunnable() {
      @Override
      public void run() {
        DistributedSystem system = getSystem();
        assertEquals(member0, system.findDistributedMember("name0"));
        assertEquals(member1, system.findDistributedMember("name1"));
        assertEquals(member2, system.findDistributedMember("name2"));
        assertNull(system.findDistributedMember("name3"));

        Set<DistributedMember> members;
        try {
          members = system.findDistributedMembers(InetAddress.getByName(member0.getHost()));
          HashSet expected = new HashSet();
          expected.add(member0);
          expected.add(member1);
          expected.add(member2);

          // Members will contain the locator as well. Just make sure it has
          // the members we're looking for.
          assertTrue("Expected" + expected + " got " + members, members.containsAll(expected));
          assertEquals(4, members.size());
        } catch (UnknownHostException e) {
          fail("Unable to get IpAddress", e);
        }
      }
    });
  }

  private DistributedMember createSystemAndGetId(VM vm, final String name) {
    return (DistributedMember) vm.invoke(new SerializableCallable("create system and get member") {

      @Override
      public Object call() throws Exception {
        Properties config = new Properties();
        config.setProperty(NAME, name);
        DistributedSystem ds = getSystem(config);
        return ds.getDistributedMember();
      }
    });
  }

}
