/*
 * Copyright 2014 The Netty Project
 *
 * The Netty Project 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 io.netty.handler.codec.spdy;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class SpdyHeaderBlockRawDecoderTest {

    private static final int maxHeaderSize = 16;

    private static final String name = "name";
    private static final String value = "value";
    private static final byte[] nameBytes = name.getBytes();
    private static final byte[] valueBytes = value.getBytes();

    private SpdyHeaderBlockRawDecoder decoder;
    private SpdyHeadersFrame frame;

    @Before
    public void setUp() {
        decoder = new SpdyHeaderBlockRawDecoder(SpdyVersion.SPDY_3_1, maxHeaderSize);
        frame = new DefaultSpdyHeadersFrame(1);
    }

    @After
    public void tearDown() {
        decoder.end();
    }

    @Test
    public void testEmptyHeaderBlock() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.EMPTY_BUFFER);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testZeroNameValuePairs() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(4));
        headerBlock.writeInt(0);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertFalse(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testNegativeNameValuePairs() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(4));
        headerBlock.writeInt(-1);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testOneNameValuePair() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(21));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertFalse(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().get(name));
    }

    @Test
    public void testMissingNameLength() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(4));
        headerBlock.writeInt(1);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testZeroNameLength() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(8));
        headerBlock.writeInt(1);
        headerBlock.writeInt(0);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testNegativeNameLength() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(8));
        headerBlock.writeInt(1);
        headerBlock.writeInt(-1);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testMissingName() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(8));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testIllegalNameOnlyNull() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(18));
        headerBlock.writeInt(1);
        headerBlock.writeInt(1);
        headerBlock.writeByte(0);
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testMissingValueLength() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(12));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testZeroValueLength() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(16));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(0);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertFalse(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals("", frame.headers().get(name));
    }

    @Test
    public void testNegativeValueLength() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(16));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(-1);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testMissingValue() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(16));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(5);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testIllegalValueOnlyNull() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(17));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(1);
        headerBlock.writeByte(0);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testIllegalValueStartsWithNull() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(22));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(6);
        headerBlock.writeByte(0);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testIllegalValueEndsWithNull() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(22));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(6);
        headerBlock.writeBytes(valueBytes);
        headerBlock.writeByte(0);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testMultipleValues() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(27));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(11);
        headerBlock.writeBytes(valueBytes);
        headerBlock.writeByte(0);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertFalse(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(2, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().getAll(name).get(0));
        assertEquals(value, frame.headers().getAll(name).get(1));
    }

    @Test
    public void testMultipleValuesEndsWithNull() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(28));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(12);
        headerBlock.writeBytes(valueBytes);
        headerBlock.writeByte(0);
        headerBlock.writeBytes(valueBytes);
        headerBlock.writeByte(0);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().get(name));
    }

    @Test
    public void testIllegalValueMultipleNulls() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(28));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(12);
        headerBlock.writeBytes(valueBytes);
        headerBlock.writeByte(0);
        headerBlock.writeByte(0);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testMissingNextNameValuePair() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(21));
        headerBlock.writeInt(2);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().get(name));
    }

    @Test
    public void testMultipleNames() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(38));
        headerBlock.writeInt(2);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().get(name));
    }

    @Test
    public void testExtraData() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(22));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);
        headerBlock.writeByte(0);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().get(name));
    }

    @Test
    public void testMultipleDecodes() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(21));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);

        int readableBytes = headerBlock.readableBytes();
        for (int i = 0; i < readableBytes; i++) {
            ByteBuf headerBlockSegment = headerBlock.slice(i, 1);
            decoder.decode(ByteBufAllocator.DEFAULT, headerBlockSegment, frame);
            assertFalse(headerBlockSegment.isReadable());
        }
        decoder.endHeaderBlock(frame);

        assertFalse(frame.isInvalid());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().get(name));
    }

    @Test
    public void testContinueAfterInvalidHeaders() throws Exception {
        ByteBuf numHeaders = ReferenceCountUtil.releaseLater(Unpooled.buffer(4));
        numHeaders.writeInt(1);

        ByteBuf nameBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(8));
        nameBlock.writeInt(4);
        nameBlock.writeBytes(nameBytes);

        ByteBuf valueBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(9));
        valueBlock.writeInt(5);
        valueBlock.writeBytes(valueBytes);

        decoder.decode(ByteBufAllocator.DEFAULT, numHeaders, frame);
        decoder.decode(ByteBufAllocator.DEFAULT, nameBlock, frame);
        frame.setInvalid();
        decoder.decode(ByteBufAllocator.DEFAULT, valueBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(numHeaders.isReadable());
        assertFalse(nameBlock.isReadable());
        assertFalse(valueBlock.isReadable());
        assertEquals(1, frame.headers().names().size());
        assertTrue(frame.headers().contains(name));
        assertEquals(1, frame.headers().getAll(name).size());
        assertEquals(value, frame.headers().get(name));
    }

    @Test
    public void testTruncatedHeaderName() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(maxHeaderSize + 18));
        headerBlock.writeInt(1);
        headerBlock.writeInt(maxHeaderSize + 1);
        for (int i = 0; i < maxHeaderSize + 1; i++) {
            headerBlock.writeByte('a');
        }
        headerBlock.writeInt(5);
        headerBlock.writeBytes(valueBytes);
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isTruncated());
        assertFalse(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }

    @Test
    public void testTruncatedHeaderValue() throws Exception {
        ByteBuf headerBlock = ReferenceCountUtil.releaseLater(Unpooled.buffer(maxHeaderSize + 13));
        headerBlock.writeInt(1);
        headerBlock.writeInt(4);
        headerBlock.writeBytes(nameBytes);
        headerBlock.writeInt(13);
        for (int i = 0; i < maxHeaderSize - 3; i++) {
            headerBlock.writeByte('a');
        }
        decoder.decode(ByteBufAllocator.DEFAULT, headerBlock, frame);
        decoder.endHeaderBlock(frame);

        assertFalse(headerBlock.isReadable());
        assertTrue(frame.isTruncated());
        assertFalse(frame.isInvalid());
        assertEquals(0, frame.headers().names().size());
    }
}
