/*
 * 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.nifi.provenance;

import org.apache.nifi.provenance.serialization.RecordReader;
import org.apache.nifi.provenance.serialization.RecordWriter;
import org.apache.nifi.provenance.toc.StandardTocReader;
import org.apache.nifi.provenance.toc.StandardTocWriter;
import org.apache.nifi.provenance.toc.TocReader;
import org.apache.nifi.provenance.toc.TocUtil;
import org.apache.nifi.provenance.toc.TocWriter;
import org.apache.nifi.util.file.FileUtils;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;


public abstract class AbstractTestRecordReaderWriter {
    protected ProvenanceEventRecord createEvent() {
        return TestUtil.createEvent();
    }

    @Test
    public void testSimpleWriteWithToc() throws IOException {
        final File journalFile = new File("target/storage/" + UUID.randomUUID() + "/testSimpleWrite");
        final File tocFile = TocUtil.getTocFile(journalFile);
        final TocWriter tocWriter = new StandardTocWriter(tocFile, false, false);
        final RecordWriter writer = createWriter(journalFile, tocWriter, false, 1024 * 1024);

        writer.writeHeader(1L);
        writer.writeRecords(Collections.singletonList(createEvent()));
        writer.close();

        final TocReader tocReader = new StandardTocReader(tocFile);
        final String expectedTransitUri = "nifi://unit-test";
            final int expectedBlockIndex = 0;

        assertRecoveredRecord(journalFile, tocReader, expectedTransitUri, expectedBlockIndex);

        FileUtils.deleteFile(journalFile.getParentFile(), true);
    }

    private void assertRecoveredRecord(File journalFile, TocReader tocReader, String expectedTransitUri, int expectedBlockIndex) throws IOException {
        try (final FileInputStream fis = new FileInputStream(journalFile);
             final RecordReader reader = createReader(fis, journalFile.getName(), tocReader, 2048)) {
            assertEquals(expectedBlockIndex, reader.getBlockIndex());
            reader.skipToBlock(expectedBlockIndex);
            final StandardProvenanceEventRecord recovered = reader.nextRecord();
            assertNotNull(recovered);

            assertEquals(expectedTransitUri, recovered.getTransitUri());
            assertNull(reader.nextRecord());
        }
    }


    @Test
    public void testSingleRecordCompressed() throws IOException {
        final File journalFile = new File("target/storage/" + UUID.randomUUID() + "/testSimpleWrite.gz");
        final File tocFile = TocUtil.getTocFile(journalFile);
        final TocWriter tocWriter = new StandardTocWriter(tocFile, false, false);
        final RecordWriter writer = createWriter(journalFile, tocWriter, true, 8192);

        writer.writeHeader(1L);
        writer.writeRecords(Collections.singletonList(createEvent()));
        writer.close();

        final TocReader tocReader = new StandardTocReader(tocFile);

        assertRecoveredRecord(journalFile, tocReader, "nifi://unit-test", 0);

        FileUtils.deleteFile(journalFile.getParentFile(), true);
    }


    @Test
    public void testMultipleRecordsSameBlockCompressed() throws IOException {
        final File journalFile = new File("target/storage/" + UUID.randomUUID() + "/testSimpleWrite.gz");
        final File tocFile = TocUtil.getTocFile(journalFile);
        final TocWriter tocWriter = new StandardTocWriter(tocFile, false, false);
        // new record each 1 MB of uncompressed data
        final RecordWriter writer = createWriter(journalFile, tocWriter, true, 1024 * 1024);

        writer.writeHeader(1L);
        for (int i = 0; i < 10; i++) {
            writer.writeRecords(Collections.singletonList(createEvent()));
        }
        writer.close();

        final TocReader tocReader = new StandardTocReader(tocFile);

        try (final FileInputStream fis = new FileInputStream(journalFile);
            final RecordReader reader = createReader(fis, journalFile.getName(), tocReader, 2048)) {
            for (int i = 0; i < 10; i++) {
                assertEquals(0, reader.getBlockIndex());

                // call skipToBlock half the time to ensure that we can; avoid calling it
                // the other half of the time to ensure that it's okay.
                if (i <= 5) {
                    reader.skipToBlock(0);
                }

                final StandardProvenanceEventRecord recovered = reader.nextRecord();
                assertNotNull(recovered);
                assertEquals("nifi://unit-test", recovered.getTransitUri());
            }

            assertNull(reader.nextRecord());
        }

        FileUtils.deleteFile(journalFile.getParentFile(), true);
    }


    @Test
    public void testMultipleRecordsMultipleBlocksCompressed() throws IOException {
        final File journalFile = new File("target/storage/" + UUID.randomUUID() + "/testSimpleWrite.gz");
        final File tocFile = TocUtil.getTocFile(journalFile);
        final TocWriter tocWriter = new StandardTocWriter(tocFile, false, false);
        // new block each 10 bytes
        final RecordWriter writer = createWriter(journalFile, tocWriter, true, 100);

        writer.writeHeader(1L);
        for (int i = 0; i < 10; i++) {
            writer.writeRecords(Collections.singletonList(createEvent()));
        }
        writer.close();

        final TocReader tocReader = new StandardTocReader(tocFile);

        try (final FileInputStream fis = new FileInputStream(journalFile);
            final RecordReader reader = createReader(fis, journalFile.getName(), tocReader, 2048)) {
            for (int i = 0; i < 10; i++) {
                final StandardProvenanceEventRecord recovered = reader.nextRecord();
                System.out.println(recovered);
                assertNotNull(recovered);
                assertEquals(i, recovered.getEventId());
                assertEquals("nifi://unit-test", recovered.getTransitUri());

                final Map<String, String> updatedAttrs = recovered.getUpdatedAttributes();
                assertNotNull(updatedAttrs);
                assertEquals(2, updatedAttrs.size());
                assertEquals("1.txt", updatedAttrs.get("filename"));
                assertTrue(updatedAttrs.containsKey("uuid"));
            }

            assertNull(reader.nextRecord());
        }

        FileUtils.deleteFile(journalFile.getParentFile(), true);
    }

    @Test
    public void testSkipToEvent() throws IOException {
        final File journalFile = new File("target/storage/" + UUID.randomUUID() + "/testSimpleWrite.gz");
        final File tocFile = TocUtil.getTocFile(journalFile);
        final TocWriter tocWriter = new StandardTocWriter(tocFile, false, false);
        // new block each 10 bytes
        final RecordWriter writer = createWriter(journalFile, tocWriter, true, 100);

        writer.writeHeader(0L);
        final int numEvents = 10;
        final List<ProvenanceEventRecord> events = new ArrayList<>();
        for (int i = 0; i < numEvents; i++) {
            final ProvenanceEventRecord event = createEvent();
            events.add(event);
            writer.writeRecords(Collections.singletonList(event));
        }
        writer.close();

        final TocReader tocReader = new StandardTocReader(tocFile);

        try (final FileInputStream fis = new FileInputStream(journalFile);
            final RecordReader reader = createReader(fis, journalFile.getName(), tocReader, 2048)) {

            for (int i = 0; i < numEvents; i++) {
                System.out.println(i);

                final Optional<ProvenanceEventRecord> eventOption = reader.skipToEvent(i);
                assertTrue(eventOption.isPresent());
                assertEquals(i, eventOption.get().getEventId());
                assertEquals(events.get(i), eventOption.get());

                final StandardProvenanceEventRecord consumedEvent = reader.nextRecord();
                assertEquals(eventOption.get(), consumedEvent);
            }

            assertFalse(reader.skipToEvent(numEvents + 1).isPresent());
        }

        try (final FileInputStream fis = new FileInputStream(journalFile);
            final RecordReader reader = createReader(fis, journalFile.getName(), tocReader, 2048)) {

            for (int i = 0; i < 3; i++) {
                final Optional<ProvenanceEventRecord> eventOption = reader.skipToEvent(8);
                assertTrue(eventOption.isPresent());
                assertEquals(events.get(8), eventOption.get());
            }

            final StandardProvenanceEventRecord consumedEvent = reader.nextRecord();
            assertEquals(events.get(8), consumedEvent);
        }
    }

    protected abstract RecordWriter createWriter(File file, TocWriter tocWriter, boolean compressed, int uncompressedBlockSize) throws IOException;

    protected abstract RecordReader createReader(InputStream in, String journalFilename, TocReader tocReader, int maxAttributeSize) throws IOException;
}
