//
//  ========================================================================
//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.http2.hpack;

import java.nio.ByteBuffer;

import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.compression.EncodingException;
import org.eclipse.jetty.http.compression.HuffmanDecoder;
import org.eclipse.jetty.http.compression.NBitIntegerDecoder;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 *
 */
public class HpackContextTest
{
    public static String decode(ByteBuffer buffer, int length) throws EncodingException
    {
        HuffmanDecoder huffmanDecoder = new HuffmanDecoder();
        huffmanDecoder.setLength(length);
        String decoded = huffmanDecoder.decode(buffer);
        if (decoded == null)
            throw new EncodingException("invalid string encoding");

        huffmanDecoder.reset();
        return decoded;
    }

    public static int decodeInt(ByteBuffer buffer, int prefix) throws EncodingException
    {
        // This is a fix for HPACK as it already takes the first byte of the encoded integer.
        if (prefix != 8)
            buffer.position(buffer.position() - 1);

        NBitIntegerDecoder decoder = new NBitIntegerDecoder();
        decoder.setPrefix(prefix);
        int decodedInt = decoder.decodeInt(buffer);
        if (decodedInt < 0)
            throw new EncodingException("invalid integer encoding");
        decoder.reset();
        return decodedInt;
    }

    @Test
    public void testStaticName()
    {
        HpackContext ctx = new HpackContext(4096);
        Entry entry = ctx.get(":method");
        assertEquals(":method", entry.getHttpField().getName());
        assertTrue(entry.isStatic());
        assertThat(entry.toString(), Matchers.startsWith("{S,2,:method: "));
    }

    @Test
    public void testEmptyAdd()
    {
        HpackContext ctx = new HpackContext(0);
        HttpField field = new HttpField("foo", "bar");
        assertNull(ctx.add(field));
    }

    @Test
    public void testTooBigAdd()
    {
        HpackContext ctx = new HpackContext(37);
        HttpField field = new HttpField("foo", "bar");
        assertNull(ctx.add(field));
    }

    @Test
    public void testJustRight()
    {
        HpackContext ctx = new HpackContext(38);
        HttpField field = new HttpField("foo", "bar");
        Entry entry = ctx.add(field);
        assertNotNull(entry);
        assertThat(entry.toString(), Matchers.startsWith("{D,0,foo: bar,"));
    }

    @Test
    public void testEvictOne()
    {
        HpackContext ctx = new HpackContext(38);
        HttpField field0 = new HttpField("foo", "bar");

        assertEquals(field0, ctx.add(field0).getHttpField());
        assertEquals(field0, ctx.get("foo").getHttpField());

        HttpField field1 = new HttpField("xxx", "yyy");
        assertEquals(field1, ctx.add(field1).getHttpField());

        assertNull(ctx.get(field0));
        assertNull(ctx.get("foo"));
        assertEquals(field1, ctx.get(field1).getHttpField());
        assertEquals(field1, ctx.get("xxx").getHttpField());
    }

    @Test
    public void testEvictNames()
    {
        HpackContext ctx = new HpackContext(38 * 2);
        HttpField[] field =
            {
                new HttpField("name", "v0"),
                new HttpField("name", "v1"),
                new HttpField("name", "v2"),
                new HttpField("name", "v3"),
                new HttpField("name", "v4"),
                new HttpField("name", "v5"),
            };

        Entry[] entry = new Entry[field.length];

        // Add 2 name entries to fill table
        for (int i = 0; i <= 1; i++)
        {
            entry[i] = ctx.add(field[i]);
        }

        // check there is a name reference and it is the most recent added
        assertEquals(entry[1], ctx.get("name"));

        // Add 1 other entry to table and evict 1
        ctx.add(new HttpField("xxx", "yyy"));

        // check the name reference has been not been evicted
        assertEquals(entry[1], ctx.get("name"));

        // Add 1 other entry to table and evict 1
        ctx.add(new HttpField("foo", "bar"));

        // name is evicted
        assertNull(ctx.get("name"));
    }

    @Test
    @SuppressWarnings("ReferenceEquality")
    public void testGetAddStatic()
    {
        HpackContext ctx = new HpackContext(4096);

        // Look for the field.  Should find static version.
        HttpField methodGet = new HttpField(":method", "GET");
        assertEquals(methodGet, ctx.get(methodGet).getHttpField());
        assertTrue(ctx.get(methodGet).isStatic());

        // Add static version to dynamic table
        Entry e0 = ctx.add(ctx.get(methodGet).getHttpField());

        // Look again and should see dynamic version
        assertEquals(methodGet, ctx.get(methodGet).getHttpField());
        assertFalse(methodGet == ctx.get(methodGet).getHttpField());
        assertFalse(ctx.get(methodGet).isStatic());

        // Duplicates allows
        Entry e1 = ctx.add(ctx.get(methodGet).getHttpField());

        // Look again and should see dynamic version
        assertEquals(methodGet, ctx.get(methodGet).getHttpField());
        assertFalse(methodGet == ctx.get(methodGet).getHttpField());
        assertFalse(ctx.get(methodGet).isStatic());
        assertFalse(e0 == e1);
    }

    @Test
    public void testGetAddStaticName()
    {
        HpackContext ctx = new HpackContext(4096);
        HttpField methodOther = new HttpField(":method", "OTHER");

        // Look for the field by name.  Should find static version.
        assertEquals(":method", ctx.get(":method").getHttpField().getName());
        assertTrue(ctx.get(":method").isStatic());

        // Add dynamic entry with method
        ctx.add(methodOther);

        // Look for the field by name.  Should find static version.
        assertEquals(":method", ctx.get(":method").getHttpField().getName());
        assertTrue(ctx.get(":method").isStatic());
    }

    @Test
    public void testIndexes()
    {
        // Only enough space for 5 entries
        HpackContext ctx = new HpackContext(38 * 5);

        HttpField methodPost = new HttpField(":method", "POST");
        HttpField[] field =
            {
                new HttpField("fo0", "b0r"),
                new HttpField("fo1", "b1r"),
                new HttpField("fo2", "b2r"),
                new HttpField("fo3", "b3r"),
                new HttpField("fo4", "b4r"),
                new HttpField("fo5", "b5r"),
                new HttpField("fo6", "b6r"),
                new HttpField("fo7", "b7r"),
                new HttpField("fo8", "b8r"),
                new HttpField("fo9", "b9r"),
                new HttpField("foA", "bAr"),
            };

        Entry[] entry = new Entry[100];

        // Lookup the index of a static field
        assertEquals(0, ctx.size());
        assertEquals(":authority", ctx.get(1).getHttpField().getName());
        assertEquals(3, ctx.index(ctx.get(methodPost)));
        assertEquals(methodPost, ctx.get(3).getHttpField());
        assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
        assertEquals(null, ctx.get(62));

        // Add a single entry  
        entry[0] = ctx.add(field[0]);

        // Check new entry is 62 
        assertEquals(1, ctx.size());
        assertEquals(62, ctx.index(entry[0]));
        assertEquals(entry[0], ctx.get(62));

        // and statics still OK
        assertEquals(":authority", ctx.get(1).getHttpField().getName());
        assertEquals(3, ctx.index(ctx.get(methodPost)));
        assertEquals(methodPost, ctx.get(3).getHttpField());
        assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
        assertEquals(null, ctx.get(62 + ctx.size()));

        // Add 4 more entries
        for (int i = 1; i <= 4; i++)
        {
            entry[i] = ctx.add(field[i]);
        }

        // Check newest entry is at 62 oldest at 66
        assertEquals(5, ctx.size());
        int index = 66;
        for (int i = 0; i <= 4; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }

        // and statics still OK
        assertEquals(":authority", ctx.get(1).getHttpField().getName());
        assertEquals(3, ctx.index(ctx.get(methodPost)));
        assertEquals(methodPost, ctx.get(3).getHttpField());
        assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
        assertEquals(null, ctx.get(62 + ctx.size()));

        // add 1 more entry and this should cause an eviction!
        entry[5] = ctx.add(field[5]);

        // Check newest entry is at 1 oldest at 5
        index = 66;
        for (int i = 1; i <= 5; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }
        // check entry 0 evicted
        assertNull(ctx.get(field[0]));
        assertEquals(0, ctx.index(entry[0]));

        // and statics still OK
        assertEquals(":authority", ctx.get(1).getHttpField().getName());
        assertEquals(3, ctx.index(ctx.get(methodPost)));
        assertEquals(methodPost, ctx.get(3).getHttpField());
        assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
        assertEquals(null, ctx.get(62 + ctx.size()));

        // Add 4 more entries
        for (int i = 6; i <= 9; i++)
        {
            entry[i] = ctx.add(field[i]);
        }

        // Check newest entry is at 1 oldest at 5
        index = 66;
        for (int i = 5; i <= 9; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }
        // check entry 0-4 evicted
        for (int i = 0; i <= 4; i++)
        {
            assertNull(ctx.get(field[i]));
            assertEquals(0, ctx.index(entry[i]));
        }

        // Add new entries enough so that array queue will wrap
        for (int i = 10; i <= 52; i++)
        {
            entry[i] = ctx.add(new HttpField("n" + i, "v" + i));
        }

        index = 66;
        for (int i = 48; i <= 52; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }
    }

    @Test
    public void testResize()
    {
        // Only enough space for 5 entries
        HpackContext ctx = new HpackContext(38 * 5);

        HttpField[] field =
            {
                new HttpField("fo0", "b0r"),
                new HttpField("fo1", "b1r"),
                new HttpField("fo2", "b2r"),
                new HttpField("fo3", "b3r"),
                new HttpField("fo4", "b4r"),
                new HttpField("fo5", "b5r"),
                new HttpField("fo6", "b6r"),
                new HttpField("fo7", "b7r"),
                new HttpField("fo8", "b8r"),
                new HttpField("fo9", "b9r"),
                new HttpField("foA", "bAr"),
            };
        Entry[] entry = new Entry[field.length];

        // Add 5 entries
        for (int i = 0; i <= 4; i++)
        {
            entry[i] = ctx.add(field[i]);
        }

        assertEquals(5, ctx.size());

        // check indexes
        int index = 66;
        for (int i = 0; i <= 4; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }

        // resize so that only 2 entries may be held
        ctx.resize(38 * 2);
        assertEquals(2, ctx.size());

        // check indexes
        index = 63;
        for (int i = 3; i <= 4; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }

        // resize so that 6.5 entries may be held
        ctx.resize(38 * 6 + 19);
        assertEquals(2, ctx.size());

        // check indexes
        index = 63;
        for (int i = 3; i <= 4; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }

        // Add 5 entries
        for (int i = 5; i <= 9; i++)
        {
            entry[i] = ctx.add(field[i]);
        }

        assertEquals(6, ctx.size());

        // check indexes
        index = 67;
        for (int i = 4; i <= 9; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }

        // resize so that only 100 entries may be held
        ctx.resize(38 * 100);
        assertEquals(6, ctx.size());
        // check indexes
        index = 67;
        for (int i = 4; i <= 9; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }

        // add 50 fields
        for (int i = 0; i < 50; i++)
        {
            ctx.add(new HttpField("n" + i, "v" + i));
        }

        // check indexes
        index = 67 + 50;
        for (int i = 4; i <= 9; i++)
        {
            assertEquals(index, ctx.index(entry[i]));
            assertEquals(entry[i], ctx.get(index));
            index--;
        }
    }

    @Test
    public void testStaticHuffmanValues() throws Exception
    {
        HpackContext ctx = new HpackContext(4096);
        for (int i = 2; i <= 14; i++)
        {
            Entry entry = ctx.get(i);
            assertTrue(entry.isStatic());

            ByteBuffer buffer = ByteBuffer.wrap(entry.getStaticHuffmanValue());
            int huff = 0xff & buffer.get();
            assertTrue((0x80 & huff) == 0x80);

            int len = decodeInt(buffer, 7);

            assertEquals(len, buffer.remaining());
            String value = decode(buffer, buffer.remaining());

            assertEquals(entry.getHttpField().getValue(), value);
        }
    }

    @Test
    public void testNameInsensitivity()
    {
        HpackContext ctx = new HpackContext(4096);
        assertEquals("content-length", ctx.get("content-length").getHttpField().getName());
        assertEquals("content-length", ctx.get("Content-Length").getHttpField().getName());
        assertTrue(ctx.get("Content-Length").isStatic());
        assertTrue(ctx.get("Content-Type").isStatic());

        ctx.add(new HttpField("Wibble", "Wobble"));
        assertEquals("Wibble", ctx.get("wibble").getHttpField().getName());
        assertEquals("Wibble", ctx.get("Wibble").getHttpField().getName());
    }
}
