/*
 * Copyright 2019 The Exonum Team
 *
 * 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.
 */

package com.exonum.binding.core.storage.indices;

import static com.exonum.binding.common.hash.Hashing.DEFAULT_HASH_SIZE_BITS;
import static com.exonum.binding.common.serialization.StandardSerializers.string;
import static com.exonum.binding.core.storage.indices.ProofListContainsMatcher.provesAbsence;
import static com.exonum.binding.core.storage.indices.ProofListContainsMatcher.provesThatContains;
import static com.exonum.binding.core.storage.indices.TestStorageItems.V1;
import static com.exonum.binding.core.storage.indices.TestStorageItems.V2;
import static com.exonum.binding.core.storage.indices.TestStorageItems.V3;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;

import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.serialization.Serializer;
import com.exonum.binding.core.proxy.Cleaner;
import com.exonum.binding.core.storage.database.Access;
import com.exonum.messages.proof.ListProofOuterClass;
import com.exonum.messages.proof.ListProofOuterClass.ListProofEntry;
import com.google.protobuf.ByteString;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
 * Inherits base tests of ListIndex interface methods and also contains tests
 * of ProofListIndexProxy methods that are not present in {@link ListIndex} interface.
 */
class ProofListIndexProxyIntegrationTest extends BaseListIndexIntegrationTestable {

  private static final HashCode EMPTY_LIST_INDEX_HASH =
      HashCode.fromString("c6c0aa07f27493d2f2e5cff56c890a353a20086d6c25ec825128e12ae752b2d9");

  private static final String LIST_NAME = "test_proof_list";

  @Override
  ProofListIndexProxy<String> create(String name, Access access) {
    return access.getProofList(IndexAddress.valueOf(name), string());
  }

  @Override
  ProofListIndexProxy<String> createInGroup(String groupName, byte[] idInGroup, Access access) {
    return access.getProofList(IndexAddress.valueOf(groupName, idInGroup), string());
  }

  @Override
  StorageIndex createOfOtherType(String name, Access access) {
    return access.getList(IndexAddress.valueOf(name), string());
  }

  @Override
  Object getAnyElement(AbstractListIndexProxy<String> index) {
    return index.get(0L);
  }

  @Override
  void update(AbstractListIndexProxy<String> index) {
    index.add(V1);
  }

  @Test
  void getIndexHashEmptyList() {
    runTestWithView(database::createSnapshot, (list) -> {
      assertThat(list.getIndexHash(), equalTo(EMPTY_LIST_INDEX_HASH));
    });
  }

  @Test
  void getIndexHashSingletonList() {
    runTestWithView(database::createFork, (list) -> {
      list.add(V1);

      HashCode indexHash = list.getIndexHash();
      assertThat(indexHash.bits(), equalTo(DEFAULT_HASH_SIZE_BITS));
      assertThat(indexHash, not(equalTo(EMPTY_LIST_INDEX_HASH)));
    });
  }

  @ParameterizedTest
  @ValueSource(ints = {0, 1, 2})
  void getProofThreeElementList(int index) {
    runTestWithView(database::createFork, (list) -> {
      List<String> elements = asList(V1, V2, V3);
      list.addAll(elements);

      ListProof proof = list.getProof(index);

      ListProofOuterClass.ListProof asMessage = proof.getAsMessage();
      assertThat(asMessage.getLength()).isEqualTo(3L);
      ListProofEntry expectedEntry = listProofEntry(index, elements.get(index));
      assertThat(asMessage.getEntriesList()).containsExactly(expectedEntry);
    });
  }

  @ParameterizedTest
  @ValueSource(longs = {3, 4, Long.MAX_VALUE})
  void getProofThreeElementListOutOfRange(long index) {
    runTestWithView(database::createFork, (list) -> {
      List<String> elements = asList(V1, V2, V3);
      list.addAll(elements);

      ListProof proof = list.getProof(index);

      ListProofOuterClass.ListProof asMessage = proof.getAsMessage();
      assertThat(asMessage.getLength()).isEqualTo(3L);
      assertThat(asMessage.getEntriesList()).isEmpty();
    });
  }

  @Test
  void getRangeProofThreeElementListFullRange() {
    runTestWithView(database::createFork, (list) -> {
      List<String> elements = asList(V1, V2, V3);
      list.addAll(elements);

      ListProof proof = list.getRangeProof(0, 3);

      ListProofOuterClass.ListProof asMessage = proof.getAsMessage();
      assertThat(asMessage.getLength()).isEqualTo(3L);
      assertThat(asMessage.getEntriesList()).containsExactlyInAnyOrder(
          listProofEntry(0, V1), listProofEntry(1, V2), listProofEntry(2, V3));
    });
  }

  @Test
  void getRangeProofThreeElementListHalfInRange() {
    runTestWithView(database::createFork, (list) -> {
      List<String> elements = asList(V1, V2, V3);
      list.addAll(elements);

      ListProof proof = list.getRangeProof(2, 4);

      ListProofOuterClass.ListProof asMessage = proof.getAsMessage();
      assertThat(asMessage.getLength()).isEqualTo(3L);
      assertThat(asMessage.getEntriesList()).containsExactly(listProofEntry(2, V3));
    });
  }

  private static ListProofEntry listProofEntry(long index, String element) {
    Serializer<String> serializer = string();
    return ListProofEntry.newBuilder()
        .setIndex(index)
        .setValue(ByteString.copyFrom(serializer.toBytes(element)))
        .build();
  }

  @Test
  @DisabledProofTest
  void verifyProofSingletonList() {
    runTestWithView(database::createFork, (list) -> {
      list.add(V1);

      assertThat(list, provesThatContains(0, V1));
    });
  }

  @Test
  @DisabledProofTest
  void verifyProofOfAbsenceEmptyList() {
    runTestWithView(database::createFork, (list) -> {
      assertThat(list, provesAbsence(0));
    });
  }

  @Test
  @DisabledProofTest
  void verifyProofOfAbsenceSingletonList() {
    runTestWithView(database::createFork, (list) -> {
      list.add(V1);

      assertThat(list, provesAbsence(1));
    });
  }

  @Test
  @DisabledProofTest
  void verifyRangeProofOfAbsenceEmptyList() {
    runTestWithView(database::createFork, (list) -> {
      assertThat(list, provesAbsence(0, 1));
    });
  }

  @Test
  @DisabledProofTest
  void verifyRangeProofOfAbsenceSingletonList() {
    runTestWithView(database::createFork, (list) -> {
      list.add(V1);

      assertThat(list, provesAbsence(0, 2));
    });
  }

  @Test
  @DisabledProofTest
  void verifyRangeProofSingletonList() {
    runTestWithView(database::createFork, (list) -> {
      list.add(V1);

      assertThat(list, provesThatContains(0, singletonList(V1)));
    });
  }

  @ParameterizedTest
  @DisabledProofTest
  @ValueSource(ints = {2, 3, 4, 5, 7, 8, 9})
  void verifyProofMultipleItemList(int size) {
    runTestWithView(database::createFork, (list) -> {
      List<String> values = TestStorageItems.values.subList(0, size);

      list.addAll(values);

      for (int i = 0; i < values.size(); i++) {
        assertThat(list, provesThatContains(i, values.get(i)));
      }
    });
  }

  @Test
  @DisabledProofTest
  void verifyRangeProofMultipleItemList_FullRange() {
    runTestWithView(database::createFork, (list) -> {
      List<String> values = TestStorageItems.values;
      list.addAll(values);

      assertThat(list, provesThatContains(0, values));
    });
  }

  @Test
  @DisabledProofTest
  void verifyRangeProofMultipleItemList_1stHalf() {
    runTestWithView(database::createFork, (list) -> {
      List<String> values = TestStorageItems.values;
      list.addAll(values);

      int from = 0;
      int to = values.size() / 2;
      assertThat(list, provesThatContains(from, values.subList(from, to)));
    });
  }

  @Test
  @DisabledProofTest
  void verifyRangeProofMultipleItemList_2ndHalf() {
    runTestWithView(database::createFork, (list) -> {
      List<String> values = TestStorageItems.values;
      list.addAll(values);

      int from = values.size() / 2;
      int to = values.size();
      assertThat(list, provesThatContains(from, values.subList(from, to)));
    });
  }

  @ParameterizedTest
  @ValueSource(ints = {1, 2, 3, 4})
  @DisabledProofTest
  @Disabled("ECR-3673: empty ranges are not supported with the current tree format; "
      + "need a flat one")
  void verifyRangeProofMultipleItemList_EmptyRange(int size) {
    runTestWithView(database::createFork, (list) -> {
      List<String> values = TestStorageItems.values.subList(0, size);

      list.addAll(values);

      assertThat(list, provesThatContains(0, emptyList()));
    });
  }

  private static void runTestWithView(Function<Cleaner, Access> viewFactory,
                                      Consumer<ProofListIndexProxy<String>> listTest) {
    runTestWithView(viewFactory, (ignoredView, list) -> listTest.accept(list));
  }

  private static void runTestWithView(Function<Cleaner, Access> viewFactory,
                                      BiConsumer<Access, ProofListIndexProxy<String>> listTest) {
    IndicesTests.runTestWithView(
        viewFactory,
        LIST_NAME,
        ((address, access, serializer) -> access.getProofList(address, serializer)),
        listTest
    );
  }
}