package com.atlassian.bitbucket.mesh.io;

import org.junit.Test;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.*;

public class EncodingUtilsTest {

    //The following 5 tests verify that byte order marks correctly identify UTF-8, 16 and 32. UTF-16 and 32
    //both have big endian and little endian variants, which are both validated.
    // http://en.wikipedia.org/wiki/Byte_order_mark

    @Test
    public void testDetectEncodingFromUtf8Bom() throws Exception {
        assertEquals("UTF-8", EncodingUtils.detectEncoding(stream(0xEF, 0xBB, 0xBF, 0x41), 5));
    }

    @Test
    public void testDetectEncodingFromUtf16BeBom() {
        assertEquals("UTF-16BE", EncodingUtils.detectEncoding(directBuffer(0xFE, 0xFF, 0x00, 0x41)));
    }

    @Test
    public void testDetectEncodingFromUtf16LeBom() {
        assertEquals("UTF-16LE", EncodingUtils.detectEncoding(heapBuffer(0xFF, 0xFE, 0x41, 0x00)));
    }

    @Test
    public void testDetectEncodingFromUtf32BeBom() throws Exception {
        assertEquals("UTF-32BE", EncodingUtils.detectEncoding(stream(0x00, 0x00, 0xFE, 0xFF), 5));
    }

    @Test
    public void testDetectEncodingFromUtf32LeBom() throws Exception {
        assertEquals("UTF-32LE", EncodingUtils.detectEncoding(stream(0xFF, 0xFE, 0x00, 0x00), 5));
    }

    @Test
    public void testDetectEncodingForByteArray() {
        assertEquals("UTF-8", EncodingUtils.detectEncoding(bytes(0xEF, 0xBB, 0xBF, 0x41)));
    }

    @Test
    public void testDetectEncodingForByteArrayWithEmojis() {
        assertEquals("UTF-8", EncodingUtils.detectEncoding(
                // Person woman man camera TV
                "\uD83E\uDDD1 \uD83D\uDC69 \uD83D\uDC68 \uD83D\uDCF7 \uD83D\uDCFA".getBytes(UTF_8)));
    }

    @Test
    public void testDetectEncodingForByteArrayWithUnsupportedEncoding() {
        // Detected as HZ-GB-2312, see https://jira.atlassian.com/browse/BSERV-10865
        assertNull(EncodingUtils.detectEncoding("RETURN TRIM(attribute.val, \"~{~}\").".getBytes(UTF_8)));
    }

    @Test
    public void testDetectEncodingForByteBufferWithUnsupportedEncoding() {
        // Detected as HZ-GB-2312, see https://jira.atlassian.com/browse/BSERV-10865
        assertNull(EncodingUtils.detectEncoding(
                ByteBuffer.wrap("RETURN TRIM(attribute.val, \"~{~}\").".getBytes(UTF_8))));
    }

    @Test
    public void testDetectEncodingForDirectByteBuffer() {
        assertEquals("UTF-8", EncodingUtils.detectEncoding(directBuffer(0xEF, 0xBB, 0xBF, 0x41)));
    }

    @Test
    public void testDetectEncodingForHeapByteBuffer() {
        assertEquals("UTF-8", EncodingUtils.detectEncoding(heapBuffer(0xEF, 0xBB, 0xBF, 0x41)));
    }

    @Test
    public void testDetectEncodingForStreamReadingFullBuffer() throws Exception {
        //See testIsBinaryForStreamReadingFullBuffer
        assertNull(EncodingUtils.detectEncoding(
                new BufferedInputStream(stream("This is 22 characters."), 22), 22));
    }

    @Test
    public void testDetectEncodingForStreamWithUnsupportedEncoding() throws Exception {
        // Detected as HZ-GB-2312, see https://jira.atlassian.com/browse/BSERV-10865
        assertNull(EncodingUtils.detectEncoding(stream("RETURN TRIM(attribute.val, \"~{~}\")."), 36));
    }

    @Test
    public void testIsBinaryReturnsFalseForNonBinaryByteArray() {
        assertFalse(EncodingUtils.isBinary(new byte[]{'a', 'b', 'c', 'd', 'e', 'f'}));
    }

    @Test
    public void testIsBinaryReturnsFalseForNonBinaryDirectBuffer() {
        assertFalse(EncodingUtils.isBinary(directBuffer('a', 'b', 'c', 'd', 'e', 'f')));
    }

    @Test
    public void testIsBinaryReturnsFalseForNonBinaryHeapBuffer() {
        assertFalse(EncodingUtils.isBinary(heapBuffer('a', 'b', 'c', 'd', 'e', 'f')));
    }

    @Test
    public void testIsBinaryReturnsFalseForNonBinaryStream() throws Exception {
        assertFalse(EncodingUtils.isBinary(stream("abcdef"), 200));
    }

    @Test
    public void testIsBinaryReturnsFalseForUtf32ByteArray() {
        //UTF-32 contains a lot of null bytes, but it isn't binary
        assertFalse(EncodingUtils.isBinary(bytes(0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0x41)));
    }

    @Test
    public void testIsBinaryReturnsFalseForUtf32Stream() throws Exception {
        //UTF-32 contains a lot of null bytes, but it isn't binary
        assertFalse(EncodingUtils.isBinary(stream(0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0x41), 8));
    }

    @Test
    public void testIsBinaryReturnsTrueForBinaryByteArray() {
        assertTrue(EncodingUtils.isBinary(new byte[]{'a', 'b', 'c', 'd', 'e', '\0', 'f'}));
    }

    @Test
    public void testIsBinaryReturnsTrueForBinaryDirectBuffer() {
        assertTrue(EncodingUtils.isBinary(directBuffer('a', 'b', 'c', 'd', 'e', '\0', 'f')));
    }

    @Test
    public void testIsBinaryReturnsTrueForBinaryHeapBuffer() {
        assertTrue(EncodingUtils.isBinary(heapBuffer('a', 'b', 'c', 'd', 'e', '\0', 'f')));
    }

    @Test
    public void testIsBinaryReturnsTrueForBinaryStream() throws Exception {
        assertTrue(EncodingUtils.isBinary(stream("abcd\0f"), 200));
    }

    @Test
    public void testIsBinaryForStreamReadingFullBuffer() throws Exception {
        //There was a bug in isBinary(Stream) for many years where it did its read()/length check in
        //the wrong order, such that it would read one byte more than it marked. The buffer sizes we
        //use here ensure if the mark is over-read the test will fail
        assertFalse(EncodingUtils.isBinary(
                //ByteArrayInputStream is not sensitive to over-reading (the size passed to mark(int)
                //is ignored), so this uses BufferedInputStream
                new BufferedInputStream(stream("This is 22 characters."), 22), 22));
    }

    /**
     * A helper method for converting {@code int}s to {@code byte}s, rather than doing inline {@code (byte)}
     * casts in the tests.
     *
     * @param content bytes, represented as {@code int}s
     * @return the provided bytes, cast to {@code byte}
     */
    private static byte[] bytes(int... content) {
        byte[] bytes = new byte[content.length];
        for (int i = 0; i < bytes.length; ++i) {
            bytes[i] = (byte) content[i];
        }

        return bytes;
    }

    private static ByteBuffer directBuffer(int... content) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(content.length);
        buffer.put(bytes(content));
        buffer.flip();

        return buffer;
    }

    private static ByteBuffer heapBuffer(int... content) {
        byte[] bytes = bytes(content);
        return ByteBuffer.wrap(bytes);
    }

    private static InputStream stream(int... content) {
        return new ByteArrayInputStream(bytes(content));
    }

    private static InputStream stream(String content) {
        return new ByteArrayInputStream(content.getBytes(UTF_8));
    }
}