/*
 * 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.ignite.internal.vault;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import org.apache.ignite.internal.util.Cursor;
import org.apache.ignite.lang.ByteArray;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Base class for testing {@link VaultService} implementations.
 */
public abstract class VaultServiceTest {
    /** Vault. */
    private VaultService vaultService;

    /**
     * Before each.
     */
    @BeforeEach
    public void setUp() throws IOException {
        vaultService = getVaultService();

        vaultService.start();
    }

    /**
     * After each.
     */
    @AfterEach
    void tearDown() throws Exception {
        vaultService.close();
    }

    /**
     * Returns the vault service that will be tested.
     */
    protected abstract VaultService getVaultService();

    /**
     * Tests regular behaviour of the {@link VaultService#put} method.
     */
    @Test
    public void testPut() {
        ByteArray key = getKey(1);

        assertThat(vaultService.get(key), willBe(nullValue(VaultEntry.class)));

        byte[] val = getValue(1);

        assertThat(vaultService.put(key, val), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(key), willBe(equalTo(new VaultEntry(key, val))));

        // test idempotency
        assertThat(vaultService.put(key, val), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(key), willBe(equalTo(new VaultEntry(key, val))));
    }

    /**
     * Tests that the {@link VaultService#put} method removes the given {@code key} if {@code value} equalTo {@code null}.
     */
    @Test
    public void testPutWithNull() {
        ByteArray key = getKey(1);

        byte[] val = getValue(1);

        assertThat(vaultService.put(key, val), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(key), willBe(equalTo(new VaultEntry(key, val))));

        assertThat(vaultService.put(key, null), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(key), willBe(nullValue(VaultEntry.class)));
    }

    /**
     * Tests regular behaviour of the {@link VaultService#remove} method.
     */
    @Test
    public void testRemove() {
        ByteArray key = getKey(1);

        // Remove non-existent value.
        assertThat(vaultService.remove(key), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(key), willBe(nullValue(VaultEntry.class)));

        byte[] val = getValue(1);

        assertThat(vaultService.put(key, val), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(key), willBe(equalTo(new VaultEntry(key, val))));

        // Remove existing value.
        assertThat(vaultService.remove(key), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(key), willBe(nullValue(VaultEntry.class)));
    }

    /**
     * Tests regular behaviour of the {@link VaultService#putAll} method.
     */
    @Test
    public void testPutAll() {
        Map<ByteArray, byte[]> batch = IntStream.range(0, 10)
                .boxed()
                .collect(toMap(VaultServiceTest::getKey, VaultServiceTest::getValue));

        assertThat(vaultService.putAll(batch), willBe(nullValue(Void.class)));

        batch.forEach((k, v) -> assertThat(vaultService.get(k), willBe(equalTo(new VaultEntry(k, v)))));

        assertThat(vaultService.putAll(batch), willBe(nullValue(Void.class)));

        batch.forEach((k, v) -> assertThat(vaultService.get(k), willBe(equalTo(new VaultEntry(k, v)))));
    }

    /**
     * Tests that the {@link VaultService#putAll} method will remove keys, which values are {@code null}.
     */
    @Test
    public void testPutAllWithNull() {
        Map<ByteArray, byte[]> batch = IntStream.range(0, 10)
                .boxed()
                .collect(toMap(VaultServiceTest::getKey, VaultServiceTest::getValue));

        assertThat(vaultService.putAll(batch), willBe(nullValue(Void.class)));

        batch.forEach((k, v) -> assertThat(vaultService.get(k), willBe(equalTo(new VaultEntry(k, v)))));

        Map<ByteArray, byte[]> secondBatch = new HashMap<>();

        secondBatch.put(getKey(4), getValue(3));
        secondBatch.put(getKey(8), getValue(3));
        secondBatch.put(getKey(1), null);
        secondBatch.put(getKey(3), null);

        assertThat(vaultService.putAll(secondBatch), willBe(nullValue(Void.class)));

        assertThat(vaultService.get(getKey(4)), willBe(equalTo(new VaultEntry(getKey(4), getValue(3)))));
        assertThat(vaultService.get(getKey(8)), willBe(equalTo(new VaultEntry(getKey(8), getValue(3)))));
        assertThat(vaultService.get(getKey(1)), willBe(nullValue(VaultEntry.class)));
        assertThat(vaultService.get(getKey(3)), willBe(nullValue(VaultEntry.class)));
    }

    /**
     * Tests regular behaviour of the {@link VaultService#range} method.
     */
    @Test
    public void testRange() throws Exception {
        List<VaultEntry> entries = getRange(0, 10);

        Map<ByteArray, byte[]> batch = entries.stream().collect(toMap(VaultEntry::key, VaultEntry::value));

        assertThat(vaultService.putAll(batch), willBe(nullValue(Void.class)));

        List<VaultEntry> range = range(getKey(3), getKey(7));

        assertThat(range, equalTo(getRange(3, 7)));
    }

    /**
     * Tests that the {@link VaultService#range} returns valid entries when passed a larger range, than the available data.
     */
    @Test
    public void testRangeBoundaries() throws Exception {
        List<VaultEntry> entries = getRange(3, 5);

        Map<ByteArray, byte[]> batch = entries.stream().collect(toMap(VaultEntry::key, VaultEntry::value));

        assertThat(vaultService.putAll(batch), willBe(nullValue(Void.class)));

        List<VaultEntry> range = range(getKey(0), getKey(9));

        assertThat(range, equalTo(entries));
    }

    /**
     * Tests that the {@link VaultService#range} upper bound equalTo not included.
     */
    @Test
    public void testRangeNotIncludedBoundary() throws Exception {
        List<VaultEntry> entries = getRange(3, 5);

        Map<ByteArray, byte[]> batch = entries.stream().collect(toMap(VaultEntry::key, VaultEntry::value));

        assertThat(vaultService.putAll(batch), willBe(nullValue(Void.class)));

        List<VaultEntry> range = range(getKey(3), getKey(4));

        assertThat(range, contains(new VaultEntry(getKey(3), getValue(3))));
    }

    /**
     * Tests that an empty result equalTo returned when {@link VaultService#range} contains invalid boundaries.
     */
    @Test
    public void testRangeInvalidBoundaries() throws Exception {
        Map<ByteArray, byte[]> batch = getRange(3, 5).stream().collect(toMap(VaultEntry::key, VaultEntry::value));

        assertThat(vaultService.putAll(batch), willBe(nullValue(Void.class)));

        List<VaultEntry> range = range(getKey(4), getKey(1));

        assertThat(range, is(empty()));

        range = range(getKey(4), getKey(4));

        assertThat(range, is(empty()));
    }

    /**
     * Creates a test key.
     */
    private static ByteArray getKey(int k) {
        return new ByteArray("key" + k);
    }

    /**
     * Creates a test value.
     */
    private static byte[] getValue(int v) {
        return ("val" + v).getBytes(StandardCharsets.UTF_8);
    }

    /**
     * Creates a range of test Vault entries.
     */
    private static List<VaultEntry> getRange(int from, int to) {
        return IntStream.range(from, to)
                .mapToObj(i -> new VaultEntry(getKey(i), getValue(i)))
                .collect(toList());
    }

    /**
     * Extracts the given range of values from the Vault.
     */
    private List<VaultEntry> range(ByteArray from, ByteArray to) throws Exception {
        try (Cursor<VaultEntry> cursor = vaultService.range(from, to)) {
            return cursor.stream().collect(toList());
        }
    }
}
