/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library 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 library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */
// https://issues.jboss.org/browse/TEIID-4697
// https://issues.jboss.org/browse/TEIID-5187
package org.teiid.translator.swagger;

import static org.junit.jupiter.api.Assertions.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.activation.DataSource;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service.Mode;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.http.HTTPBinding;

import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.teiid.cdk.api.TranslationUtility;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.util.ObjectConverterUtil;
import org.teiid.language.Call;
import org.teiid.language.Command;
import org.teiid.translator.ExecutionContext;
import org.teiid.translator.ProcedureExecution;
import org.teiid.translator.TranslatorException;
import org.teiid.translator.WSConnection;

@SuppressWarnings({ "nls", "unused" })
public class TestSwaggerQueryExecution {

    private ProcedureExecution helpProcedureExecute(String query,
            final String resultJson, String expectedURL, int responseCode)
            throws Exception {
        Map<String, Object> headers = new HashMap<String, Object>();

        return helpProcedureExecute(query, resultJson, expectedURL,
                responseCode, true, "GET", null, headers);
    }

    private ProcedureExecution helpProcedureExecute(String query,
            final String resultJson, String expectedURL, int responseCode,
            boolean decode, String expectedMethod, String expectedInput,
            Map<String, Object> userHeaders) throws Exception {
        
        userHeaders.put(MessageContext.HTTP_REQUEST_HEADERS,
                new HashMap<String, List<String>>());
        userHeaders.put(WSConnection.RESPONSE_CODE, new Integer(responseCode));
        userHeaders.put("Content-Type", Arrays.asList("application/json"));

        SwaggerExecutionFactory translator = new SwaggerExecutionFactory();
        translator.start();

        TranslationUtility utility = new TranslationUtility(
                TestSwaggerMetadataProcessor
                        .getTransformationMetadata(TestSwaggerMetadataProcessor
                                .petstoreMetadata(translator), translator));

        Command cmd = utility.parseCommand(query);
        ExecutionContext context = Mockito.mock(ExecutionContext.class);
        WSConnection connection = Mockito.mock(WSConnection.class);

        Dispatch<DataSource> dispatch = Mockito.mock(Dispatch.class);
        Mockito.when(dispatch.getRequestContext()).thenReturn(userHeaders);
        Mockito.when(dispatch.getResponseContext()).thenReturn(userHeaders);

        Mockito.when(connection.createDispatch(
                Mockito.eq(HTTPBinding.HTTP_BINDING), Mockito.anyString(),
                Mockito.eq(DataSource.class), Mockito.eq(Mode.MESSAGE), Mockito.anyBoolean()))
                .thenReturn(dispatch);

        DataSource outputDS = new DataSource() {
            @Override
            public OutputStream getOutputStream() throws IOException {
                return new ByteArrayOutputStream();
            }

            @Override
            public String getName() {
                return "result";
            }

            @Override
            public InputStream getInputStream() throws IOException {
                ByteArrayInputStream in = new ByteArrayInputStream(
                        resultJson.getBytes());
                return in;
            }

            @Override
            public String getContentType() {
                return "application/json";
            }
        };
        Mockito.when(dispatch.invoke(Mockito.any()))
                .thenReturn(outputDS);

        ProcedureExecution execution = translator.createProcedureExecution(
                (Call) cmd, context, utility.createRuntimeMetadata(),
                connection);
        execution.execute();

        ArgumentCaptor<String> endpoint = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> binding = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<DataSource> input = ArgumentCaptor
                .forClass(DataSource.class);

        Mockito.verify(connection).createDispatch(binding.capture(),
                endpoint.capture(), Mockito.eq(DataSource.class),
                Mockito.eq(Mode.MESSAGE), Mockito.anyBoolean());
        Mockito.verify(dispatch).invoke(input.capture());

        assertEquals(expectedURL,
                decode ? URLDecoder.decode(endpoint.getValue(), "utf-8")
                        : endpoint.getValue());
        assertEquals(expectedMethod, dispatch.getRequestContext()
                .get(MessageContext.HTTP_REQUEST_METHOD));
        if (expectedInput != null) {
            assertEquals(expectedInput, ObjectConverterUtil
                    .convertToString(input.getValue().getInputStream()));
        }
        return execution;
    }

    @Test
    public void testSimpleInputArray() throws Exception {
        String query = "exec findPetsByStatus(status=>('available',));";
        String expectedURL = "http://petstore.swagger.io/v2/pet/findByStatus?status=available";
        String response = "[\n" 
                + "  {\n" + "\"id\": 7,\n"
                + "    \"category\": {\n" 
                + "         \"id\": 4,\n"
                + "         \"name\": \"Lions\"\n" 
                + "    },\n"
                + "    \"name\": \"Lion 1\",\n" 
                + "    \"photoUrls\": [\n"
                + "      \"url1\",\n" 
                + "      \"url2\"\n" 
                + "    ],\n"
                + "    \"tags\": [\n" 
                + "      {\n" 
                + "        \"id\": 1,\n"
                + "        \"name\": \"tag1\"\n" 
                + "      },\n" 
                + "      {\n"
                + "        \"id\": 2,\n" 
                + "        \"name\": \"tag2\"\n"
                + "      }\n" 
                + "    ],\n" 
                + "    \"status\": \"available\"\n"
                + "  },\n" 
                + "  {\n" 
                + "    \"id\": 10008,\n"
                + "    \"category\": {\n" 
                + "      \"id\": 0,\n"
                + "      \"name\": \"string\"\n" 
                + "    },\n"
                + "    \"name\": \"doggie\",\n" 
                + "    \"photoUrls\": [\n"
                + "      \"string\"\n" 
                + "    ],\n" 
                + "    \"tags\": [\n"
                + "      {\n" 
                + "        \"id\": 0,\n"
                + "        \"name\": \"string\"\n" 
                + "      }\n" 
                + "    ],\n"
                + "    \"status\": \"available\"\n" 
                + "  }]";

        ProcedureExecution excution = helpProcedureExecute(query, response,
                expectedURL, 200);

        assertArrayEquals(new Object[] { 7L, 4L, "Lions", "Lion 1",
                new String[] { "url1", "url2" }, 1L, "tag1", "available" },
                excution.next().toArray(new Object[8]));

        assertArrayEquals(new Object[] { 7L, 4L, "Lions", "Lion 1",
                new String[] { "url1", "url2" }, 2L, "tag2", "available" },
                excution.next().toArray(new Object[8]));

        assertArrayEquals(
                new Object[] { 10008L, 0L, "string", "doggie",
                        new String[] { "string" }, 0L, "string", "available" },
                excution.next().toArray(new Object[8]));

        assertNull(excution.next());
    }

    @Test
    public void testPostBasedQuery() throws Exception {
        String query = "exec addPet(id=>99, category_id=>0,category_name=>'canine',name=>'nikky',"
                + "photoUrls=>('photo1','photo2'),tags_tag_id=>0, tags_tag_name=>'doggie',"
                + "status=>'available');";
        String expectedURL = "http://petstore.swagger.io/v2/pet";
        String response = "{"
                    + "\"id\":99,"
                    + "\"name\":\"nikky\","
                    + "\"photoUrls\":[\"photo1\",\"photo2\"],"
                    + "\"status\":\"available\","
                    + "\"category\":{"
                        + "\"id\":0,"
                        + "\"name\":\"canine\""
                     + "},"
                    + "\"tags\":["
                        + "{\"id\":0,"
                        + "\"name\":\"doggie\""
                        + "}"
                    + "]}";

        ProcedureExecution excution = helpProcedureExecute(query, response,
                expectedURL, 200, true, "POST", response, getHeaders());
    }

    @Test
    public void testMapReturn() throws Exception {
        String query = "exec getInventory();";
        String expectedURL = "http://petstore.swagger.io/v2/store/inventory";
        String response = "{\n" 
                + "  \"sold\": 6,\n" 
                + "  \"string\": 7,\n"
                + "  \"pending\": 62,\n" 
                + "  \"available\": 891,\n"
                + "  \"Live\": 7,\n" 
                + "  \"fulfilled\": 1\n" 
                + "}";

        ProcedureExecution excution = helpProcedureExecute(query, response,
                expectedURL, 200, true, "GET", null, getHeaders());
        assertArrayEquals(new Object[] { "sold", 6 },
                excution.next().toArray(new Object[2]));
        assertArrayEquals(new Object[] { "string", 7 },
                excution.next().toArray(new Object[2]));
        assertArrayEquals(new Object[] { "pending", 62 },
                excution.next().toArray(new Object[2]));
        assertArrayEquals(new Object[] { "available", 891 },
                excution.next().toArray(new Object[2]));
        assertArrayEquals(new Object[] { "Live", 7 },
                excution.next().toArray(new Object[2]));
        assertArrayEquals(new Object[] { "fulfilled", 1 },
                excution.next().toArray(new Object[2]));
        assertNull(excution.next());
    }

    @Test
    public void testHeadersInResponseWithReturn() throws Exception {
        String query = "exec loginUser(username=>'foo',password=>'bar');";
        String expectedURL = "http://petstore.swagger.io/v2/user/login?username=foo&password=bar";
        String response = "sucess";
        TimeZone tz = TimeZone.getDefault();
        try {
            TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
            ProcedureExecution excution = helpProcedureExecute(query, response,
                    expectedURL, 200, true, "GET", null, getHeaders());
            assertArrayEquals(new Object[] { "sucess", 1, new Timestamp(1460110463000L) },
                    excution.next().toArray(new Object[3]));
            assertNull(excution.next());
        } finally {
            TimeZone.setDefault(tz);    
        }
    }
    
    @Test
    public void testParameterInPath() throws Exception {
        String query = "exec getPetById(petId=>687789);";
        String expectedURL = "http://petstore.swagger.io/v2/pet/687789";
        String response = "{\n" + 
                "  \"id\": 687789,\n" + 
                "  \"category\": {\n" + 
                "    \"id\": 0,\n" + 
                "    \"name\": \"Lions\"\n" + 
                "  },\n" + 
                "  \"name\": \"nikky\",\n" + 
                "  \"photoUrls\": [\n" + 
                "    \"url1\"\n" + 
                "  ],\n" + 
                "  \"tags\": [\n" + 
                "    {\n" + 
                "      \"id\": 1,\n" + 
                "      \"name\": \"tag1\"\n" + 
                "    }\n" + 
                "  ],\n" + 
                "  \"status\": \"sold\"\n" + 
                "}";
        
        ProcedureExecution excution = helpProcedureExecute(query, response,
                expectedURL, 200, true, "GET", null, getHeaders());
        assertArrayEquals(new Object[] { 687789L, 0L, "Lions", "nikky",
                new String[] { "url1"}, 1L, "tag1", "sold" },
                excution.next().toArray(new Object[8]));
        assertNull(excution.next());
    }
    
    @Test
    public void testErrorResponse() throws Exception {
        assertThrows(TranslatorException.class, () -> {
            String query = "exec getPetById(petId=>687789);";
            String expectedURL = "http://petstore.swagger.io/v2/pet/687789";
            String response = "";

            ProcedureExecution excution = helpProcedureExecute(query, response,
                    expectedURL, 400, true, "GET", null, getHeaders());
            assertNull(excution.next());
        });
    }

    @Test
    public void testDefaultResponse() throws Exception {
        String query = "exec logoutUser();";
        String expectedURL = "http://petstore.swagger.io/v2/user/logout";
        String response = "";
        
        ProcedureExecution excution = helpProcedureExecute(query, response,
                expectedURL, 200, true, "GET", null, getHeaders());
        assertNull(excution.next());
    }
    
    @Test
    public void testScalarResponse() throws Exception {
        String query = "exec loginStatus('foo');";
        String expectedURL = "http://petstore.swagger.io/v2/user/loginStatus?username=foo";
        String response = "foo";
        
        ProcedureExecution excution = helpProcedureExecute(query, response,
                expectedURL, 200, true, "GET", null, getHeaders());
        assertEquals("foo", excution.getOutputParameterValues().get(0));
    }

    @Test
 	public void testNullParameter() throws Exception {
        String query = "exec loginStatus(null);";
        String expectedURL = null;
        String response = null;

        assertThrows(TeiidRuntimeException.class, () -> {
            ProcedureExecution excution = helpProcedureExecute(query, response,
                    expectedURL, 200, true, "GET", null, getHeaders());
        });
    }

    @Test
 	public void testOptionalParameter() throws Exception {
        String query = "exec updatePet(name=>'fido');";
        String expectedURL = "http://petstore.swagger.io/v2/pet";
        String response = "";

        ProcedureExecution excution = helpProcedureExecute(query, response,
                expectedURL, 200, true, "PUT", null, getHeaders());
    }
    
    private Map<String, Object> getHeaders() {
        Map<String, Object> headers = new HashMap<String, Object>();
        headers.put(MessageContext.HTTP_REQUEST_HEADERS,
                new HashMap<String, List<String>>());
        headers.put(WSConnection.RESPONSE_CODE, new Integer(200));
        headers.put("Content-Type", "application/json");
        headers.put("X-Rate-Limit", "1");
        headers.put("X-Expires-After", "2016-04-08T10:14:23Z");
        return headers;
    }

    @Test
    public void testGetSerializer(){
        assertTrue(SwaggerProcedureExecution.getSerializer("application/json") instanceof JsonSerializer);
        assertTrue(SwaggerProcedureExecution.getSerializer("application/json;charset=utf-8") instanceof JsonSerializer);
    }

    @Test
    public void testGetSerializerFails() {
        assertThrows(AssertionError.class, () -> SwaggerProcedureExecution.getSerializer("application/xml"));
    }

}
