/*
 * Copyright 2016-2020 Chronicle Software
 *
 * https://chronicle.software
 *
 * 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 net.openhft.chronicle.queue;

import net.openhft.chronicle.core.OS;
import net.openhft.chronicle.core.annotation.RequiredForClient;
import net.openhft.chronicle.core.time.SetTimeProvider;
import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder;
import net.openhft.chronicle.wire.DocumentContext;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import static org.junit.Assert.*;

@RequiredForClient
public class TailerDirectionTest extends ChronicleQueueTestBase {

    private static final String TEST_MESSAGE_PREFIX = "Test entry: ";

    /**
     * Return a test message string for a specific ID.
     */
    private String testMessage(int id) {
        return TEST_MESSAGE_PREFIX + id;
    }

    /**
     * Add a test message with the given ExcerptAppender and return the index position of the entry
     *
     * @param appender ExceptAppender
     * @param msg      test message
     * @return index position of the entry
     */
    private long appendEntry(@NotNull final ExcerptAppender appender, String msg) {
        DocumentContext dc = appender.writingDocument();
        try {
            dc.wire().write().text(msg);
            return dc.index();
        } finally {
            dc.close();
        }
    }

    /**
     * Read next message, forward or backward, depending on the settings of the Tailer
     *
     * @param tailer ExcerptTailer
     * @return entry or null, if no entry available
     */
    private String readNextEntry(@NotNull final ExcerptTailer tailer) {
        DocumentContext dc = tailer.readingDocument();
        try {
            if (dc.isPresent()) {
                Object parent = dc.wire().parent();
                assert parent == tailer;
                return dc.wire().read().text();
            }
            return null;
        } finally {
            dc.close();
        }
    }

    //
    // Test procedure:
    // 1) Read in FORWARD direction 2 of the 4 entries
    // 2) Toggle Tailer direction and read in BACKWARD direction 2 entries
    // 3) Redo step 1)
    //
    @Test
    public void testTailerForwardBackwardRead() {
        String basePath = OS.TARGET + "/tailerForwardBackward-" + System.nanoTime();

        ChronicleQueue queue = ChronicleQueue.singleBuilder(basePath)
                .testBlockSize()
                .rollCycle(RollCycles.HOURLY)
                .build();
        ExcerptAppender appender = queue.acquireAppender();
        ExcerptTailer tailer = queue.createTailer();

        //
        // Prepare test messages in queue
        //
        // Map of test messages with their queue index position
        HashMap<String, Long> msgIndexes = new HashMap<>();

        for (int i = 0; i < 4; i++) {
            String msg = testMessage(i);
            long idx = appendEntry(appender, msg);
            msgIndexes.put(msg, idx);
        }

        assertEquals("[Forward 1] Wrong message 0", testMessage(0), readNextEntry(tailer));
        assertEquals("[Forward 1] Wrong Tailer index after reading msg 0", msgIndexes.get(testMessage(1)).longValue(), tailer.index());
        assertEquals("[Forward 1] Wrong message 1", testMessage(1), readNextEntry(tailer));
        assertEquals("[Forward 1] Wrong Tailer index after reading msg 1", msgIndexes.get(testMessage(2)).longValue(), tailer.index());

        tailer.direction(TailerDirection.BACKWARD);

        assertEquals("[Backward] Wrong message 2", testMessage(2), readNextEntry(tailer));
        assertEquals("[Backward] Wrong Tailer index after reading msg 2", msgIndexes.get(testMessage(1)).longValue(), tailer.index());
        assertEquals("[Backward] Wrong message 1", testMessage(1), readNextEntry(tailer));
        assertEquals("[Backward] Wrong Tailer index after reading msg 1", msgIndexes.get(testMessage(0)).longValue(), tailer.index());
        assertEquals("[Backward] Wrong message 0", testMessage(0), readNextEntry(tailer));

        String res = readNextEntry(tailer);
        assertNull("Backward: res is" + res, res);

        tailer.direction(TailerDirection.FORWARD);

        res = readNextEntry(tailer);
        assertNull("Forward: res is" + res, res);

        assertEquals("[Forward 2] Wrong message 0", testMessage(0), readNextEntry(tailer));
        assertEquals("[Forward 2] Wrong Tailer index after reading msg 0", msgIndexes.get(testMessage(1)).longValue(), tailer.index());
        assertEquals("[Forward 2] Wrong message 1", testMessage(1), readNextEntry(tailer));
        assertEquals("[Forward 2] Wrong Tailer index after reading msg 1", msgIndexes.get(testMessage(2)).longValue(), tailer.index());

        queue.close();
    }

    @Test
    public void uninitialisedTailerCreatedBeforeFirstAppendWithDirectionNoneShouldNotFindDocument() {
        final AtomicLong clock = new AtomicLong(System.currentTimeMillis());
        String path = OS.TARGET + "/" + getClass().getSimpleName() + "-" + System.nanoTime();
        try (final ChronicleQueue queue = SingleChronicleQueueBuilder.single(path).timeProvider(clock::get).testBlockSize()
                .rollCycle(RollCycles.TEST_SECONDLY).build()) {

            final ExcerptTailer tailer = queue.createTailer();
            tailer.direction(TailerDirection.NONE);

            final ExcerptAppender excerptAppender = queue.acquireAppender();
            for (int i = 0; i < 10; i++) {
                excerptAppender.writeDocument(i, (out, value) -> {
                    out.int32(value);
                });
            }

            DocumentContext document = tailer.readingDocument();
            assertFalse(document.isPresent());
        }
    }

    @Test
    public void testTailerBackwardsReadBeyondCycle() {
        File basePath = DirectoryUtils.tempDir("tailerForwardBackwardBeyondCycle");
        SetTimeProvider timeProvider = new SetTimeProvider();
        ChronicleQueue queue = ChronicleQueue.singleBuilder(basePath)
                .testBlockSize()
                .timeProvider(timeProvider)
                .build();
        ExcerptAppender appender = queue.acquireAppender();

        //
        // Prepare test messages in queue
        //
        // List of test messages with their queue index position
        List<Long> indexes = new ArrayList<>();
        List<String> messages = new ArrayList<>();
        for (int d = -7; d <= 0; d++) {
            if (d == -5 || d == -4)
                continue; // nothing on those days.
            timeProvider.currentTimeMillis(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(d));
            for (int i = 0; i < 3; i++) {
                String msg = testMessage(indexes.size());
                long idx = appendEntry(appender, msg);
                messages.add(msg);
                indexes.add(idx);
            }
        }
        ExcerptTailer tailer = queue.createTailer()
                .direction(TailerDirection.BACKWARD)
                .toEnd();

        for (int i = indexes.size() - 1; i >= 0; i--) {
            long index = indexes.get(i);
            String msg = messages.get(i);

            assertEquals("[Backward] Wrong index " + i, index, tailer.index());
            assertEquals("[Backward] Wrong message " + i, msg, readNextEntry(tailer));
        }
        queue.close();
    }
}