/*
 * 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.management.internal.cli.functions;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.junit.Before;
import org.junit.Test;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.configuration.RegionConfig;
import org.apache.geode.cache.execute.FunctionContext;
import org.apache.geode.cache.execute.ResultSender;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.IndexCreationException;
import org.apache.geode.cache.query.IndexType;
import org.apache.geode.cache.query.MultiIndexCreationException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.internal.InternalQueryService;
import org.apache.geode.cache.query.internal.index.CompactMapRangeIndex;
import org.apache.geode.cache.query.internal.index.HashIndex;
import org.apache.geode.cache.query.internal.index.PrimaryKeyIndex;
import org.apache.geode.internal.cache.execute.FunctionContextImpl;
import org.apache.geode.test.fake.Fakes;

public class CreateDefinedIndexesFunctionTest {
  private Cache cache;
  private Region region1;
  private Region region2;
  private FunctionContext context;
  private QueryService queryService;
  private TestResultSender resultSender;
  private Set<RegionConfig.Index> indexDefinitions;
  private CreateDefinedIndexesFunction function;

  @Before
  public void setUp() throws Exception {
    cache = Fakes.cache();
    resultSender = new TestResultSender();
    queryService = spy(InternalQueryService.class);
    region1 = Fakes.region("Region1", cache);
    region2 = Fakes.region("Region2", cache);
    function = spy(CreateDefinedIndexesFunction.class);
    doReturn(queryService).when(cache).getQueryService();

    indexDefinitions = new HashSet<>();
    indexDefinitions.add(new RegionConfig.Index() {
      {
        setName("index1");
        setExpression("value1");
        setFromClause("/Region1");
        setType(IndexType.HASH.getName());
      }
    });
    indexDefinitions.add(new RegionConfig.Index() {
      {
        setName("index2");
        setExpression("value2");
        setFromClause("/Region2");
        setType(IndexType.FUNCTIONAL.getName());
      }
    });
    indexDefinitions.add(new RegionConfig.Index() {
      {
        setName("index3");
        setExpression("value3");
        setFromClause("/Region1");
        setType(IndexType.PRIMARY_KEY.getName());
      }
    });
  }

  @Test
  public void noIndexDefinitionsAsFunctionArgument() throws Exception {
    context = new FunctionContextImpl(cache, CreateDefinedIndexesFunction.class.getName(),
        Collections.emptySet(), resultSender);

    function.execute(context);
    List<?> results = resultSender.getResults();

    assertThat(results).isNotNull();
    assertThat(results.size()).isEqualTo(2);
    Object firstResult = results.get(0);
    assertThat(firstResult).isInstanceOf(CliFunctionResult.class);
    assertThat(((CliFunctionResult) firstResult).isSuccessful()).isTrue();
    assertThat(((CliFunctionResult) firstResult).getMessage()).isNotEmpty();
    assertThat(((CliFunctionResult) firstResult).getMessage()).contains("No indexes defined");
  }

  @Test
  public void noIndexPreviouslyDefinedInQueryService() throws Exception {
    when(queryService.createDefinedIndexes()).thenReturn(Collections.emptyList());
    context = new FunctionContextImpl(cache, CreateDefinedIndexesFunction.class.getName(),
        indexDefinitions, resultSender);

    function.execute(context);
    List<?> results = resultSender.getResults();

    assertThat(results).isNotNull();
    assertThat(results.size()).isEqualTo(2);
    Object firstResult = results.get(0);
    assertThat(firstResult).isInstanceOf(CliFunctionResult.class);
    assertThat(((CliFunctionResult) firstResult).isSuccessful()).isTrue();
    assertThat(((CliFunctionResult) firstResult).getMessage()).isNotEmpty();
    assertThat(((CliFunctionResult) firstResult).getMessage()).contains("No indexes defined");
  }

  @Test
  public void multiIndexCreationExceptionThrowByQueryService() throws Exception {
    HashMap<String, Exception> exceptions = new HashMap<>();
    exceptions.put("index1", new IndexCreationException("Mock Failure."));
    exceptions.put("index3", new IndexCreationException("Another Mock Failure."));
    when(queryService.createDefinedIndexes())
        .thenThrow(new MultiIndexCreationException(exceptions));
    context = new FunctionContextImpl(cache, CreateDefinedIndexesFunction.class.getName(),
        indexDefinitions, resultSender);

    function.execute(context);
    List<?> results = resultSender.getResults();

    assertThat(results).isNotNull();
    assertThat(results.size()).isEqualTo(4);

    CliFunctionResult result1 = (CliFunctionResult) results.get(0);
    assertThat(result1.isSuccessful()).isTrue();
    assertThat(result1.getStatusMessage()).isEqualTo("Created index index2");

    CliFunctionResult result2 = (CliFunctionResult) results.get(1);
    assertThat(result2.isSuccessful()).isFalse();
    assertThat(result2.getStatusMessage())
        .isEqualTo("Failed to create index index1: Mock Failure.");

    CliFunctionResult result3 = (CliFunctionResult) results.get(2);
    assertThat(result3.isSuccessful()).isFalse();
    assertThat(result3.getStatusMessage())
        .isEqualTo("Failed to create index index3: Another Mock Failure.");
  }

  @Test
  public void unexpectedExceptionThrowByQueryService() throws Exception {
    when(queryService.createDefinedIndexes()).thenThrow(new RuntimeException("Mock Exception"));
    context = new FunctionContextImpl(cache, CreateDefinedIndexesFunction.class.getName(),
        indexDefinitions, resultSender);

    function.execute(context);
    List<?> results = resultSender.getResults();

    assertThat(results).isNotNull();
    assertThat(results.size()).isEqualTo(2);
    CliFunctionResult firstResult = (CliFunctionResult) results.get(0);

    assertThat(firstResult.isSuccessful()).isFalse();
    assertThat(firstResult.getStatusMessage()).isEqualTo("Mock Exception");
  }

  @Test
  public void creationSuccess() throws Exception {
    Index index1 = mock(HashIndex.class);
    when(index1.getName()).thenReturn("index1");
    when(index1.getRegion()).thenReturn(region1);

    Index index2 = mock(CompactMapRangeIndex.class);
    when(index2.getName()).thenReturn("index2");
    when(index2.getRegion()).thenReturn(region2);

    Index index3 = mock(PrimaryKeyIndex.class);
    when(index3.getName()).thenReturn("index3");
    when(index3.getRegion()).thenReturn(region1);

    when(queryService.createDefinedIndexes()).thenReturn(Arrays.asList(index1, index2, index3));
    context = new FunctionContextImpl(cache, CreateDefinedIndexesFunction.class.getName(),
        indexDefinitions, resultSender);

    function.execute(context);
    List<?> results = resultSender.getResults();

    assertThat(results).isNotNull();
    assertThat(results.size()).isEqualTo(4);

    Object firstIndex = results.get(0);
    assertThat(firstIndex).isNotNull();
    assertThat(firstIndex).isInstanceOf(CliFunctionResult.class);
    assertThat(((CliFunctionResult) firstIndex).isSuccessful());
  }

  private static class TestResultSender implements ResultSender {

    private final List<Object> results = new LinkedList<>();

    private Exception t;

    protected List<Object> getResults() throws Exception {
      if (t != null) {
        throw t;
      }
      return Collections.unmodifiableList(results);
    }

    @Override
    public void lastResult(final Object lastResult) {
      results.add(lastResult);
    }

    @Override
    public void sendResult(final Object oneResult) {
      results.add(oneResult);
    }

    @Override
    public void sendException(final Throwable t) {
      this.t = (Exception) t;
    }
  }
}
