/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * Licensed 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. See accompanying
 * LICENSE file.
 */
package com.pivotal.gemfirexd.internal.engine.management.impl;

import java.io.Serializable;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;

import org.junit.Test;

import com.gemstone.gemfire.management.internal.MBeanJMXAdapter;
import com.gemstone.gemfire.management.internal.ManagementConstants;
import com.pivotal.gemfirexd.internal.engine.Misc;
import com.pivotal.gemfirexd.internal.engine.management.impl.TableMBeanDataUpdater.MemoryAnalyticsHolder;
import com.pivotal.gemfirexd.internal.engine.store.GemFireStore;

import dunit.Host;
import dunit.SerializableRunnable;
import dunit.VM;
import dunit.DistributedTestCase.WaitCriterion;

@SuppressWarnings("serial")
public class InternalManagementServiceDUnit extends GfxdManagementTestBase {

  private static final String GROUP1_NAME = InternalManagementServiceDUnit.class.getSimpleName() + "_Group1";
  private static final String GROUP2_NAME = InternalManagementServiceDUnit.class.getSimpleName() + "_Group2";
  
  static final String CREATE_TABLE_SQL = "CREATE TABLE APP.BOOKS (" +
                                         "ID VARCHAR(10) NOT NULL, " +
                                         "NAME VARCHAR(25), " +
                                         "TAG VARCHAR(25), " +
                                         "SIZE BIGINT, " +
                                         "LOCATION VARCHAR(25)" +
                                         ", CONSTRAINT BOOK_PK PRIMARY KEY (ID) " +
                                         ")";
  
  static final String DROP_TABLE_SQL   = "DROP TABLE APP.BOOKS;";

  public InternalManagementServiceDUnit(String name) {
    super(name);
  }

  @Test
  public void testGfxdMemberMBeanCreation() throws Exception {
    startServerVMs(1, 0, GROUP1_NAME+","+GROUP2_NAME);
    VM vm = Host.getHost(0).getVM(0);
    String memberNameOrId = (String) vm.invoke(InternalManagementServiceDUnit.class, "verifyMemberMBeanCreation");

    stopVMNum(-1);
    vm.invoke(InternalManagementServiceDUnit.class, "verifyMemberMBeanCleanUp", new Object[] {memberNameOrId});
  }

  static String verifyMemberMBeanCreation() {
    MBeanServer mBeanServer = MBeanJMXAdapter.getMBeanServer();
    String memberNameOrId = MBeanJMXAdapter.getMemberNameOrId(Misc.getDistributedSystem().getDistributedMember());

    ObjectName memberObjectNamePattern = getMemberObjectNamePattern(memberNameOrId);
//    System.out.println("memberObjectNamePattern :: "+memberObjectNamePattern);

    Set<ObjectInstance> queryMBeans = mBeanServer.queryMBeans(memberObjectNamePattern, null);
    assertTrue("Gfxd Member MBean didn't get created.", !queryMBeans.isEmpty());

    return memberNameOrId;
  }

  static void verifyMemberMBeanCleanUp(String memberNameOrId) {
    MBeanServer mBeanServer = MBeanJMXAdapter.getMBeanServer();

    Set<ObjectInstance> queryMBeans = mBeanServer.queryMBeans(getMemberObjectNamePattern(memberNameOrId), null);
    assertTrue("Gfxd Member MBean didn't get unregistered.", queryMBeans.isEmpty());

    queryMBeans = mBeanServer.queryMBeans(getObjectName(ManagementConstants.OBJECTNAME__PREFIX_GFXD + "*"), null);
    assertTrue("All Gfxd MBeans didn't get unregistered.", queryMBeans.isEmpty());
  }
  
  @Test
  public void testMemoryAnalyticsQueryInterval() throws Exception {
    boolean serverStarted = false;
    boolean tableCreated = false;
    invokeInEveryVM(new SerializableRunnable() {
      @Override
      public void run() {
        System.setProperty("gemfirexd.tableAnalyticsUpdateIntervalSeconds",
            "30");
      }
    });
    try {
      startServerVMs(1, 0, GROUP1_NAME+","+GROUP2_NAME);
      serverStarted = true;
      
      final VM vm = Host.getHost(0).getVM(0);
      
      long lastUpdateTime = (Long) vm.invoke(InternalManagementServiceDUnit.class, "getMemoryAnalyticsQueryTime");
      assertFalse("TableMBeanBridge updater is not initialized yet.", -2L == lastUpdateTime);
      assertEquals("Without any updatable TableMBeanBridge lastUpdateTime should be -1.", ManagementConstants.NOT_AVAILABLE_LONG, lastUpdateTime);
      
      int updateRate = (Integer) vm.invoke(InternalManagementServiceDUnit.class, "getUpdateSchedulerRate");
      assertFalse("UpdateScheduler is not initalized.", -2 == updateRate);
      
      serverSQLExecute(1, CREATE_TABLE_SQL);
      tableCreated = true;

      waitForCriterion(new WaitCriterion() {
        public String description() {
          return "Waiting for TableMBeanBridge lastUpdateTime to get updated";
        }

        public boolean done() {
          long returnedLastUpdateTime = (Long) vm.invoke(InternalManagementServiceDUnit.class,"getMemoryAnalyticsQueryTime");
          boolean done = (ManagementConstants.NOT_AVAILABLE_LONG != returnedLastUpdateTime);
          return done;
        }

      }, updateRate * 4, 1000, true);
      
      lastUpdateTime = (Long) vm.invoke(InternalManagementServiceDUnit.class, "getMemoryAnalyticsQueryTime");
      assertFalse("TableMBeanBridge lastUpdateTime should not be -1.", ManagementConstants.NOT_AVAILABLE_LONG == lastUpdateTime);
      
      long latestUpdateTime = lastUpdateTime;
      do {
        sleep(updateRate);
        latestUpdateTime = (Long) vm.invoke(InternalManagementServiceDUnit.class, "getMemoryAnalyticsQueryTime");
      } while(latestUpdateTime == lastUpdateTime);

      long timeBetweenUpdates = latestUpdateTime - lastUpdateTime;
      getLogWriter().info("MemoryAnalyticsQueryInterval:: latestUpdateTime="+latestUpdateTime+",lastUpdateTime="+lastUpdateTime+". Diff(millis): "+timeBetweenUpdates);

      assertTrue("MemoryAnalytics Query is running more frequent than expected.", (timeBetweenUpdates >= MemoryAnalyticsHolder.DEFAULT_MEMANA_QUERY_INTERVAL_SEC));
      
    } finally {
      try {
        if (tableCreated) {
          serverSQLExecute(1, DROP_TABLE_SQL);
        } 
      } finally {
        if (serverStarted) {
          stopVMNum(-1);
        }
        invokeInEveryVM(new SerializableRunnable() {
          @Override
          public void run() {
            System
                .clearProperty("gemfirexd.tableAnalyticsUpdateIntervalSeconds");
          }
        });
      }
    }
  }

  void sleep(long millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
    }
  }
  
  static long getMemoryAnalyticsQueryTime() {
    GemFireStore bootedInstance = GemFireStore.getBootedInstance();
    assertNotNull("GemFireStore is booted instance is null.", bootedInstance);
    
    InternalManagementService internalMS = InternalManagementService.getInstance(bootedInstance);
    return internalMS.getLastMemoryAnalyticsQueryTime();
  }
  
  static int getUpdateSchedulerRate() {
    GemFireStore bootedInstance = GemFireStore.getBootedInstance();
    assertNotNull("GemFireStore is booted instance is null.", bootedInstance);
    
    InternalManagementService internalMS = InternalManagementService.getInstance(bootedInstance);
    return internalMS.getUpdateSchedulerRate();
  }


  public void testUnregisterByPattern() throws Exception {
    startServerVMs(1, 0, GROUP1_NAME+","+GROUP2_NAME);

    VM vm = Host.getHost(0).getVM(0);

    ObjectName emp1 = (ObjectName) vm.invoke(InternalManagementServiceDUnit.class, "addEmployee", new Object[] {"DIV01", "Employee01_01"});
    @SuppressWarnings("unused")
    ObjectName emp2 = (ObjectName) vm.invoke(InternalManagementServiceDUnit.class, "addEmployee", new Object[] {"DIV01", "Employee01_02"});
    @SuppressWarnings("unused")
    ObjectName emp3 = (ObjectName) vm.invoke(InternalManagementServiceDUnit.class, "addEmployee", new Object[] {"DIV02", "Employee02_03"});
    @SuppressWarnings("unused")
    ObjectName emp4 = (ObjectName) vm.invoke(InternalManagementServiceDUnit.class, "addEmployee", new Object[] {"DIV03", "Employee03_04"});

    // failure for re-registration
    Exception expectedException = null;
    try {
      vm.invoke(InternalManagementServiceDUnit.class, "addEmployee", new Object[] {"DIV01", "Employee01_01"});
    } catch (Exception e) {
      expectedException = e;
      getLogWriter().info(e);
    }
    assertNotNull("Expected an exception on re-registration of employee MBean.", expectedException);

    Exception unexpectedException = null;
    try {
      vm.invoke(InternalManagementServiceDUnit.class, "removeEmployee", new Object[] {emp1});
      vm.invoke(InternalManagementServiceDUnit.class, "addEmployee", new Object[] {"DIV01", "Employee01_01"});
    } catch (Exception e) {
      unexpectedException = e;
      getLogWriter().info(e);
    }
    assertNull("Unexpected exception on re-registration of employee MBean.", unexpectedException);

    vm.invoke(InternalManagementServiceDUnit.class, "removeAllEmployees");
    stopVMNum(-1);
  }

  static ObjectName addEmployee(String uid, String employeeId) {
    InternalManagementService ims = InternalManagementService.getInstance(GemFireStore.getBootedInstance());
    assertNotNull("InternalManagementService not found.", ims);

    EmployeeMBean employeeMBean = new EmployeeMBean(uid, employeeId);
    ObjectName registeredMBean = ims.registerMBean(employeeMBean, EmployeeMBean.getEmployeeObjectName(uid, employeeId));

    assertNotNull("registeredMBean is null.", registeredMBean);

    return registeredMBean;
  }

  static void removeEmployee(ObjectName employeeMBeanName) {
    InternalManagementService ims = InternalManagementService.getInstance(GemFireStore.getBootedInstance());
    EmployeeMBean employeeMBean = ims.getMBeanInstance(employeeMBeanName, EmployeeMBean.class);
    assertNotNull("InternalManagementService not found.", ims);;
    ims.unregisterMBean(EmployeeMBean.getEmployeeObjectName(employeeMBean.getDivision(), employeeMBean.getEmployeeId()));
  }

  static void removeAllEmployees() {
    InternalManagementService ims = InternalManagementService.getInstance(GemFireStore.getBootedInstance());
    assertNotNull("InternalManagementService not found.", ims);

    ObjectName employeesPattern = EmployeeMBean.getEmployeeObjectName("*", "*");

    Set<ObjectName> unregisteredMBeans = ims.unregisterMBeanByPattern(employeesPattern);
    assertTrue("No MBeans unregstered", !unregisteredMBeans.isEmpty());
    getLogWriter().info("Unregstered MBeans "+unregisteredMBeans);
  }

  static interface EmployeeMXBean {
    String getDivision();
    String getEmployeeId();
    double calculateBonus(Double performanceIndex);
    void paySalary(boolean withBouns, double bonusPercent);
    void updateSalary(Double salary);
  }

  static class EmployeeMBean implements EmployeeMXBean, Serializable {
    private String employeeId;
    private String division;
    private double salary;

    EmployeeMBean(String division, String employeeId) {
      this.division = division;
      this.employeeId = employeeId;
      this.salary = 1000;
    }

    /**
     * @return the division
     */
    public String getDivision() {
      return division;
    }

    @Override
    public String getEmployeeId() {
      return employeeId;
    }

    @Override
    public double calculateBonus(Double performanceIndex) {
      return this.salary * performanceIndex/100;
    }

    @Override
    public void paySalary(boolean withBouns, double bonusPercent) {
      if (withBouns) {
        System.out.println("Paying " + salary +" with bonus "+ calculateBonus(bonusPercent));
      } else {
        System.out.println("Paying " + salary);
      }
    }

    @Override
    public void updateSalary(Double salary) {
      this.salary = salary.doubleValue();
    }

    static ObjectName getEmployeeObjectName(String uid, String employeeId) {
      return MBeanJMXAdapter.getObjectName(ManagementConstants.OBJECTNAME__PREFIX_GFXD + "type=Employee,uid="+uid+",eid="+employeeId);
    }
  }
}