package com.atlassian.bitbucket.mesh.git.hook;

import com.atlassian.bitbucket.mesh.junit.TemporaryDirectory;
import org.apache.commons.lang3.StringUtils;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.EnumSet;

import static java.util.stream.Collectors.toList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.*;

public class HookStdinBuilderTest {

    private static final String TEST_DATA = StringUtils.repeat("This is a test\n", 9000);

    @Rule
    public final TemporaryDirectory tempDir = new TemporaryDirectory();

    @Test
    public void testAppendCleansUpWhenThrowing() throws Exception {
        assertThrows(IOException.class, () -> {
            HookStdinBuilder builder = new HookStdinBuilder(tempDir.getRoot(), 0L);
            builder.append(ByteBuffer.wrap("Test".getBytes(StandardCharsets.UTF_8))); //Creates the temp file

            //Find the temp file and mark it read-only. This will prevent appending the next buffer
            //but will not prevent deleting the file (that's based on the directory's permissions)
            Path temp = Files.list(tempDir.getRoot()).findFirst().orElse(null);
            assertNotNull("Could not locate temp file", temp);
            Files.setPosixFilePermissions(temp, EnumSet.of(PosixFilePermission.OWNER_READ));

            builder.append(ByteBuffer.wrap("More".getBytes(StandardCharsets.UTF_8)));
        });

        assertThat(Files.list(tempDir.getRoot()).collect(toList()), empty());
    }

    @Test
    public void testAppendThrowsIfBufferCapacityExceededAndFileBufferingNotAllowed() {
        assertThrows(BufferOverflowException.class, () -> new HookStdinBuilder(null, 3L)
                .append(ByteBuffer.wrap("Test".getBytes(StandardCharsets.UTF_8))));
    }

    @Test
    public void testAppendThrowsIfBufferingToDiskFails() {
        assertThrows(IOException.class, () -> new HookStdinBuilder(tempDir.getRoot().resolve("nonexistent"), 10L)
                    .append(ByteBuffer.wrap("This should throw".getBytes(StandardCharsets.UTF_8))));
    }

    @Test
    public void testBufferingInMemory() throws Exception {
        HookStdin stdin = appendAndBuild(TEST_DATA);
        assertThat(stdin, instanceOf(ByteStringHookStdin.class));
    }

    @Test
    public void testBufferingOnDisk() throws Exception {
        HookStdin stdin = appendAndBuild(TEST_DATA, TEST_DATA.length() / 2); //Ensure an overflow
        assertThat(stdin, instanceOf(PathHookStdin.class));
    }

    @Test
    public void testBufferingStraightToDisk() throws Exception {
        HookStdin stdin = appendAndBuild(TEST_DATA, 0); //No buffering in memory allowed
        assertThat(stdin, instanceOf(PathHookStdin.class));
    }

    @Test
    public void testEmpty() throws Exception {
        HookStdin stdin = appendAndBuild("");
        assertThat(stdin, instanceOf(ByteStringHookStdin.class)); //Ensure we've got an in-memory buffer
    }

    private HookStdin appendAndBuild(String data) throws Exception {
        return appendAndBuild(data, data.length() + 1);
    }

    private HookStdin appendAndBuild(String data, int capacity) throws Exception {
        HookStdinBuilder builder = new HookStdinBuilder(tempDir.getRoot(), capacity);

        //Append the data in chunks up to 4K in size
        ByteBuffer bytes = ByteBuffer.wrap(data.getBytes(StandardCharsets.UTF_8));
        while (bytes.hasRemaining()) {
            ByteBuffer slice;
            if (bytes.remaining() > 4192) {
                slice = bytes.slice();
                slice.limit(4192);

                bytes.position(bytes.position() + 4192);
            } else {
                slice = bytes;
            }

            builder.append(slice);
        }

        //Verify the assembled stdin matches the input
        HookStdin stdin = builder.build();
        assertNotNull(stdin);
        assertEquals(data, stdin.read());

        return stdin;
    }
}