/*
 * 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.internal.cache.wan.misc;

import static org.apache.geode.distributed.ConfigurationProperties.*;
import static org.apache.geode.test.dunit.Assert.*;

import java.util.Properties;
import java.util.concurrent.TimeUnit;

import org.awaitility.Awaitility;
import org.apache.geode.security.TestSecurityManager;
import org.apache.logging.log4j.Logger;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.cache.wan.WANTestBase;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.security.AuthInitialize;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.SecurityTestUtils;
import org.apache.geode.security.generator.CredentialGenerator;
import org.apache.geode.security.generator.DummyCredentialGenerator;
import org.apache.geode.security.templates.UserPasswordAuthInit;
import org.apache.geode.test.junit.categories.DistributedTest;

@Category(DistributedTest.class)
public class NewWanAuthenticationDUnitTest extends WANTestBase {

  public static final Logger logger = LogService.getLogger();

  public static boolean isDifferentServerInGetCredentialCall = false;

  /**
   * Authentication test for new WAN with valid credentials. Although, nothing related to
   * authentication has been changed in new WAN, this test case is added on request from QA for
   * defect 44650.
   */
  @Test
  public void testWanAuthValidCredentials() {
    Integer lnPort = (Integer) vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    logger.info("Created locator on local site");

    Integer nyPort = (Integer) vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));
    logger.info("Created locator on remote site");


    CredentialGenerator gen = new DummyCredentialGenerator();
    Properties extraProps = gen.getSystemProperties();

    String clientauthenticator = gen.getAuthenticator();
    String clientauthInit = gen.getAuthInit();

    Properties credentials1 = gen.getValidCredentials(1);
    if (extraProps != null) {
      credentials1.putAll(extraProps);
    }
    Properties javaProps1 = gen.getJavaProperties();

    // vm3's invalid credentials
    Properties credentials2 = gen.getInvalidCredentials(1);
    if (extraProps != null) {
      credentials2.putAll(extraProps);
    }
    Properties javaProps2 = gen.getJavaProperties();

    Properties props1 =
        buildProperties(clientauthenticator, clientauthInit, null, credentials1, null);

    // have vm 3 start a cache with invalid credentails
    Properties props2 =
        buildProperties(clientauthenticator, clientauthInit, null, credentials2, null);

    vm2.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props1, javaProps1, lnPort));
    logger.info("Created secured cache in vm2");

    vm3.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props2, javaProps2, nyPort));
    logger.info("Created secured cache in vm3");

    vm2.invoke(() -> WANTestBase.createSender("ln", 2, false, 100, 10, false, false, null, true));
    logger.info("Created sender in vm2");

    vm3.invoke(() -> createReceiverInSecuredCache());
    logger.info("Created receiver in vm3");

    vm2.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", "ln", isOffHeap()));
    logger.info("Created RR in vm2");
    vm3.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", null, isOffHeap()));
    logger.info("Created RR in vm3");

    // this tests verifies that even though vm3 has invalid credentials, vm2 can still send data to
    // vm3 because
    // vm2 has valid credentials
    vm2.invoke(() -> WANTestBase.startSender("ln"));
    vm2.invoke(() -> WANTestBase.waitForSenderRunningState("ln"));

    vm2.invoke(() -> WANTestBase.doPuts(getTestMethodName() + "_RR", 1));
    vm3.invoke(() -> {
      Region r = cache.getRegion(Region.SEPARATOR + getTestMethodName() + "_RR");
      Awaitility.waitAtMost(20, TimeUnit.SECONDS).until(() -> assertTrue(r.size() > 0));
    });
    logger.info("Done successfully.");
  }

  @Test
  public void testWanIntegratedSecurityWithValidCredentials() {
    Integer lnPort = (Integer) vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    logger.info("Created locator on local site");

    Integer nyPort = (Integer) vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));
    logger.info("Created locator on remote site");


    Properties props1 = buildSecurityProperties("admin", "secret");
    Properties props2 = buildSecurityProperties("guest", "guest");

    vm2.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props1, null, lnPort));
    logger.info("Created secured cache in vm2");

    vm3.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props2, null, nyPort));
    logger.info("Created secured cache in vm3");

    vm2.invoke(() -> WANTestBase.createSender("ln", 2, false, 100, 10, false, false, null, true));
    logger.info("Created sender in vm2");

    vm3.invoke(() -> createReceiverInSecuredCache());
    logger.info("Created receiver in vm3");

    vm2.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", "ln", isOffHeap()));
    logger.info("Created RR in vm2");
    vm3.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", null, isOffHeap()));
    logger.info("Created RR in vm3");

    vm2.invoke(() -> WANTestBase.startSender("ln"));
    vm2.invoke(() -> WANTestBase.waitForSenderRunningState("ln"));
    vm2.invoke(() -> WANTestBase.doPuts(getTestMethodName() + "_RR", 1));
    vm3.invoke(() -> {
      Region r = cache.getRegion(Region.SEPARATOR + getTestMethodName() + "_RR");
      Awaitility.waitAtMost(20, TimeUnit.SECONDS).until(() -> assertTrue(r.size() > 0));

    });
    logger.info("Done successfully.");
  }

  /**
   * Test authentication with new WAN with invalid credentials. Although, nothing related to
   * authentication has been changed in new WAN, this test case is added on request from QA for
   * defect 44650.
   */
  @Test
  public void testWanAuthInvalidCredentials() {
    Integer lnPort = (Integer) vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    logger.info("Created locator on local site");

    Integer nyPort = (Integer) vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));
    logger.info("Created locator on remote site");


    CredentialGenerator gen = new DummyCredentialGenerator();
    logger.info("Picked up credential: " + gen);

    Properties extraProps = gen.getSystemProperties();

    String clientauthenticator = gen.getAuthenticator();
    String clientauthInit = gen.getAuthInit();

    Properties credentials1 = gen.getInvalidCredentials(1);
    if (extraProps != null) {
      credentials1.putAll(extraProps);
    }
    Properties javaProps1 = gen.getJavaProperties();
    Properties credentials2 = gen.getInvalidCredentials(2);
    if (extraProps != null) {
      credentials2.putAll(extraProps);
    }
    Properties javaProps2 = gen.getJavaProperties();

    Properties props1 =
        buildProperties(clientauthenticator, clientauthInit, null, credentials1, null);
    Properties props2 =
        buildProperties(clientauthenticator, clientauthInit, null, credentials2, null);

    logger.info("Done building auth properties");

    vm2.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props1, javaProps1, lnPort));
    logger.info("Created secured cache in vm2");

    vm3.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props2, javaProps2, nyPort));
    logger.info("Created secured cache in vm3");

    vm2.invoke(() -> WANTestBase.createSender("ln", 2, false, 100, 10, false, false, null, true));
    logger.info("Created sender in vm2");

    vm3.invoke(() -> createReceiverInSecuredCache());
    logger.info("Created receiver in vm3");

    vm2.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", "ln", isOffHeap()));
    logger.info("Created RR in vm2");
    vm3.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", null, isOffHeap()));
    logger.info("Created RR in vm3");

    try {
      vm2.invoke(() -> WANTestBase.startSender("ln"));
      fail(
          "Authentication Failed: While starting the sender, an exception should have been thrown");
    } catch (Exception e) {
      if (!(e.getCause().getCause() instanceof AuthenticationFailedException)) {
        fail("Authentication is not working as expected", e);
      }
    }
  }

  /**
   * Test authentication with new WAN with invalid credentials. Although, nothing related to
   * authentication has been changed in new WAN, this test case is added on request from QA for
   * defect 44650.
   */
  @Test
  public void testWanSecurityManagerWithInvalidCredentials() {
    Integer lnPort = (Integer) vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    logger.info("Created locator on local site");

    Integer nyPort = (Integer) vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));
    logger.info("Created locator on remote site");

    Properties props1 = buildSecurityProperties("admin", "wrongPswd");
    Properties props2 = buildSecurityProperties("guest", "wrongPswd");

    logger.info("Done building auth properties");

    vm2.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props1, null, lnPort));
    logger.info("Created secured cache in vm2");

    vm3.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props2, null, nyPort));
    logger.info("Created secured cache in vm3");

    vm2.invoke(() -> WANTestBase.createSender("ln", 2, false, 100, 10, false, false, null, true));
    logger.info("Created sender in vm2");

    vm3.invoke(() -> createReceiverInSecuredCache());
    logger.info("Created receiver in vm3");

    vm2.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", "ln", isOffHeap()));
    logger.info("Created RR in vm2");
    vm3.invoke(
        () -> WANTestBase.createReplicatedRegion(getTestMethodName() + "_RR", null, isOffHeap()));
    logger.info("Created RR in vm3");

    try {
      vm2.invoke(() -> WANTestBase.startSender("ln"));
      fail(
          "Authentication Failed: While starting the sender, an exception should have been thrown");
    } catch (Exception e) {
      if (!(e.getCause().getCause() instanceof AuthenticationFailedException)) {
        fail("Authentication is not working as expected", e);
      }
    }
  }

  private static Properties buildProperties(String clientauthenticator, String clientAuthInit,
      String accessor, Properties extraAuthProps, Properties extraAuthzProps) {
    Properties authProps = new Properties();
    if (clientauthenticator != null) {
      authProps.setProperty(SECURITY_CLIENT_AUTHENTICATOR, clientauthenticator);
    }
    if (accessor != null) {
      authProps.setProperty(SECURITY_CLIENT_ACCESSOR, accessor);
    }
    if (clientAuthInit != null) {
      authProps.setProperty(SECURITY_CLIENT_AUTH_INIT, clientAuthInit);
    }
    if (extraAuthProps != null) {
      authProps.putAll(extraAuthProps);
    }
    if (extraAuthzProps != null) {
      authProps.putAll(extraAuthzProps);
    }
    return authProps;
  }

  private static Properties buildSecurityProperties(String username, String password) {
    Properties props = new Properties();
    props.put(SECURITY_MANAGER, TestSecurityManager.class.getName());
    props.put("security-json", "org/apache/geode/security/templates/security.json");
    props.put(SECURITY_CLIENT_AUTH_INIT, UserPasswdAI.class.getName());
    props.put("security-username", username);
    props.put("security-password", password);
    return props;
  }

  public static void createSecuredCache(Properties authProps, Object javaProps, Integer locPort) {
    authProps.setProperty(MCAST_PORT, "0");
    authProps.setProperty(LOCATORS, "localhost[" + locPort + "]");

    logger.info("Set the server properties to: " + authProps);
    logger.info("Set the java properties to: " + javaProps);

    SecurityTestUtils tmpInstance = new SecurityTestUtils("temp");
    DistributedSystem ds = tmpInstance.createSystem(authProps, (Properties) javaProps);
    assertNotNull(ds);
    assertTrue(ds.isConnected());
    cache = CacheFactory.create(ds);
    assertNotNull(cache);
  }

  public static class UserPasswdAI extends UserPasswordAuthInit {

    public static AuthInitialize createAI() {
      return new UserPasswdAI();
    }

    @Override
    public Properties getCredentials(Properties props, DistributedMember server, boolean isPeer)
        throws AuthenticationFailedException {
      boolean val = (CacheFactory.getAnyInstance().getDistributedSystem().getDistributedMember()
          .getProcessId() != server.getProcessId());
      Assert.assertTrue(val, "getCredentials: Server should be different");
      Properties p = super.getCredentials(props, server, isPeer);
      if (val) {
        isDifferentServerInGetCredentialCall = true;
        CacheFactory.getAnyInstance().getLoggerI18n().convertToLogWriter()
            .config("setting  isDifferentServerInGetCredentialCall "
                + isDifferentServerInGetCredentialCall);
      } else {
        CacheFactory.getAnyInstance().getLoggerI18n().convertToLogWriter()
            .config("setting22  isDifferentServerInGetCredentialCall "
                + isDifferentServerInGetCredentialCall);
      }
      return p;
    }
  }

  public static void verifyDifferentServerInGetCredentialCall() {
    Assert.assertTrue(isDifferentServerInGetCredentialCall,
        "verifyDifferentServerInGetCredentialCall: Server should be different");
    isDifferentServerInGetCredentialCall = false;
  }

  @Test
  public void testWanAuthValidCredentialsWithServer() {
    disconnectAllFromDS();
    {
      Integer lnPort = (Integer) vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
      logger.info("Created locator on local site");

      Integer nyPort = (Integer) vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));
      logger.info("Created locator on remote site");

      DummyCredentialGenerator gen = new DummyCredentialGenerator();
      gen.init();
      Properties extraProps = gen.getSystemProperties();

      String clientauthenticator = gen.getAuthenticator();
      String clientauthInit = UserPasswdAI.class.getName() + ".createAI";

      Properties credentials1 = gen.getValidCredentials(1);
      if (extraProps != null) {
        credentials1.putAll(extraProps);
      }
      Properties javaProps1 = gen.getJavaProperties();

      Properties credentials2 = gen.getInvalidCredentials(2);
      if (extraProps != null) {
        credentials2.putAll(extraProps);
      }
      Properties javaProps2 = gen.getJavaProperties();

      Properties props1 =
          buildProperties(clientauthenticator, clientauthInit, null, credentials1, null);
      Properties props2 =
          buildProperties(clientauthenticator, clientauthInit, null, credentials2, null);

      vm2.invoke(
          () -> NewWanAuthenticationDUnitTest.createSecuredCache(props1, javaProps1, lnPort));
      logger.info("Created secured cache in vm2");

      vm3.invoke(
          () -> NewWanAuthenticationDUnitTest.createSecuredCache(props2, javaProps2, nyPort));
      logger.info("Created secured cache in vm3");

      vm2.invoke(() -> WANTestBase.createSender("ln", 2, false, 100, 10, false, false, null, true));
      logger.info("Created sender in vm2");

      vm3.invoke(() -> createReceiverInSecuredCache());
      logger.info("Created receiver in vm3");

      vm2.invoke(() -> WANTestBase.startSender("ln"));
      vm2.invoke(() -> WANTestBase.waitForSenderRunningState("ln"));

      vm2.invoke(() -> verifyDifferentServerInGetCredentialCall());
      vm3.invoke(() -> verifyDifferentServerInGetCredentialCall());
    }
  }

  @Test
  public void testWanSecurityManagerAuthValidCredentialsWithServer() {
    disconnectAllFromDS();
    {
      Integer lnPort = (Integer) vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
      logger.info("Created locator on local site");

      Integer nyPort = (Integer) vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));
      logger.info("Created locator on remote site");

      Properties props1 = buildSecurityProperties("admin", "secret");
      Properties props2 = buildSecurityProperties("guest", "guest");

      vm2.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props1, null, lnPort));
      logger.info("Created secured cache in vm2");

      vm3.invoke(() -> NewWanAuthenticationDUnitTest.createSecuredCache(props2, null, nyPort));
      logger.info("Created secured cache in vm3");

      vm2.invoke(() -> WANTestBase.createSender("ln", 2, false, 100, 10, false, false, null, true));
      logger.info("Created sender in vm2");

      vm3.invoke(() -> createReceiverInSecuredCache());
      logger.info("Created receiver in vm3");

      vm2.invoke(() -> WANTestBase.startSender("ln"));
      vm2.invoke(() -> WANTestBase.waitForSenderRunningState("ln"));

      vm2.invoke(() -> verifyDifferentServerInGetCredentialCall());

      // this would fail for now because for integrated security, we are not sending the receiver's
      // credentials back
      // to the sender. Because in the old security implementation, even though the receiver's
      // credentials are sent back to the sender
      // the sender is not checking it.
      // vm3.invoke(() -> verifyDifferentServerInGetCredentialCall());
    }
  }
}
