package de.ids_mannheim.korap.web.controller;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockserver.integration.ClientAndServer.startClientAndServer;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import jakarta.ws.rs.core.Response.Status;

import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.Header;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.ws.rs.core.Response;

import de.ids_mannheim.korap.config.SpringJerseyTest;
import de.ids_mannheim.korap.exceptions.KustvaktException;
import de.ids_mannheim.korap.exceptions.StatusCodes;
import de.ids_mannheim.korap.utils.JsonUtils;

public class SearchPipeTest extends SpringJerseyTest {

    private ClientAndServer mockServer;

    private MockServerClient mockClient;

    private int port = 6071;

    private String pipeJson, pipeWithParamJson;

    private String glemmUri = "http://localhost:" + port + "/glemm";

    public SearchPipeTest () throws URISyntaxException, IOException {
        pipeJson = IOUtils.toString(
                ClassLoader.getSystemResourceAsStream(
                        "pipe-output/test-pipes.jsonld"),
                StandardCharsets.UTF_8);
        pipeWithParamJson = IOUtils.toString(
                ClassLoader.getSystemResourceAsStream(
                        "pipe-output/with-param.jsonld"),
                StandardCharsets.UTF_8);
    }

    @BeforeEach
    public void startMockServer () {
        mockServer = startClientAndServer(port);
        mockClient = new MockServerClient("localhost", mockServer.getPort());
    }

    @AfterEach
    public void stopMockServer () {
        mockServer.stop();
    }

    @Test
    public void testMockServer () throws IOException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/test")
                        .withHeader(new Header("Content-Type",
                                "application/json; charset=utf-8")))
                .respond(response()
                        .withHeader(new Header("Content-Type",
                                "application/json; charset=utf-8"))
                        .withBody("{test}").withStatusCode(200));
        URL url = new URL("http://localhost:" + port + "/test");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type",
                "application/json; charset=UTF-8");
        connection.setRequestProperty("Accept", "application/json");
        connection.setDoOutput(true);
        String json = "{\"name\" : \"dory\"}";
        try (OutputStream os = connection.getOutputStream()) {
            byte[] input = json.getBytes("utf-8");
            os.write(input, 0, input.length);
        }
        assertEquals(200, connection.getResponseCode());
        BufferedReader br = new BufferedReader(
                new InputStreamReader(connection.getInputStream(), "utf-8"));
        assertEquals(br.readLine(), "{test}");
    }

    @Test
    public void testSearchWithPipes ()
            throws IOException, KustvaktException, URISyntaxException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/glemm")
                        .withHeaders(
                                new Header("Content-Type",
                                        "application/json; charset=utf-8"),
                                new Header("Accept", "application/json")))
                .respond(response()
                        .withHeader(new Header("Content-Type",
                                "application/json; charset=utf-8"))
                        .withBody(pipeJson).withStatusCode(200));
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", glemmUri).request().get();
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        String entity = response.readEntity(String.class);
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(2, node.at("/query/wrap/key").size());
        assertEquals(1, node.at("/collection/rewrites").size());
        assertEquals(node.at("/collection/rewrites/0/operation").asText(),
                "operation:insertion");
        assertEquals(node.at("/collection/rewrites/0/scope").asText(),
                "availability(FREE)");
        node = node.at("/query/wrap/rewrites");
        assertEquals(2, node.size());
        assertEquals(node.at("/0/src").asText(), "Glemm");
        assertEquals(node.at("/0/operation").asText(), "operation:override");
        assertEquals(node.at("/0/scope").asText(), "key");
        assertEquals(node.at("/1/src").asText(), "Kustvakt");
        assertEquals(node.at("/1/operation").asText(), "operation:injection");
        assertEquals(node.at("/1/scope").asText(), "foundry");
    }

    @Test
    public void testSearchWithUrlEncodedPipes ()
            throws IOException, KustvaktException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/glemm")
                        .withHeaders(
                                new Header("Content-Type",
                                        "application/json; charset=utf-8"),
                                new Header("Accept", "application/json")))
                .respond(response()
                        .withHeader(new Header("Content-Type",
                                "application/json; charset=utf-8"))
                        .withBody(pipeJson).withStatusCode(200));
        glemmUri = URLEncoder.encode(glemmUri, "utf-8");
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", glemmUri).request().get();
        String entity = response.readEntity(String.class);
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(2, node.at("/query/wrap/key").size());
    }

    @Test
    public void testSearchWithMultiplePipes () throws KustvaktException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/glemm")
                        .withQueryStringParameter("param").withHeaders(
                                new Header("Content-Type",
                                        "application/json; charset=utf-8"),
                                new Header("Accept", "application/json")))
                .respond(response()
                        .withHeader(new Header("Content-Type",
                                "application/json; charset=utf-8"))
                        .withBody(pipeWithParamJson).withStatusCode(200));
        String glemmUri2 = glemmUri + "?param=blah";
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", glemmUri + "," + glemmUri2).request()
                .get();
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        String entity = response.readEntity(String.class);
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(3, node.at("/query/wrap/key").size());
    }

    @Test
    public void testSearchWithUnknownURL ()
            throws IOException, KustvaktException {
        String url = target().getUri().toString() + API_VERSION
                + "/test/tralala";
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", url).request().get();
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        String entity = response.readEntity(String.class);
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(StatusCodes.PIPE_FAILED, node.at("/warnings/0/0").asInt());
        assertEquals(node.at("/warnings/0/3").asText(), "404 Not Found");
    }

    @Test
    public void testSearchWithUnknownHost () throws KustvaktException {
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", "http://glemm").request().get();
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        String entity = response.readEntity(String.class);
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(StatusCodes.PIPE_FAILED, node.at("/warnings/0/0").asInt());
        assertEquals(node.at("/warnings/0/3").asText(), "glemm");
    }

    @Test
    public void testSearchUnsupportedMediaType () throws KustvaktException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/non-json-pipe"))
                .respond(response().withStatusCode(415));
        String pipeUri = "http://localhost:" + port + "/non-json-pipe";
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", pipeUri).request().get();
        String entity = response.readEntity(String.class);
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(StatusCodes.PIPE_FAILED, node.at("/warnings/0/0").asInt());
        assertEquals(node.at("/warnings/0/3").asText(),
                "415 Unsupported Media Type");
    }

    @Test
    public void testSearchWithMultiplePipeWarnings () throws KustvaktException {
        String url = target().getUri().toString() + API_VERSION
                + "/test/tralala";
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", url + "," + "http://glemm").request()
                .get();
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        String entity = response.readEntity(String.class);
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(2, node.at("/warnings").size());
        assertEquals(StatusCodes.PIPE_FAILED, node.at("/warnings/0/0").asInt());
        assertEquals(url, node.at("/warnings/0/2").asText());
        assertEquals(node.at("/warnings/0/3").asText(), "404 Not Found");
        assertEquals(StatusCodes.PIPE_FAILED, node.at("/warnings/1/0").asInt());
        assertEquals(node.at("/warnings/1/2").asText(), "http://glemm");
        assertEquals(node.at("/warnings/1/3").asText(), "glemm");
    }

    @Test
    public void testSearchWithInvalidJsonResponse () throws KustvaktException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/invalid-response")
                        .withHeaders(
                                new Header("Content-Type",
                                        "application/json; charset=utf-8"),
                                new Header("Accept", "application/json")))
                .respond(response().withBody("{blah:}").withStatusCode(200)
                        .withHeaders(new Header("Content-Type",
                                "application/json; charset=utf-8")));
        String pipeUri = "http://localhost:" + port + "/invalid-response";
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", pipeUri).request().get();
        String entity = response.readEntity(String.class);
        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(StatusCodes.DESERIALIZATION_FAILED,
                node.at("/errors/0/0").asInt());
    }

    @Test
    public void testSearchWithPlainTextResponse () throws KustvaktException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/plain-text")
                        .withHeaders(
                                new Header("Content-Type",
                                        "application/json; charset=utf-8"),
                                new Header("Accept", "application/json")))
                .respond(response().withBody("blah").withStatusCode(200));
        String pipeUri = "http://localhost:" + port + "/plain-text";
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", pipeUri).request().get();
        String entity = response.readEntity(String.class);
        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(StatusCodes.DESERIALIZATION_FAILED,
                node.at("/errors/0/0").asInt());
    }

    @Test
    public void testSearchWithMultipleAndUnknownPipes ()
            throws KustvaktException {
        mockClient.reset()
                .when(request().withMethod("POST").withPath("/glemm")
                        .withHeaders(
                                new Header("Content-Type",
                                        "application/json; charset=utf-8"),
                                new Header("Accept", "application/json")))
                .respond(response()
                        .withHeader(new Header("Content-Type",
                                "application/json; charset=utf-8"))
                        .withBody(pipeJson).withStatusCode(200));
        Response response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", "http://unknown" + "," + glemmUri)
                .request().get();
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        String entity = response.readEntity(String.class);
        JsonNode node = JsonUtils.readTree(entity);
        assertEquals(2, node.at("/query/wrap/key").size());
        assertTrue(node.at("/warnings").isMissingNode());
        response = target().path(API_VERSION).path("search")
                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
                .queryParam("pipes", glemmUri + ",http://unknown").request()
                .get();
        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        entity = response.readEntity(String.class);
        node = JsonUtils.readTree(entity);
        assertEquals(StatusCodes.PIPE_FAILED, node.at("/warnings/0/0").asInt());
    }
}
