//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.http2.tests;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PrefaceFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.internal.HttpConnection;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class HTTP2CServerTest extends AbstractServerTest
{
    @BeforeEach
    public void before() throws Exception
    {
        server = new HTTP2CServer(0);
        server.start();
        connector = (ServerConnector)server.getConnectors()[0];
    }

    @AfterEach
    public void after() throws Exception
    {
        server.stop();
    }

    @Test
    public void testHTTP10Simple() throws Exception
    {
        try (Socket client = new Socket("localhost", connector.getLocalPort()))
        {
            client.setSoTimeout(5000);

            client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
            client.getOutputStream().flush();

            String response = IO.toString(client.getInputStream());

            assertThat(response, containsString("HTTP/1.1 200 OK"));
            assertThat(response, containsString("Hello from Jetty using HTTP/1.0"));
        }
    }

    @Test
    public void testHTTP11Simple() throws Exception
    {
        try (Socket client = new Socket("localhost", connector.getLocalPort()))
        {
            client.setSoTimeout(5000);

            client.getOutputStream().write("GET /one HTTP/1.1\r\nHost: localhost\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
            client.getOutputStream().write("GET /two HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
            client.getOutputStream().flush();

            String response = IO.toString(client.getInputStream());

            assertThat(response, containsString("HTTP/1.1 200 OK"));
            assertThat(response, containsString("Hello from Jetty using HTTP/1.1"));
            assertThat(response, containsString("uri=/one"));
            assertThat(response, containsString("uri=/two"));
        }
    }

    @Test
    public void testHTTP11Upgrade() throws Exception
    {
        try (Socket client = new Socket("localhost", connector.getLocalPort()))
        {
            client.setSoTimeout(5000);

            OutputStream output = client.getOutputStream();
            output.write((
                "GET /one HTTP/1.1\r\n" +
                    "Host: localhost\r\n" +
                    "Connection: something, else, upgrade, HTTP2-Settings\r\n" +
                    "Upgrade: h2c\r\n" +
                    "HTTP2-Settings: AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA\r\n" +
                    "\r\n").getBytes(StandardCharsets.ISO_8859_1));
            output.flush();

            InputStream input = client.getInputStream();
            Utf8StringBuilder upgrade = new Utf8StringBuilder();
            int crlfs = 0;
            while (true)
            {
                int read = input.read();
                if (read == '\r' || read == '\n')
                    ++crlfs;
                else
                    crlfs = 0;
                upgrade.append((byte)read);
                if (crlfs == 4)
                    break;
            }

            assertTrue(upgrade.toCompleteString().startsWith("HTTP/1.1 101 "));

            bufferPool = new ArrayByteBufferPool();
            generator = new Generator(bufferPool);

            final AtomicReference<HeadersFrame> headersRef = new AtomicReference<>();
            final AtomicReference<DataFrame> dataRef = new AtomicReference<>();
            final AtomicReference<CountDownLatch> latchRef = new AtomicReference<>(new CountDownLatch(2));
            Parser parser = new Parser(bufferPool, 8192);
            parser.init(new Parser.Listener()
            {
                @Override
                public void onHeaders(HeadersFrame frame)
                {
                    headersRef.set(frame);
                    latchRef.get().countDown();
                }

                @Override
                public void onData(DataFrame frame)
                {
                    dataRef.set(frame);
                    latchRef.get().countDown();
                }
            });

            parseResponse(client, parser);

            assertTrue(latchRef.get().await(5, TimeUnit.SECONDS));

            HeadersFrame response = headersRef.get();
            assertNotNull(response);
            MetaData.Response responseMetaData = (MetaData.Response)response.getMetaData();
            assertEquals(200, responseMetaData.getStatus());

            DataFrame responseData = dataRef.get();
            assertNotNull(responseData);

            String content = BufferUtil.toString(responseData.getByteBuffer());

            // The upgrade request is seen as HTTP/1.1.
            assertThat(content, containsString("Hello from Jetty using HTTP/2.0"));
            assertThat(content, containsString("uri=/one"));

            // Send an HTTP/2 request.
            headersRef.set(null);
            dataRef.set(null);
            latchRef.set(new CountDownLatch(2));
            ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
            generator.control(accumulator, new PrefaceFrame());
            generator.control(accumulator, new SettingsFrame(new HashMap<>(), false));
            MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP.asString(), new HostPortHttpField("localhost:" + connector.getLocalPort()), "/two", HttpVersion.HTTP_2, HttpFields.EMPTY, -1);
            generator.control(accumulator, new HeadersFrame(3, metaData, null, true));
            for (ByteBuffer buffer : accumulator.getByteBuffers())
            {
                output.write(BufferUtil.toArray(buffer));
            }
            output.flush();

            parseResponse(client, parser);

            assertTrue(latchRef.get().await(5, TimeUnit.SECONDS));

            response = headersRef.get();
            assertNotNull(response);
            responseMetaData = (MetaData.Response)response.getMetaData();
            assertEquals(200, responseMetaData.getStatus());

            responseData = dataRef.get();
            assertNotNull(responseData);

            content = BufferUtil.toString(responseData.getByteBuffer());

            assertThat(content, containsString("Hello from Jetty using HTTP/2.0"));
            assertThat(content, containsString("uri=/two"));
        }
    }

    @Test
    public void testHTTP20Direct() throws Exception
    {
        final CountDownLatch latch = new CountDownLatch(3);

        bufferPool = new ArrayByteBufferPool();
        generator = new Generator(bufferPool);

        ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
        generator.control(accumulator, new PrefaceFrame());
        generator.control(accumulator, new SettingsFrame(new HashMap<>(), false));
        MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP.asString(), new HostPortHttpField("localhost:" + connector.getLocalPort()), "/test", HttpVersion.HTTP_2, HttpFields.EMPTY, -1);
        generator.control(accumulator, new HeadersFrame(1, metaData, null, true));

        try (Socket client = new Socket("localhost", connector.getLocalPort()))
        {
            client.setSoTimeout(5000);

            OutputStream output = client.getOutputStream();
            for (ByteBuffer buffer : accumulator.getByteBuffers())
            {
                output.write(BufferUtil.toArray(buffer));
            }

            final AtomicReference<HeadersFrame> headersRef = new AtomicReference<>();
            final AtomicReference<DataFrame> dataRef = new AtomicReference<>();
            Parser parser = new Parser(bufferPool, 8192);
            parser.init(new Parser.Listener()
            {
                @Override
                public void onSettings(SettingsFrame frame)
                {
                    latch.countDown();
                }

                @Override
                public void onHeaders(HeadersFrame frame)
                {
                    headersRef.set(frame);
                    latch.countDown();
                }

                @Override
                public void onData(DataFrame frame)
                {
                    dataRef.set(frame);
                    latch.countDown();
                }
            });

            parseResponse(client, parser);

            assertTrue(latch.await(5, TimeUnit.SECONDS));

            HeadersFrame response = headersRef.get();
            assertNotNull(response);
            MetaData.Response responseMetaData = (MetaData.Response)response.getMetaData();
            assertEquals(200, responseMetaData.getStatus());

            DataFrame responseData = dataRef.get();
            assertNotNull(responseData);

            String s = BufferUtil.toString(responseData.getByteBuffer());

            assertThat(s, containsString("Hello from Jetty using HTTP/2.0"));
            assertThat(s, containsString("uri=/test"));
        }
    }

    @Test
    public void testHTTP20DirectWithoutH2C() throws Exception
    {
        AtomicLong fills = new AtomicLong();
        // Remove "h2c", leaving only "http/1.1".
        connector.stop();
        connector.removeConnectionFactory("h2c");
        connector.start();
        HttpConnectionFactory connectionFactory = new HttpConnectionFactory()
        {
            @Override
            public Connection newConnection(Connector connector, EndPoint endPoint)
            {
                HttpConnection connection = new HttpConnection(getHttpConfiguration(), connector, endPoint)
                {
                    @Override
                    public void onFillable()
                    {
                        fills.incrementAndGet();
                        super.onFillable();
                    }
                };
                return configure(connection, connector, endPoint);
            }
        };
        connector.stop();
        connector.addConnectionFactory(connectionFactory);
        connector.setDefaultProtocol(connectionFactory.getProtocol());
        connector.start();

        // Now send an HTTP/2 direct request, which
        // will have the PRI * HTTP/2.0 preface.

        bufferPool = new ArrayByteBufferPool();
        generator = new Generator(bufferPool);

        ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
        generator.control(accumulator, new PrefaceFrame());

        try (Socket client = new Socket("localhost", connector.getLocalPort()))
        {
            client.setSoTimeout(5000);

            OutputStream output = client.getOutputStream();
            for (ByteBuffer buffer : accumulator.getByteBuffers())
            {
                output.write(BufferUtil.toArray(buffer));
            }

            // We sent an HTTP/2 preface, but the server has no "h2c" connection
            // factory so it does not know how to handle this request.

            InputStream input = client.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
            String responseLine = reader.readLine();
            assertThat(responseLine, Matchers.containsString(" 426 "));
            while (true)
            {
                if (reader.read() < 0)
                    break;
            }
        }

        // Make sure we did not spin.
        Thread.sleep(1000);
        assertThat(fills.get(), Matchers.lessThan(5L));
    }
}
