/*
 * 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.development.utils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.cache.QueryEntity;
import org.apache.ignite.cache.QueryIndex;
import org.apache.ignite.cluster.ClusterState;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.DataRegionConfiguration;
import org.apache.ignite.configuration.DataStorageConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.GridKernalContext;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.pagemem.wal.IgniteWriteAheadLogManager;
import org.apache.ignite.internal.pagemem.wal.record.DataEntry;
import org.apache.ignite.internal.pagemem.wal.record.DataRecord;
import org.apache.ignite.internal.pagemem.wal.record.MetastoreDataRecord;
import org.apache.ignite.internal.pagemem.wal.record.WALRecord;
import org.apache.ignite.internal.processors.cache.CacheObjectImpl;
import org.apache.ignite.internal.processors.cache.GridCacheOperation;
import org.apache.ignite.internal.processors.cache.KeyCacheObjectImpl;
import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
import org.apache.ignite.internal.util.typedef.internal.CU;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.apache.ignite.transactions.Transaction;
import org.junit.Test;

import static java.lang.String.valueOf;
import static java.lang.System.setOut;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
import static java.util.function.Function.identity;
import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
import static org.apache.ignite.testframework.GridTestUtils.assertContains;
import static org.apache.ignite.testframework.GridTestUtils.assertNotContains;
import static org.apache.ignite.testframework.wal.record.RecordUtils.isIncludeIntoLog;

/**
 * Class for testing sensitive data when reading {@link WALRecord} using
 * {@link IgniteWalConverter}.
 */
public class IgniteWalConverterSensitiveDataTest extends GridCommonAbstractTest {
    /** Sensitive data prefix. */
    private static final String SENSITIVE_DATA_VALUE_PREFIX = "must_hide_it_";

    /** Path to directory where WAL is stored. */
    private static String walDirPath;

    /** Page size. */
    private static int pageSize;

    /** System out. */
    private static PrintStream sysOut;

    /** Sensitive data values. */
    private static List<String> sensitiveValues = new ArrayList<>();

    /**
     * Test out - can be injected via {@link #injectTestSystemOut()} instead
     * of System.out and analyzed in test.
     */
    private static ByteArrayOutputStream testOut;

    /** {@inheritDoc} */
    @Override protected void beforeTestsStarted() throws Exception {
        super.beforeTestsStarted();

        sysOut = System.out;
        testOut = new ByteArrayOutputStream(16 * 1024);

        int nodeId = 0;

        IgniteEx crd = startGrid(nodeId);
        crd.cluster().state(ClusterState.ACTIVE);

        try (Transaction tx = crd.transactions().txStart()) {
            IgniteCache<Object, Object> cache = crd.cache(DEFAULT_CACHE_NAME);

            sensitiveValues.add(SENSITIVE_DATA_VALUE_PREFIX + 0);
            sensitiveValues.add(SENSITIVE_DATA_VALUE_PREFIX + 1);
            sensitiveValues.add(SENSITIVE_DATA_VALUE_PREFIX + 2);

            String val0 = sensitiveValues.get(0);
            String val1 = sensitiveValues.get(1);
            String val2 = sensitiveValues.get(2);

            cache.put(val0, val0);
            cache.withKeepBinary().put(val1, val1);
            cache.put(val2, new Person(1, val2));

            tx.commit();
        }

        GridKernalContext kernalCtx = crd.context();
        IgniteWriteAheadLogManager wal = kernalCtx.cache().context().wal();

        for (WALRecord walRecord : withSensitiveData()) {
            if (isIncludeIntoLog(walRecord))
                wal.log(walRecord);
        }

        sensitiveValues.add(SENSITIVE_DATA_VALUE_PREFIX);

        wal.flush(null, true);

        IgniteConfiguration cfg = crd.configuration();

        String wd = cfg.getWorkDirectory();
        String wp = cfg.getDataStorageConfiguration().getWalPath();
        String fn = kernalCtx.pdsFolderResolver().resolveFolders().folderName();

        walDirPath = wd + File.separator + wp + File.separator + fn;
        pageSize = cfg.getDataStorageConfiguration().getPageSize();

        stopGrid(nodeId);
    }

    /** {@inheritDoc} */
    @Override protected void beforeTest() throws Exception {
        super.beforeTest();

        clearGridToStringClassCache();
    }

    /** {@inheritDoc} */
    @Override protected void afterTest() throws Exception {
        super.afterTest();

        log.info("Test output for " + getName());
        log.info("----------------------------------------");

        setOut(sysOut);

        log.info(testOut.toString());
        resetTestOut();
    }

    /** {@inheritDoc} */
    @Override protected void afterTestsStopped() throws Exception {
        super.afterTestsStopped();

        stopAllGrids();

        cleanPersistenceDir();
    }

    /** {@inheritDoc} */
    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
        return super.getConfiguration(igniteInstanceName)
            .setCacheConfiguration(
                new CacheConfiguration<>(DEFAULT_CACHE_NAME)
                    .setAtomicityMode(TRANSACTIONAL)
                    .setQueryEntities(asList(personQueryEntity()))
            )
            .setDataStorageConfiguration(
                new DataStorageConfiguration().setDefaultDataRegionConfiguration(
                    new DataRegionConfiguration().setPersistenceEnabled(true)
                )
            );
    }

    /**
     * Test checks that by default sensitive data is displayed.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testShowSensitiveDataByDefault() throws Exception {
        exeWithCheck(null, true, true, identity());
    }

    /**
     * Test verifies that sensitive data will be hidden.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testHideSensitiveData() throws Exception {
        exeWithCheck(ProcessSensitiveData.HIDE, false, false, identity());
    }

    /**
     * Test verifies that sensitive data should be replaced with hash.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testHashSensitiveData() throws Exception {
        exeWithCheck(ProcessSensitiveData.HASH, true, false, s -> valueOf(s.hashCode()));
    }

    /**
     * Test verifies that sensitive data should be replaced with MD5 hash.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testMd5HashSensitiveData() throws Exception {
        exeWithCheck(ProcessSensitiveData.MD5, true, false, ProcessSensitiveDataUtils::md5);
    }

    /**
     * Executing {@link IgniteWalConverter} with checking the content of its output.
     *
     * @param processSensitiveData Strategy for the processing of sensitive data.
     * @param containsData         Contains or not elements {@link #sensitiveValues} in utility output.
     * @param containsPrefix       Contains or not {@link #SENSITIVE_DATA_VALUE_PREFIX} in utility output.
     * @param converter            Converting elements {@link #sensitiveValues} for checking in utility output.
     * @throws Exception If failed.
     */
    private void exeWithCheck(
        ProcessSensitiveData processSensitiveData,
        boolean containsData,
        boolean containsPrefix,
        Function<String, String> converter
    ) throws Exception {
        requireNonNull(converter);

        injectTestSystemOut();

        List<String> args = new ArrayList<>();
        args.add("pageSize=" + pageSize);
        args.add("walDir=" + walDirPath);
        if (processSensitiveData != null)
            args.add("processSensitiveData=" + processSensitiveData.name());

        IgniteWalConverter.main(args.toArray(new String[args.size()]));

        String testOutStr = testOut.toString();

        if (containsPrefix)
            assertContains(log, testOutStr, SENSITIVE_DATA_VALUE_PREFIX);
        else
            assertNotContains(log, testOutStr, SENSITIVE_DATA_VALUE_PREFIX);

        for (String sensitiveDataValue : sensitiveValues) {
            if (containsData)
                assertContains(log, testOutStr, converter.apply(sensitiveDataValue));
            else
                assertNotContains(log, testOutStr, converter.apply(sensitiveDataValue));
        }
    }

    /**
     * Inject {@link #testOut} to System.out for analyze in test.
     */
    private void injectTestSystemOut() {
        setOut(new PrintStream(testOut));
    }

    /**
     * Reset {@link #testOut}.
     */
    private void resetTestOut() {
        testOut.reset();
    }

    /**
     * Creating {@link WALRecord} instances with sensitive data.
     *
     * @return {@link WALRecord} instances with sensitive data.
     */
    private Collection<WALRecord> withSensitiveData() {
        List<WALRecord> walRecords = new ArrayList<>();

        int cacheId = CU.cacheId(DEFAULT_CACHE_NAME);

        DataEntry dataEntry = new DataEntry(
            cacheId,
            new KeyCacheObjectImpl(SENSITIVE_DATA_VALUE_PREFIX, null, 0),
            new CacheObjectImpl(SENSITIVE_DATA_VALUE_PREFIX, null),
            GridCacheOperation.CREATE,
            new GridCacheVersion(),
            new GridCacheVersion(),
            0,
            0,
            0,
            DataEntry.EMPTY_FLAGS
        );

        byte[] sensitiveDataBytes = SENSITIVE_DATA_VALUE_PREFIX.getBytes(StandardCharsets.UTF_8);

        walRecords.add(new DataRecord(dataEntry));
        walRecords.add(new MetastoreDataRecord(SENSITIVE_DATA_VALUE_PREFIX, sensitiveDataBytes));

        return walRecords;
    }

    /**
     * Create {@link QueryEntity} for {@link Person}.
     *
     * @return QueryEntity for {@link Person}.
     */
    private QueryEntity personQueryEntity() {
        String orgIdField = "orgId";
        String nameField = "name";

        return new QueryEntity()
            .setKeyType(String.class.getName())
            .setValueType(Person.class.getName())
            .addQueryField(orgIdField, Integer.class.getName(), null)
            .addQueryField(nameField, String.class.getName(), null)
            .setIndexes(asList(new QueryIndex(nameField), new QueryIndex(orgIdField)));
    }

    /**
     * Simple class Person for tests.
     */
    private static class Person implements Serializable {
        /** Id organization. */
        int orgId;

        /** Name organization. */
        String name;

        /**
         * Constructor.
         *
         * @param orgId Organization id.
         * @param name  Organization name.
         */
        Person(int orgId, String name) {
            this.orgId = orgId;
            this.name = name;
        }
    }
}
