/*
 * Copyright (C) 2016 Red Hat, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.syndesis.server.runtime;

import static org.assertj.core.api.Assertions.assertThat;

import de.mkammerer.wiremock.WireMockExtension;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;

import javax.annotation.PostConstruct;

import io.syndesis.common.util.json.JsonUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;

import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;

import io.syndesis.server.dao.manager.DataManager;
import io.syndesis.server.jsondb.impl.SqlJsonDB;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@ContextConfiguration(
    classes = {
        Application.class,
        CacheConfiguration.class,
        DataStoreConfiguration.class,
        FileStoreConfiguration.class,
        ExtensionConfiguration.class,
        IntegrationConfiguration.class,
        SyndesisCorsConfiguration.class
    }
)
public abstract class BaseITCase {

    private static final Logger LOG = LoggerFactory.getLogger(BaseITCase.class);

    @RegisterExtension
    public static final WireMockExtension WIRE_MOCK = new WireMockExtension();

    @Autowired
    protected SqlJsonDB jsondb;

    @Autowired
    protected DataManager dataManager;

    @BeforeAll
    public static void envSetup() {
        // On some systems (like running a build in a k8s pod), you don't get home dir.
        if( System.getProperty("user.home")==null ) {
            // But ivy/grape fails if it does not know where the user home dir is located..
            String target = Paths.get("target").toAbsolutePath().toString();
            System.setProperty("user.home", target);
        }
    }

    @PostConstruct()
    public void resetDB() {
        get("/api/v1/test-support/reset-db", Void.class, tokenRule.validToken(), HttpStatus.NO_CONTENT);
    }

    protected void clearDB() {
        try {
            this.dataManager.clearCache();
            this.jsondb.dropTables();
        } catch (Exception ignore) {
          // ignored
        }
        this.jsondb.createTables();
    }

    private TestRestTemplate restTemplate;

    public final APITokenRule tokenRule = new APITokenRule();

    public TestRestTemplate restTemplate() {
        return restTemplate;
    }

    public static class TestConfigurationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(final ConfigurableApplicationContext applicationContext) {
            final ConfigurableEnvironment environment = applicationContext.getEnvironment();
            environment.getPropertySources().addFirst(new MapPropertySource("test-source",
                    Collections.singletonMap("meta.service", "localhost:" + WIRE_MOCK.port())));
        }
    }

    @Autowired
    public void setRestTemplate(TestRestTemplate testRestTemplate) {
        List<HttpMessageConverter<?>> messageConverters = testRestTemplate.getRestTemplate().getMessageConverters();
        messageConverters.add(0, new YamlJackson2HttpMessageConverter());
        messageConverters.add(0, new JsonJackson2HttpMessageConverter());
        messageConverters.add( 0, new ByteArrayHttpMessageConverter());
        this.restTemplate = testRestTemplate;
    }


    protected <T> ResponseEntity<T> delete(String url) {
        return delete(url, null, tokenRule.validToken(), HttpStatus.NO_CONTENT);
    }

    protected <T> ResponseEntity<T> delete(String url, Class<T> responseClass) {
        return delete(url, responseClass, tokenRule.validToken(), HttpStatus.NO_CONTENT);
    }

    protected <T> ResponseEntity<T> delete(String url, Class<T> responseClass, String token) {
        return delete(url, responseClass, token, HttpStatus.NO_CONTENT);
    }

    protected <T> ResponseEntity<T> delete(String url, Class<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.DELETE, url, null, responseClass, token, expectedStatus);
    }

    protected <T> ResponseEntity<T> get(String url, Class<T> responseClass) {
        return get(url, responseClass, responseClass == null ? HttpStatus.NO_CONTENT : HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> get(String url, Class<T> responseClass, HttpStatus expectedStatus) {
        return get(url, responseClass, tokenRule.validToken(), expectedStatus);
    }

    protected <T> ResponseEntity<T> get(String url, Class<T> responseClass, String token) {
        return get(url, responseClass, token, responseClass == null ? HttpStatus.NO_CONTENT : HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> get(String url, Class<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.GET, url, null, responseClass, token, expectedStatus);
    }

    protected <T> ResponseEntity<T> get(String url, ParameterizedTypeReference<T> responseClass) {
        return http(HttpMethod.GET, url, null, responseClass, tokenRule.validToken(), new HttpHeaders(), HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> get(String url, ParameterizedTypeReference<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.GET, url, null, responseClass, token, new HttpHeaders(), expectedStatus);
    }

    protected <T> ResponseEntity<T> post(String url, Object body,  Class<T> responseClass) {
        return post(url, body, responseClass, tokenRule.validToken(), responseClass == null ? HttpStatus.NO_CONTENT : HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> post(String url, Object body, Class<T> responseClass, String token) {
        return post(url, body, responseClass, token, responseClass == null ? HttpStatus.NO_CONTENT : HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> post(String url, Object body, Class<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.POST, url, body, responseClass, token, expectedStatus);
    }

    protected <T> ResponseEntity<T> post(String url, Object body, ParameterizedTypeReference<T> responseClass) {
        return http(HttpMethod.POST, url, body, responseClass, tokenRule.validToken(), new HttpHeaders(), HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> post(String url, Object body, ParameterizedTypeReference<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.POST, url, body, responseClass, token, new HttpHeaders(), expectedStatus);
    }

    protected <T> ResponseEntity<T> post(String url, Object body, Class<T> responseClass, String token, HttpStatus expectedStatus, HttpHeaders headers) {
        return http(HttpMethod.POST, url, body, responseClass, token, headers, expectedStatus);
    }

    protected <T> ResponseEntity<T> put(String url, Object body) {
        return put(url, body, null, tokenRule.validToken(), HttpStatus.NO_CONTENT);
    }

    protected <T> ResponseEntity<T> put(String url, Object body, Class<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.PUT, url, body, responseClass, token, new HttpHeaders(), expectedStatus);
    }

    protected <T> ResponseEntity<T> put(String url, Object body, Class<T> responseClass, String token, HttpStatus expectedStatus, HttpHeaders headers) {
        return http(HttpMethod.PUT, url, body, responseClass, token, headers, expectedStatus);
    }

    protected <T> ResponseEntity<T> patch(String url, Object body) {
        return patch(url, body, null, tokenRule.validToken());
    }

    protected <T> ResponseEntity<T> patch(String url, Object body,  Class<T> responseClass) {
        return patch(url, body, responseClass, tokenRule.validToken(), responseClass == null ? HttpStatus.NO_CONTENT : HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> patch(String url, Object body, Class<T> responseClass, String token) {
        return patch(url, body, responseClass, token, responseClass == null ? HttpStatus.NO_CONTENT : HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> patch(String url, Object body, Class<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.PATCH, url, body, responseClass, token, expectedStatus);
    }

    protected <T> ResponseEntity<T> patch(String url, Object body, ParameterizedTypeReference<T> responseClass) {
        return http(HttpMethod.PATCH, url, body, responseClass, tokenRule.validToken(), new HttpHeaders(), HttpStatus.OK);
    }

    protected <T> ResponseEntity<T> patch(String url, Object body, ParameterizedTypeReference<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(HttpMethod.PATCH, url, body, responseClass, token, new HttpHeaders(), expectedStatus);
    }

    protected <T> ResponseEntity<T> patch(String url, Object body, Class<T> responseClass, String token, HttpStatus expectedStatus, HttpHeaders headers) {
        return http(HttpMethod.PATCH, url, body, responseClass, token, headers, expectedStatus);
    }


    protected <T> ResponseEntity<T> http(HttpMethod method, String url, Object body, Class<T> responseClass, String token, HttpStatus expectedStatus) {
        return http(method, url, body, responseClass, token, new HttpHeaders(), expectedStatus);
    }

    protected <T> ResponseEntity<T> http(HttpMethod method, String url, Object body, ParameterizedTypeReference<T> responseClass, String token, HttpHeaders headers, HttpStatus expectedStatus) {
        prepareHeaders(body, headers, token);

        ResponseEntity<T> response = restTemplate().exchange(url, method, new HttpEntity<>(body, headers), responseClass);

        return processResponse(expectedStatus, response);
    }

    protected <T> ResponseEntity<T> http(HttpMethod method, String url, Object body, Class<T> responseClass, String token, HttpHeaders headers, HttpStatus expectedStatus) {
        prepareHeaders(body, headers, token);

        ResponseEntity<T> response = restTemplate().exchange(url, method, new HttpEntity<>(body, headers), responseClass);

        return processResponse(expectedStatus, response);
    }

    private static <T> ResponseEntity<T> processResponse(HttpStatus expectedStatus, ResponseEntity<T> response) {
        if( expectedStatus!=null ) {
            if( !response.getStatusCode().equals(expectedStatus) ) {
                LOG.warn("Got unexpected status code: {}, body: {}", response.getStatusCode(), response.getBody());
            }
            assertThat(response.getStatusCode()).as("status code").isEqualTo(expectedStatus);
        }
        return response;
    }

    private static void prepareHeaders(Object body, HttpHeaders headers, String token) {
        if( body!=null && !headers.containsKey(HttpHeaders.CONTENT_TYPE) ) {
            headers.set(HttpHeaders.CONTENT_TYPE, "application/json");
        }
        if (token != null) {
            headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
        }
        headers.set("X-Forwarded-User", "someone_important");
        headers.set("X-Forwarded-Access-Token", token);
        headers.set("SYNDESIS-XSRF-TOKEN", "awesome");
    }

    static final class YamlJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
        YamlJackson2HttpMessageConverter() {
            super(new YAMLMapper(), MediaType.parseMediaType("application/yaml"));
        }
    }
    static final class JsonJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
        JsonJackson2HttpMessageConverter() {
            super(JsonUtils.copyObjectMapperConfiguration(), MediaType.parseMediaType("application/json"));
        }
    }

    public static String resource(String file) throws IOException {
        try (InputStream is = BaseITCase.class.getClassLoader().getResourceAsStream(file)) {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            copy(is, os);
            return new String(os.toByteArray(), StandardCharsets.UTF_8);
        }
    }

    public static void copy(InputStream is, ByteArrayOutputStream os) throws IOException {
        int c;
        while( (c=is.read())>=0 ) { // PMD
            os.write(c);
        }
    }


}
