/*
 * Copyright 2012-2018 the original author or authors.
 *
 * 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 org.glowroot.agent.embedded.util;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

import com.google.common.base.Ticker;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class CappedDatabaseOutputStreamTest {

    private static final int BLOCK_HEADER_SIZE = 8;

    private File tempFile;
    private ScheduledExecutorService scheduledExecutor;
    private CappedDatabaseOutputStream cappedOut;
    private RandomAccessFile in;

    @BeforeEach
    public void onBefore() throws IOException {
        tempFile = File.createTempFile("glowroot-test-", ".capped.txt");
        scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
        cappedOut = CappedDatabaseOutputStream.create(tempFile, 10, scheduledExecutor,
                Ticker.systemTicker());
        in = new RandomAccessFile(tempFile, "r");
    }

    @AfterEach
    public void onAfter() throws IOException {
        scheduledExecutor.shutdownNow();
        cappedOut.close();
        in.close();
        tempFile.delete();
    }

    @Test
    public void shouldWrite() throws IOException {
        // given
        Writer out = new OutputStreamWriter(cappedOut);
        String text = "0123456789";

        // when
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        long cappedId = cappedOut.endBlock();

        // then
        assertWrite(text, cappedId);
    }

    @Test
    public void shouldWriteUsingByteArray() throws IOException {
        // given
        String text = "0123456789";

        // when
        cappedOut.startBlock();
        cappedOut.write(text.getBytes());
        cappedOut.flush();
        long cappedId = cappedOut.endBlock();

        // then
        assertWrite(text, cappedId);
    }

    @Test
    public void shouldWriteUsingSingleBytes() throws IOException {
        // when
        cappedOut.startBlock();
        cappedOut.write('0');
        cappedOut.write('1');
        cappedOut.write('2');
        cappedOut.write('3');
        cappedOut.write('4');
        cappedOut.write('5');
        cappedOut.write('6');
        cappedOut.write('7');
        cappedOut.write('8');
        cappedOut.write('9');
        cappedOut.flush();
        long cappedId = cappedOut.endBlock();

        // then
        assertWrite("0123456789", cappedId);
    }

    @Test
    public void shouldWrap() throws IOException {
        // given
        Writer out = new OutputStreamWriter(cappedOut);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 600; i++) {
            sb.append("0123456789");
        }
        String text = sb.toString();
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        cappedOut.endBlock();

        // when
        out = new OutputStreamWriter(cappedOut);
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        long cappedId = cappedOut.endBlock();

        // then
        assertThat(cappedId).isEqualTo(6000 + BLOCK_HEADER_SIZE);
        long currIndex = in.readLong();
        int cappedDatabaseSizeKb = in.readInt();
        long lastCompactionBaseIndex = in.readLong();
        assertThat(currIndex).isEqualTo(12000 + 2 * BLOCK_HEADER_SIZE);
        assertThat(cappedDatabaseSizeKb).isEqualTo(10);
        assertThat(lastCompactionBaseIndex).isEqualTo(0);
        in.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES + 6000 + BLOCK_HEADER_SIZE);
        long blockSize = in.readLong();
        assertThat(blockSize).isEqualTo(6000);
        byte[] bytes = new byte[(int) blockSize];
        int remaining = 10240 - 6000 - 2 * BLOCK_HEADER_SIZE;
        in.readFully(bytes, 0, remaining);
        in.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES);
        in.readFully(bytes, remaining, 6000 - remaining);
        String content = new String(bytes);
        assertThat(content).isEqualTo(text);
    }

    @Test
    public void shouldWrapAndKeepGoing() throws IOException {
        // given
        Writer out = new OutputStreamWriter(cappedOut);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 600; i++) {
            sb.append("0123456789");
        }
        String text = sb.toString();
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        cappedOut.endBlock();
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        cappedOut.endBlock();

        // when
        out = new OutputStreamWriter(cappedOut);
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        long cappedId = cappedOut.endBlock();

        // then
        assertThat(cappedId).isEqualTo(12000 + 2 * BLOCK_HEADER_SIZE);
        long currIndex = in.readLong();
        int cappedDatabaseSizeKb = in.readInt();
        long lastCompactionBaseIndex = in.readLong();
        assertThat(currIndex).isEqualTo(18000 + 3 * BLOCK_HEADER_SIZE);
        assertThat(cappedDatabaseSizeKb).isEqualTo(10);
        assertThat(lastCompactionBaseIndex).isEqualTo(0);
        int totalOfFirstTwoBlocks = 2 * (6000 + BLOCK_HEADER_SIZE);
        in.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES + totalOfFirstTwoBlocks - 10240);
        long blockSize = in.readLong();
        assertThat(blockSize).isEqualTo(6000);
        byte[] bytes = new byte[(int) blockSize];
        in.readFully(bytes, 0, bytes.length);
        String content = new String(bytes);
        assertThat(content).isEqualTo(text);
    }

    @Test
    public void shouldWrapAndResize() throws IOException {
        // given
        Writer out = new OutputStreamWriter(cappedOut);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 600; i++) {
            sb.append("0123456789");
        }
        String text = sb.toString();
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        cappedOut.endBlock();
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        long cappedId = cappedOut.endBlock();

        // when
        // have to close in before resizing
        in.close();
        cappedOut.resize(20);
        in = new RandomAccessFile(tempFile, "r");

        // then
        assertThat(cappedId).isEqualTo(6000 + BLOCK_HEADER_SIZE);
        long currIndex = in.readLong();
        int cappedDatabaseSizeKb = in.readInt();
        long lastCompactionBaseIndex = in.readLong();
        assertThat(currIndex).isEqualTo(12000 + 2 * BLOCK_HEADER_SIZE);
        assertThat(cappedDatabaseSizeKb).isEqualTo(20);
        int total = 2 * (6000 + BLOCK_HEADER_SIZE);
        assertThat(lastCompactionBaseIndex).isEqualTo(total - 10240);
        int totalOfFirstBlock = 6000 + BLOCK_HEADER_SIZE;
        in.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES + 10240 - totalOfFirstBlock);
        long blockSize = in.readLong();
        assertThat(blockSize).isEqualTo(6000);
        byte[] bytes = new byte[(int) blockSize];
        in.readFully(bytes, 0, 6000);
        String content = new String(bytes);
        assertThat(content).isEqualTo(text);
    }

    @Test
    public void shouldWrapAndResizeVerySmall() throws IOException {
        // given
        Writer out = new OutputStreamWriter(cappedOut);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 60; i++) {
            sb.append("0123456789");
        }
        String text = sb.toString();
        for (int i = 0; i < 9; i++) {
            cappedOut.startBlock();
            out.write(text);
            out.flush();
            cappedOut.endBlock();
        }
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        long cappedId = cappedOut.endBlock();

        // when
        // have to close in before resizing
        in.close();
        cappedOut.resize(1);
        in = new RandomAccessFile(tempFile, "r");

        // then
        assertThat(cappedId).isEqualTo(9 * (600 + BLOCK_HEADER_SIZE));
        long currIndex = in.readLong();
        int cappedDatabaseSizeKb = in.readInt();
        long lastCompactionBaseIndex = in.readLong();
        assertThat(currIndex).isEqualTo(10 * (600 + BLOCK_HEADER_SIZE));
        assertThat(cappedDatabaseSizeKb).isEqualTo(1);
        int total = 10 * (600 + BLOCK_HEADER_SIZE);
        assertThat(lastCompactionBaseIndex).isEqualTo(total - 1024);
        in.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES + 416);
        long blockSize = in.readLong();
        assertThat(blockSize).isEqualTo(600);
        byte[] bytes = new byte[(int) blockSize];
        in.readFully(bytes, 0, 600);
        String content = new String(bytes);
        assertThat(content).isEqualTo(text);
    }

    @Test
    public void shouldWrapWithoutEnoughSpaceAtEndForContiguousBlockHeader() throws IOException {
        // given
        String text = "0123456789";
        cappedOut.startBlock();
        int numBytesToWrite = 10240 - BLOCK_HEADER_SIZE - 1;
        for (int i = 0; i < numBytesToWrite; i++) {
            cappedOut.write(0);
        }
        cappedOut.flush();
        cappedOut.endBlock();

        // when
        Writer out = new OutputStreamWriter(cappedOut);
        out = new OutputStreamWriter(cappedOut);
        cappedOut.startBlock();
        out.write(text);
        out.flush();
        long cappedId = cappedOut.endBlock();

        // then
        assertThat(cappedId).isEqualTo(10240);
        long currIndex = in.readLong();
        int cappedDatabaseSizeKb = in.readInt();
        long lastCompactionBaseIndex = in.readLong();
        assertThat(currIndex).isEqualTo(10240 + BLOCK_HEADER_SIZE + text.length());
        assertThat(cappedDatabaseSizeKb).isEqualTo(10);
        assertThat(lastCompactionBaseIndex).isEqualTo(0);
        in.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES);
        long blockSize = in.readLong();
        assertThat(blockSize).isEqualTo(text.length());
        byte[] bytes = new byte[(int) blockSize];
        in.readFully(bytes, 0, text.length());
        String content = new String(bytes);
        assertThat(content).isEqualTo(text);
    }

    private void assertWrite(String text, long cappedId) throws IOException {
        assertThat(cappedId).isEqualTo(0);
        long currIndex = in.readLong();
        int cappedDatabaseSizeKb = in.readInt();
        long lastCompactionBaseIndex = in.readLong();
        assertThat(currIndex).isEqualTo(10 + BLOCK_HEADER_SIZE);
        assertThat(cappedDatabaseSizeKb).isEqualTo(10);
        assertThat(lastCompactionBaseIndex).isEqualTo(0);
        long blockSize = in.readLong();
        assertThat(blockSize).isEqualTo(10);
        byte[] bytes = new byte[(int) blockSize];
        in.readFully(bytes, 0, bytes.length);
        String content = new String(bytes);
        assertThat(content).isEqualTo(text);
    }
}
