//
//  ========================================================================
//  Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  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.websocket.jsr356.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.websocket.ClientEndpoint;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCode;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.ContainerProvider;
import javax.websocket.EndpointConfig;
import javax.websocket.HandshakeResponse;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.IO;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.common.test.LeakTrackingBufferPoolRule;
import org.eclipse.jetty.websocket.common.util.Hex;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;

import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.is;

public class StreamTest
{
    private static final Logger LOG = Log.getLogger(StreamTest.class);

    @Rule
    public LeakTrackingBufferPoolRule bufferPool = new LeakTrackingBufferPoolRule("Test");

    private static File outputDir;
    private static Server server;
    private static URI serverUri;

    @BeforeClass
    public static void startServer() throws Exception
    {
        server = new Server();
        ServerConnector connector = new ServerConnector(server);
        connector.setPort(0);
        server.addConnector(connector);

        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/");
        server.setHandler(context);

        ServerContainer wsContainer = WebSocketServerContainerInitializer.configureContext(context);

        // Prepare Server Side Output directory for uploaded files
        outputDir = MavenTestingUtils.getTargetTestingDir(StreamTest.class.getName());
        FS.ensureEmpty(outputDir);

        // Create Server Endpoint with output directory configuration
        ServerEndpointConfig config = ServerEndpointConfig.Builder.create(UploadSocket.class,"/upload/{filename}")
                .configurator(new ServerUploadConfigurator(outputDir)).build();
        wsContainer.addEndpoint(config);

        server.start();

        String host = connector.getHost();
        if (host == null)
        {
            host = "localhost";
        }
        int port = connector.getLocalPort();
        serverUri = new URI(String.format("ws://%s:%d/",host,port));
        if (LOG.isDebugEnabled())
            LOG.debug("Server started on {}",serverUri);
    }

    @AfterClass
    public static void stopServer() throws Exception
    {
        server.stop();
    }

    @Test
    public void testUploadSmall() throws Exception
    {
        upload("small.png");
    }

    @Test
    public void testUploadMedium() throws Exception
    {
        upload("medium.png");
    }

    @Test
    public void testUploadLarger() throws Exception
    {
        upload("larger.png");
    }

    @Test
    public void testUploadLargest() throws Exception
    {
        upload("largest.jpg");
    }

    private void upload(String filename) throws Exception
    {
        File inputFile = MavenTestingUtils.getTestResourceFile("data/" + filename);

        WebSocketContainer client = ContainerProvider.getWebSocketContainer();
        ClientSocket socket = new ClientSocket();
        URI uri = serverUri.resolve("/upload/" + filename);
        client.connectToServer(socket,uri);
        socket.uploadFile(inputFile);
        socket.awaitClose();

        File sha1File = MavenTestingUtils.getTestResourceFile("data/" + filename + ".sha");
        assertFileUpload(new File(outputDir,filename),sha1File);
    }

    /**
     * Verify that the file sha1sum matches the previously calculated sha1sum
     * 
     * @param file
     *            the file to validate
     * @param shaFile
     *            the sha1sum file to verify against
     */
    private void assertFileUpload(File file, File sha1File) throws IOException, NoSuchAlgorithmException
    {
        Assert.assertThat("Path should exist: " + file,file.exists(),is(true));
        Assert.assertThat("Path should not be a directory:" + file,file.isDirectory(),is(false));

        String expectedSha1 = loadExpectedSha1Sum(sha1File);
        String actualSha1 = calculateSha1Sum(file);

        Assert.assertThat("SHA1Sum of content: " + file,expectedSha1,equalToIgnoringCase(actualSha1));
    }

    private String calculateSha1Sum(File file) throws IOException, NoSuchAlgorithmException
    {
        MessageDigest digest = MessageDigest.getInstance("SHA1");
        try (FileInputStream fis = new FileInputStream(file);
                NoOpOutputStream noop = new NoOpOutputStream();
                DigestOutputStream digester = new DigestOutputStream(noop,digest))
        {
            IO.copy(fis,digester);
            return Hex.asHex(digest.digest());
        }
    }

    private String loadExpectedSha1Sum(File sha1File) throws IOException
    {
        String contents = IO.readToString(sha1File);
        Pattern pat = Pattern.compile("^[0-9A-Fa-f]*");
        Matcher mat = pat.matcher(contents);
        Assert.assertTrue("Should have found HEX code in SHA1 file: " + sha1File,mat.find());
        return mat.group();
    }

    @ClientEndpoint
    public static class ClientSocket
    {
        private Session session;
        private CountDownLatch closeLatch = new CountDownLatch(1);

        @OnOpen
        public void onOpen(Session session)
        {
            this.session = session;
        }

        public void close() throws IOException
        {
            this.session.close();
        }

        @OnClose
        public void onClose(CloseReason close)
        {
            closeLatch.countDown();
        }

        public void awaitClose() throws InterruptedException
        {
            Assert.assertThat("Wait for ClientSocket close success",closeLatch.await(5,TimeUnit.SECONDS),is(true));
        }

        @OnError
        public void onError(Throwable t)
        {
            t.printStackTrace(System.err);
        }

        public void uploadFile(File inputFile) throws IOException
        {
            try (OutputStream out = session.getBasicRemote().getSendStream(); FileInputStream in = new FileInputStream(inputFile))
            {
                IO.copy(in,out);
            }
        }
    }

    public static class ServerUploadConfigurator extends ServerEndpointConfig.Configurator
    {
        public static final String OUTPUT_DIR = "outputDir";
        private final File outputDir;

        public ServerUploadConfigurator(File outputDir)
        {
            this.outputDir = outputDir;
        }

        @Override
        public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response)
        {
            sec.getUserProperties().put(OUTPUT_DIR,this.outputDir);
            super.modifyHandshake(sec,request,response);
        }
    }

    @ServerEndpoint("/upload/{filename}")
    public static class UploadSocket
    {
        private File outputDir;
        private Session session;

        @OnOpen
        public void onOpen(Session session, EndpointConfig config)
        {
            this.session = session;
            // not setting max message here, as streaming handling
            // should allow any sized message.
            this.outputDir = (File)config.getUserProperties().get(ServerUploadConfigurator.OUTPUT_DIR);
        }

        @OnMessage
        public void onMessage(InputStream stream, @PathParam("filename") String filename) throws IOException
        {
            File outputFile = new File(outputDir,filename);
            CloseCode closeCode = CloseCodes.NORMAL_CLOSURE;
            String closeReason = "";
            try (FileOutputStream out = new FileOutputStream(outputFile))
            {
                IO.copy(stream,out);
                if (outputFile.exists())
                {
                    closeReason = String.format("Received %,d bytes",outputFile.length());
                    if (LOG.isDebugEnabled())
                        LOG.debug(closeReason);
                }
                else
                {
                    LOG.warn("Uploaded file does not exist: " + outputFile);
                }
            }
            catch (IOException e)
            {
                e.printStackTrace(System.err);
                closeReason = "Error writing file";
                closeCode = CloseCodes.UNEXPECTED_CONDITION;
            }
            finally
            {
                session.close(new CloseReason(closeCode,closeReason));
            }
        }

        @OnError
        public void onError(Throwable t)
        {
            t.printStackTrace(System.err);
        }
    }

    private static class NoOpOutputStream extends OutputStream
    {
        @Override
        public void write(byte[] b) throws IOException
        {
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException
        {
        }

        @Override
        public void flush() throws IOException
        {
        }

        @Override
        public void close() throws IOException
        {
        }

        @Override
        public void write(int b) throws IOException
        {
        }
    }
}
