/*
 * 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.logging.log4j;

import static org.junit.Assert.*;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DurableClientAttributes;
import org.apache.geode.distributed.Role;
import org.apache.geode.internal.admin.Alert;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.test.junit.categories.UnitTest;

/**
 * Tests the AlertAppender.
 */
@Category(UnitTest.class)
public class AlertAppenderJUnitTest {

  private final List<DistributedMember> members = new ArrayList<DistributedMember>();
  private Level previousLogLevel;

  @Before
  public void setUp() {
    this.previousLogLevel = LogService.getBaseLogLevel();
  }

  @After
  public void tearDown() {
    LogService.setBaseLogLevel(this.previousLogLevel);
    if (!this.members.isEmpty()) {
      for (DistributedMember member : this.members) {
        AlertAppender.getInstance().removeAlertListener(member);
      }
      this.members.clear();
    }
  }

  private DistributedMember createTestDistributedMember(String name) {
    return new TestDistributedMember(name);
  }

  /**
   * Verify that adding/removing/replacing listeners works correctly.
   */
  @Test
  public void testListenerHandling() throws Exception {
    DistributedMember member1 = createTestDistributedMember("Member1");
    DistributedMember member2 = createTestDistributedMember("Member2");
    DistributedMember member3 = createTestDistributedMember("Member3");
    DistributedMember member4 = createTestDistributedMember("Member4");
    DistributedMember member5 = createTestDistributedMember("Member5");
    DistributedMember member6 = createTestDistributedMember("Member6");

    LogService.setBaseLogLevel(Level.WARN);

    AlertAppender.getInstance().addAlertListener(member1, Alert.SEVERE);
    AlertAppender.getInstance().addAlertListener(member2, Alert.WARNING);
    AlertAppender.getInstance().addAlertListener(member3, Alert.ERROR);
    AlertAppender.getInstance().addAlertListener(member4, Alert.ERROR);
    AlertAppender.getInstance().addAlertListener(member5, Alert.WARNING);
    AlertAppender.getInstance().addAlertListener(member6, Alert.SEVERE);

    Field listenersField = AlertAppender.getInstance().getClass().getDeclaredField("listeners");
    listenersField.setAccessible(true);

    @SuppressWarnings("unchecked")
    final CopyOnWriteArrayList<AlertAppender.Listener> listeners =
        (CopyOnWriteArrayList<AlertAppender.Listener>) listenersField
            .get(AlertAppender.getInstance());

    // Verify add
    assertSame(member5, listeners.get(0).getMember());
    assertSame(member2, listeners.get(1).getMember());
    assertSame(member4, listeners.get(2).getMember());
    assertSame(member3, listeners.get(3).getMember());
    assertSame(member6, listeners.get(4).getMember());
    assertSame(member1, listeners.get(5).getMember());
    assertSame(6, listeners.size());

    // Verify replace with same level
    AlertAppender.getInstance().addAlertListener(member5, Alert.WARNING);
    assertSame(member5, listeners.get(0).getMember());
    assertSame(member2, listeners.get(1).getMember());
    assertSame(member4, listeners.get(2).getMember());
    assertSame(member3, listeners.get(3).getMember());
    assertSame(member6, listeners.get(4).getMember());
    assertSame(member1, listeners.get(5).getMember());
    assertSame(6, listeners.size());

    // Verify replace with difference level
    AlertAppender.getInstance().addAlertListener(member5, Alert.SEVERE);
    assertSame(member2, listeners.get(0).getMember());
    assertSame(member4, listeners.get(1).getMember());
    assertSame(member3, listeners.get(2).getMember());
    assertSame(member5, listeners.get(3).getMember());
    assertSame(member6, listeners.get(4).getMember());
    assertSame(member1, listeners.get(5).getMember());
    assertSame(6, listeners.size());

    // Verify remove
    assertTrue(AlertAppender.getInstance().removeAlertListener(member3));
    assertSame(member2, listeners.get(0).getMember());
    assertSame(member4, listeners.get(1).getMember());
    assertSame(member5, listeners.get(2).getMember());
    assertSame(member6, listeners.get(3).getMember());
    assertSame(member1, listeners.get(4).getMember());
    assertSame(5, listeners.size());

    assertTrue(AlertAppender.getInstance().removeAlertListener(member1));
    assertTrue(AlertAppender.getInstance().removeAlertListener(member2));
    assertFalse(AlertAppender.getInstance().removeAlertListener(member3));
    assertTrue(AlertAppender.getInstance().removeAlertListener(member4));
    assertTrue(AlertAppender.getInstance().removeAlertListener(member5));
    assertTrue(AlertAppender.getInstance().removeAlertListener(member6));
  }

  /**
   * Verifies that the appender is correctly added and removed from the Log4j configuration and that
   * when the configuration is changed the appender is still there.
   */
  @Test
  public void testAppenderToConfigHandling() throws Exception {
    LogService.setBaseLogLevel(Level.WARN);

    final String appenderName = AlertAppender.getInstance().getName();
    final AppenderContext appenderContext = LogService.getAppenderContext();

    LoggerConfig loggerConfig = appenderContext.getLoggerConfig();

    // Find out home many appenders exist before we get started
    final int startingSize = loggerConfig.getAppenders().size();

    // Add a listener and verify that the appender was added to log4j
    DistributedMember member1 = createTestDistributedMember("Member1");
    AlertAppender.getInstance().addAlertListener(member1, Alert.SEVERE);
    assertEquals(loggerConfig.getAppenders().values().toString(), startingSize + 1,
        loggerConfig.getAppenders().size());
    assertTrue(loggerConfig.getAppenders().containsKey(appenderName));

    // Add another listener and verify that there's still only 1 alert appender
    DistributedMember member2 = createTestDistributedMember("Member1");
    AlertAppender.getInstance().addAlertListener(member2, Alert.SEVERE);
    assertEquals(startingSize + 1, loggerConfig.getAppenders().size());

    // Modify the config and verify that the appender still exists
    assertEquals(Level.WARN, LogService.getLogger(LogService.BASE_LOGGER_NAME).getLevel());

    LogService.setBaseLogLevel(Level.INFO);

    assertEquals(Level.INFO, LogService.getLogger(LogService.BASE_LOGGER_NAME).getLevel());
    loggerConfig = appenderContext.getLoggerConfig();
    assertEquals(startingSize + 1, loggerConfig.getAppenders().size());
    assertTrue(loggerConfig.getAppenders().containsKey(appenderName));

    // Remove the listeners and verify that the appender was removed from log4j
    assertTrue(AlertAppender.getInstance().removeAlertListener(member2));
    assertFalse(AlertAppender.getInstance().removeAlertListener(member1));
    assertEquals(startingSize, loggerConfig.getAppenders().size());
    assertFalse(loggerConfig.getAppenders().containsKey(appenderName));
  }

  private static class TestDistributedMember implements DistributedMember {
    private final String name;

    public TestDistributedMember(final String name) {
      this.name = name;
    }

    @Override
    public String getName() {
      return this.name;
    }

    @Override
    public String getHost() {
      return "";
    }

    @Override
    public Set<Role> getRoles() {
      return null;
    }

    @Override
    public int getProcessId() {
      return 0;
    }

    @Override
    public String getId() {
      return this.name;
    }

    @Override
    public int compareTo(DistributedMember other) {
      return getName().compareTo(other.getName());
    }

    @Override
    public DurableClientAttributes getDurableClientAttributes() {
      return null;
    }

    @Override
    public List<String> getGroups() {
      return Collections.emptyList();
    }

    @Override
    public boolean equals(Object obj) {
      return compareTo((TestDistributedMember) obj) == 0;
    }

    @Override
    public int hashCode() {
      return getHost().hashCode();
    }

    @Override
    public String toString() {
      return "TestDistributedMember [name=" + this.name + "]";
    }
  }
}
