/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.test.integration.domain.suites;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.COMPOSITE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.EXTENSION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.PROFILE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVER;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STEPS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SYSTEM_PROPERTY;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VALUE;
import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.as.controller.client.Operation;
import org.jboss.as.controller.client.OperationBuilder;
import org.jboss.as.controller.client.OperationMessageHandler;
import org.jboss.as.controller.client.OperationResponse;
import org.jboss.as.controller.client.helpers.domain.DomainClient;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.protocol.StreamUtils;
import org.jboss.as.test.integration.domain.extension.ExtensionSetup;
import org.jboss.as.test.integration.domain.management.util.DomainLifecycleUtil;
import org.jboss.as.test.integration.domain.management.util.DomainTestSupport;
import org.jboss.as.test.integration.domain.management.util.DomainTestUtils;
import org.jboss.as.test.integration.management.extension.streams.LogStreamExtension;
import org.jboss.as.test.integration.management.util.MgmtOperationException;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.logging.Logger;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Tests of propagating response streams around a domain.
 *
 * @author Brian Stansberry (c) 2014 Red Hat Inc.
 */
public class ResponseStreamTestCase {
    private static final Logger log = Logger.getLogger(ResponseStreamTestCase.class);

    private static final int MGMT_PORT = 9990;
    private static final String MGMT_CTX = "/management";
    private static final String QUERY_PARAM = "useStreamAsResponse";
    private static final String APPLICATION_JSON = "application/json";

    private static DomainTestSupport testSupport;
    private static DomainClient masterClient;
    private static DomainClient slaveClient;

    @BeforeClass
    public static void setupDomain() throws Exception {
        testSupport = DomainTestSuite.createSupport(ResponseStreamTestCase.class.getSimpleName());
        masterClient = testSupport.getDomainMasterLifecycleUtil().getDomainClient();
        slaveClient = testSupport.getDomainSlaveLifecycleUtil().getDomainClient();
        // Initialize the test extension
        ExtensionSetup.initializeLogStreamExtension(testSupport);

        ModelNode addExtension = Util.createAddOperation(PathAddress.pathAddress(EXTENSION, LogStreamExtension.MODULE_NAME));

        executeForResult(addExtension, masterClient);

        ModelNode addSubsystem = Util.createAddOperation(PathAddress.pathAddress(
                PathElement.pathElement(PROFILE, "default"),
                PathElement.pathElement(SUBSYSTEM, LogStreamExtension.SUBSYSTEM_NAME)));
        executeForResult(addSubsystem, masterClient);
    }

    @AfterClass
    public static void tearDownDomain() throws Exception {
        ModelNode removeSubsystem = Util.createEmptyOperation(REMOVE, PathAddress.pathAddress(
                PathElement.pathElement(PROFILE, "default"),
                PathElement.pathElement(SUBSYSTEM, LogStreamExtension.SUBSYSTEM_NAME)));
        executeForResult(removeSubsystem, masterClient);

        ModelNode removeExtension = Util.createEmptyOperation(REMOVE, PathAddress.pathAddress(EXTENSION, LogStreamExtension.MODULE_NAME));
        executeForResult(removeExtension, masterClient);

        testSupport = null;
        masterClient = null;
        slaveClient = null;
        DomainTestSuite.stopSupport();
    }

    private String logMessageContent;
    private CloseableHttpClient httpClient;

    @Before
    public void before() throws IOException {

        logMessageContent = String.valueOf(System.currentTimeMillis());
        ModelNode opNode = Util.createAddOperation(PathAddress.pathAddress(SYSTEM_PROPERTY, LogStreamExtension.LOG_MESSAGE_PROP));
        opNode.get(VALUE).set(logMessageContent);
        Operation op = OperationBuilder.create(opNode).build();
        masterClient.executeOperation(op, OperationMessageHandler.DISCARD);
    }

    @After
    public void after() throws IOException {
        ModelNode opNode = Util.createEmptyOperation(REMOVE, PathAddress.pathAddress(SYSTEM_PROPERTY, LogStreamExtension.LOG_MESSAGE_PROP));
        Operation op = OperationBuilder.create(opNode).build();
        masterClient.executeOperation(op, OperationMessageHandler.DISCARD);

        shutdownHttpClient();

    }

    private void shutdownHttpClient() {
        if (httpClient != null) {
            try {
                // shut down the connection manager to ensure
                // immediate deallocation of all system resources
                httpClient.close();
            } catch (Exception e) {
                log.error(e);
            } finally {
                httpClient = null;
            }
        }
    }

    @Test
    public void testMasterHost() throws IOException {
        PathAddress base = PathAddress.pathAddress(PROFILE, "default");
        readLogFile(createReadAttributeOp(base), masterClient, false);
    }

    @Test
    public void testSlaveHost() throws IOException {
        PathAddress base = PathAddress.pathAddress(PROFILE, "default");
        readLogFile(createReadAttributeOp(base), slaveClient, false);
    }

    @Test
    public void testMasterServer() throws IOException {
        PathAddress base = PathAddress.pathAddress(HOST, "master").append(SERVER, "main-one");
        readLogFile(createReadAttributeOp(base), masterClient, true);
        readLogFile(createOperationOp(base), masterClient, true);
    }

    @Test
    public void testSlaveServer() throws IOException {
        PathAddress base = PathAddress.pathAddress(HOST, "slave").append(SERVER, "main-three");
        readLogFile(createReadAttributeOp(base), masterClient, true);
        readLogFile(createOperationOp(base), masterClient, true);
        readLogFile(createReadAttributeOp(base), slaveClient, true);
        readLogFile(createOperationOp(base), slaveClient, true);
    }

    @Test
    public void testComposite() throws IOException {
        ModelNode composite = Util.createEmptyOperation(COMPOSITE, PathAddress.EMPTY_ADDRESS);
        ModelNode steps = composite.get(STEPS);
        steps.add(createReadAttributeOp(PathAddress.pathAddress(PROFILE, "default")));
        steps.add(createReadAttributeOp(PathAddress.pathAddress(HOST, "master").append(SERVER, "main-one")));
        steps.add(createReadAttributeOp(PathAddress.pathAddress(HOST, "slave").append(SERVER, "main-three")));
        Operation op = OperationBuilder.create(composite).build();
        OperationResponse response = null;
        try {
            response = masterClient.executeOperation(op, OperationMessageHandler.DISCARD);

            ModelNode respNode = response.getResponseNode();
            System.out.println(respNode.toString());
            Assert.assertEquals(respNode.toString(), "success", respNode.get("outcome").asString());
            List<? extends OperationResponse.StreamEntry> streams = response.getInputStreams();
            //Assert.assertEquals(3, streams.size());

            ModelNode result0 = respNode.get(RESULT, "step-1", RESULT);
            Assert.assertEquals(ModelType.STRING, result0.getType());
            String uuid = result0.asString();
            processResponseStream(response, uuid, false, true);

            ModelNode result1 = respNode.get(RESULT, "step-2", RESULT);
            Assert.assertEquals(ModelType.STRING, result1.getType());
            uuid = result1.asString();
            processResponseStream(response, uuid, true, false);

            ModelNode result2 = respNode.get(RESULT, "step-3", RESULT);
            Assert.assertEquals(ModelType.STRING, result2.getType());
            uuid = result2.asString();
            processResponseStream(response, uuid, true, false);

        } finally {
            StreamUtils.safeClose(response);
        }
    }

    @Test
    public void testGetWithQueryParameter() throws Exception {
        URL url = buildURL(true, true, null);
        HttpGet httpget = new HttpGet(url.toURI());
        HttpResponse response = getHttpClient(url).execute(httpget);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("text/plain"));
    }

    @Test
    public void testGetWithSpecifiedQueryParameter() throws Exception {
        URL url = buildURL(true, true, 0);
        HttpGet httpget = new HttpGet(url.toURI());
        readHttpResponse(getHttpClient(url).execute(httpget), 200);
    }

    @Test
    public void testGetWithIncorrectQueryParameter() throws Exception {
        URL url = buildURL(true, true, 1);
        HttpGet httpget = new HttpGet(url.toURI());
        readHttpResponse(getHttpClient(url).execute(httpget), 400);
    }

    @Test
    public void testGetWithHttpHeader() throws Exception {
        URL url = buildURL(true, false, null);
        HttpGet httpget = new HttpGet(url.toURI());
        httpget.setHeader("org.wildfly.useStreamAsResponse", null);
        readHttpResponse(getHttpClient(url).execute(httpget), 200);
    }

    @Test
    public void testGetWithSpecifiedHttpHeader() throws Exception {
        URL url = buildURL(true, false, null);
        HttpGet httpget = new HttpGet(url.toURI());
        httpget.setHeader("org.wildfly.useStreamAsResponse", "0");
        readHttpResponse(getHttpClient(url).execute(httpget), 200);
    }

    @Test
    public void testGetWithIncorrectHttpHeader() throws Exception {
        URL url = buildURL(true, false, null);
        HttpGet httpget = new HttpGet(url.toURI());
        httpget.setHeader("org.wildfly.useStreamAsResponse", "1");
        readHttpResponse(getHttpClient(url).execute(httpget), 400);
    }

    @Test
    public void testGetWithMatchedContentType() throws Exception {
        URL url = buildURL(true, true, null);
        HttpGet httpget = new HttpGet(url.toURI());
        httpget.setHeader("Accept", "text/plain");
        HttpResponse response = getHttpClient(url).execute(httpget);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("text/plain"));
    }

    @Test
    public void testGetWithUnmatchedContentType() throws Exception {
        URL url = buildURL(true, true, null);
        HttpGet httpget = new HttpGet(url.toURI());
        httpget.setHeader("Accept", "text/html");
        HttpResponse response = getHttpClient(url).execute(httpget);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("application/octet-stream"));
    }

    @Test
    public void testGetWithUnmatchedOctetStreamContentType() throws Exception {
        URL url = buildURL(true, true, null);
        HttpGet httpget = new HttpGet(url.toURI());
        httpget.setHeader("Accept", "application/octet-stream");
        HttpResponse response = getHttpClient(url).execute(httpget);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("application/octet-stream"));
    }

    @Test
    public void testPostWithQueryParameter() throws Exception {
        URL url = buildURL(false, true, null);
        HttpPost httpPost = getHttpPost(url);
        HttpResponse response = getHttpClient(url).execute(httpPost);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("text/plain"));
    }

    @Test
    public void testPostWithSpecifiedQueryParameter() throws Exception {
        URL url = buildURL(false, true, 0);
        HttpPost httpPost = getHttpPost(url);
        readHttpResponse(getHttpClient(url).execute(httpPost), 200);
    }

    @Test
    public void testPostWithIncorrectQueryParameter() throws Exception {
        URL url = buildURL(false, true, 1);
        HttpPost httpPost = getHttpPost(url);
        readHttpResponse(getHttpClient(url).execute(httpPost), 400);
    }

    @Test
    public void testPostWithHttpHeader() throws Exception {
        URL url = buildURL(false, false, null);
        HttpPost httpPost = getHttpPost(url);
        httpPost.setHeader("org.wildfly.useStreamAsResponse", null);
        readHttpResponse(getHttpClient(url).execute(httpPost), 200);
    }

    @Test
    public void testPostWithSpecifiedHttpHeader() throws Exception {
        URL url = buildURL(false, false, null);
        HttpPost httpPost = getHttpPost(url);
        httpPost.setHeader("org.wildfly.useStreamAsResponse", "0");
        readHttpResponse(getHttpClient(url).execute(httpPost), 200);
    }

    @Test
    public void testPostWithIncorrectHttpHeader() throws Exception {
        URL url = buildURL(false, false, null);
        HttpPost httpPost = getHttpPost(url);
        httpPost.setHeader("org.wildfly.useStreamAsResponse", "1");
        readHttpResponse(getHttpClient(url).execute(httpPost), 400);
    }

    @Test
    public void testPostWithMatchedContentType() throws Exception {
        URL url = buildURL(false, true, null);
        HttpPost httpPost = getHttpPost(url);
        httpPost.setHeader("Accept", "text/plain");
        HttpResponse response = getHttpClient(url).execute(httpPost);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("text/plain"));
    }

    @Test
    public void testPostWithUnmatchedContentType() throws Exception {
        URL url = buildURL(false, true, null);
        HttpPost httpPost = getHttpPost(url);
        httpPost.setHeader("Accept", "text/html");
        HttpResponse response = getHttpClient(url).execute(httpPost);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("application/octet-stream"));
    }

    @Test
    public void testPostWithUnmatchedOctetStreamContentType() throws Exception {
        URL url = buildURL(false, true, null);
        HttpPost httpPost = getHttpPost(url);
        httpPost.setHeader("Accept", "application/octet-stream");
        HttpResponse response = getHttpClient(url).execute(httpPost);
        readHttpResponse(response, 200);

        String contentType = response.getEntity().getContentType().getValue();
        Assert.assertTrue(contentType, contentType.contains("application/octet-stream"));
    }

    private HttpClient getHttpClient(URL url) {
           shutdownHttpClient();
           CredentialsProvider credsProvider = new BasicCredentialsProvider();
           credsProvider.setCredentials(new AuthScope(url.getHost(), url.getPort(), "ManagementRealm", AuthSchemes.DIGEST),
                   new UsernamePasswordCredentials(DomainLifecycleUtil.SLAVE_HOST_USERNAME, DomainLifecycleUtil.SLAVE_HOST_PASSWORD));

           httpClient = HttpClientBuilder.create()
                   .setDefaultCredentialsProvider(credsProvider)
                   .build();

           return httpClient;
       }

    private URL buildURL(boolean forGet, boolean useHeader, Integer streamIndex) throws MalformedURLException {
        String filePart;
        if (forGet) {
            filePart = MGMT_CTX + "/host/slave/server/main-three/subsystem/log-stream-test?operation=attribute&name=log-file";
            if (useHeader) {
                filePart += "&" + getQueryParameter(streamIndex);
            }
        } else if (useHeader) {
            filePart = MGMT_CTX + "?" + getQueryParameter(streamIndex);
        } else {
            filePart = MGMT_CTX;
        }
        return new URL("http", DomainTestSupport.masterAddress, MGMT_PORT, filePart);
    }

    private static String getQueryParameter(Integer streamIndex) {
        String result = QUERY_PARAM;
        if (streamIndex != null) {
            result += "=" + streamIndex;
        }
        return result;
    }

    private HttpPost getHttpPost(URL url) throws URISyntaxException, UnsupportedEncodingException {
        // For POST we are using the custom op instead read-attribute that we use for GET
        // but this is just a convenient way to exercise the op (GET can't call custom ops),
        // and isn't some limitation of POST
        PathAddress base = PathAddress.pathAddress(HOST, "slave").append(SERVER, "main-three");
        ModelNode cmd = createReadAttributeOp(base);
        String cmdStr = cmd.toJSONString(true);
        HttpPost post = new HttpPost(url.toURI());
        StringEntity entity = new StringEntity(cmdStr);
        entity.setContentType(APPLICATION_JSON);
        post.setEntity(entity);

        return post;
    }

    private void readHttpResponse(HttpResponse response, int expectedStatus) throws IOException {

        StatusLine statusLine = response.getStatusLine();
        assertEquals(expectedStatus, statusLine.getStatusCode());

        if (expectedStatus == 200) {
            HttpEntity entity = response.getEntity();

            readLogStream(entity.getContent(), true, false);
        }

    }

    private ModelNode createReadAttributeOp(PathAddress base) {
        PathAddress pa = base.append(SUBSYSTEM, LogStreamExtension.SUBSYSTEM_NAME);
        return Util.getReadAttributeOperation(pa, LogStreamExtension.LOG_FILE.getName());
    }

    private ModelNode createOperationOp(PathAddress base) {
        PathAddress pa = base.append(SUBSYSTEM, LogStreamExtension.SUBSYSTEM_NAME);
        return Util.createEmptyOperation(LogStreamExtension.STREAM_LOG_FILE, pa);
    }

    private void readLogFile(ModelNode opNode, ModelControllerClient client, boolean forServer) throws IOException {
        Operation op = OperationBuilder.create(opNode).build();
        OperationResponse response = null;
        try {
            response = client.executeOperation(op, OperationMessageHandler.DISCARD);

            ModelNode respNode = response.getResponseNode();
            System.out.println(respNode.toString());
            Assert.assertEquals(respNode.toString(), "success", respNode.get("outcome").asString());
            ModelNode result = respNode.get("result");
            Assert.assertEquals(respNode.toString(), ModelType.STRING, result.getType());
            List<? extends OperationResponse.StreamEntry> streams = response.getInputStreams();
            Assert.assertEquals(1, streams.size());
            processResponseStream(response, result.asString(), forServer, client == masterClient);

        } finally {
            StreamUtils.safeClose(response);
        }

    }

    private void processResponseStream(OperationResponse response, String streamUUID, boolean forServer, boolean forMaster) throws IOException {
        OperationResponse.StreamEntry se = response.getInputStream(streamUUID);

        readLogStream(se.getStream(), forServer, forMaster);
    }


    private void readLogStream(InputStream stream, boolean forServer, boolean forMaster) throws IOException {

        LineNumberReader reader = new LineNumberReader(new InputStreamReader(stream, StandardCharsets.UTF_8));

        String expected = LogStreamExtension.getLogMessage(logMessageContent);
        boolean readRegisteredServer = false;
        boolean readRegisteredSlave = false;
        boolean readExpected = false;
        String read;
        while ((read = reader.readLine()) != null) {
            readRegisteredServer = readRegisteredServer || read.contains("WFLYHC0020");
            readRegisteredSlave = readRegisteredSlave || read.contains("WFLYHC0019");
            readExpected = readExpected || read.contains(expected);
        }

        if (forServer) {
            Assert.assertFalse(readRegisteredServer);
        } else if (forMaster) {
            Assert.assertTrue(readRegisteredSlave);
        } else {
            Assert.assertFalse(readRegisteredSlave);
            Assert.assertTrue(readRegisteredServer);
        }
        Assert.assertTrue(readExpected);

        reader.close();

    }
    private static ModelNode executeForResult(final ModelNode op, final ModelControllerClient modelControllerClient) throws IOException, MgmtOperationException {
        try {
            return DomainTestUtils.executeForResult(op, modelControllerClient);
        } catch (MgmtOperationException e) {
            System.out.println(" Op failed:");
            System.out.println(e.getOperation());
            System.out.println("with result");
            System.out.println(e.getResult());
            throw e;
        }
    }
}
