/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.component.platform.http.springboot;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
import org.apache.camel.util.IOHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

@EnableAutoConfiguration(exclude = {OAuth2ClientAutoConfiguration.class, SecurityAutoConfiguration.class})
@CamelSpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { CamelAutoConfiguration.class,
        SpringBootPlatformHttpTest.class, PlatformHttpStreamingTest.TestConfiguration.class,
        PlatformHttpComponentAutoConfiguration.class, SpringBootPlatformHttpAutoConfiguration.class })
public class PlatformHttpStreamingTest {
    @Autowired
    private Environment environment;

    @BeforeEach
    public void setUp() {
        RestAssured.port = Integer.parseInt(environment.getRequiredProperty("local.server.port"));
    }

    @Test
    void testStreamingWithStringRequestAndResponseBody() {
        String requestBody = "Spring Boot Platform HTTP";
        given().body(requestBody).post("/streaming").then().statusCode(200).body(is("Hello " + requestBody));
    }

    @Test
    void testNonStreamingWithStringRequestAndResponseBody() {
        String requestBody = "Spring Boot Platform HTTP";
        given().body(requestBody).post("/nonStreaming").then().statusCode(200).body(is("Hello " + requestBody));
    }

    @Test
    void testStreamingWithFileRequestAndResponseBody() throws Exception {
        Path testFile = null;
        try {
            testFile = Files.createTempFile("platform-http-testing", "txt");
            String content = "Hello World";
            Files.writeString(testFile, content);
            given().body(testFile.toFile()).post("/streamingFile").then().statusCode(200).body(is(content));
        } finally {
            Files.deleteIfExists(testFile);
        }
    }

    @Test
    void testNonStreamingWithFileRequestAndResponseBody() throws Exception {
        Path testFile = null;
        try {
            testFile = Files.createTempFile("platform-http-testing", "txt");
            String content = "Hello World";
            Files.writeString(testFile, content);
            given().body(testFile.toFile()).post("/nonStreamingFile").then().statusCode(200).body(is(content));
        } finally {
            Files.deleteIfExists(testFile);
        }
    }

    @Test
    void testStreamingWithFormUrlEncodedBody() throws Exception {
        given().contentType(ContentType.URLENC).formParam("foo", "bar")
                .post("/streamingUrlEncoded")
                .then().statusCode(200).body(is("foo=bar"));
    }

    @Test
    void testNonStreamingWithFormUrlEncodedBody() throws Exception {
        given().contentType(ContentType.URLENC).formParam("foo", "bar")
                .post("/nonStreamingUrlEncoded")
                .then().statusCode(200).body(is("foo=bar"));
    }

    @Test
    void testHeaderAndBodyWithFormUrlEncodedBody() throws Exception {
        given().contentType(ContentType.URLENC).formParam("foo", "bar")
                .post("/headerAndBodyUrlEncoded")
                .then()
                .statusCode(200)
                .header("foo", "bar")
                .header("BodyClass", containsString("HashMap"))
                .body(is("{foo=bar}"));
    }

    @Test
    void testOnlyHeaderWithFormUrlEncodedBody() throws Exception {
        given().contentType(ContentType.URLENC).formParam("foo", "bar")
                .post("/headerUrlEncoded")
                .then()
                .statusCode(200)
                .header("foo", "bar")
                .header("BodyClass", containsString("[B"))
                .body(is("foo=bar"));
    }

    @Test
    void testStreamingWithSpecificEncoding() throws Exception {
        Path input = Files.createTempFile("platform-http-input", "dat");
        Path output = Files.createTempFile("platform-http-output", "dat");

        String fileContent = "Content with special character ð";
        Files.writeString(input, fileContent, StandardCharsets.ISO_8859_1);

        InputStream response = given()
                .body(new FileInputStream(input.toFile())).post("/streamingSpecificEncoding")
                .then().statusCode(200)
                .extract().body()
                .asInputStream();

            try (FileOutputStream fos = new FileOutputStream(output.toFile())) {
                IOHelper.copy(response, fos);
            }

        assertEquals(fileContent, Files.readString(output, StandardCharsets.ISO_8859_1));
    }

    @Test
    void testNonStreamingWithBinaryData() {
        byte[] binaryData = new byte[] {0x00, 0x0c, (byte) 0x80, (byte) 0xff, (byte) 0xfe, 0x0f, 0x0e};
        byte[] response = given()
                .body(binaryData)
                .contentType(ContentType.BINARY)
                .post("/binaryData")
                .then()
                .statusCode(200)
                .extract().body().asByteArray();
        assertArrayEquals(binaryData, response);
    }

    @Test
    void testStreamingWithBinaryData() {
        byte[] binaryData = new byte[] {0x00, 0x0c, (byte) 0x80, (byte) 0xff, (byte) 0xfe, 0x0f, 0x0e};
        byte[] response = given()
                .body(binaryData)
                .contentType(ContentType.BINARY)
                .post("/streamingBinaryData")
                .then()
                .statusCode(200)
                .extract().body().asByteArray();
        assertArrayEquals(binaryData, response);
    }

    @Test
    void testNonStreamingWithKnownContentLength() {
        // When Content-Length is known, the optimized readNBytes(len) path is used
        // (single byte[] allocation instead of incremental readAllBytes)
        byte[] largeBody = new byte[8192];
        for (int i = 0; i < largeBody.length; i++) {
            largeBody[i] = (byte) (i % 256);
        }
        byte[] response = given()
                .body(largeBody)
                .contentType(ContentType.BINARY)
                .post("/binaryData")
                .then()
                .statusCode(200)
                .extract().body().asByteArray();
        assertArrayEquals(largeBody, response);
    }

    @Test
    void testStreamingBodyIsStreamCache() {
        String requestBody = "streaming body type check";
        given()
                .body(requestBody)
                .post("/streamingBodyType")
                .then()
                .statusCode(200)
                .header("BodyIsStreamCache", "true")
                .body(is(requestBody));
    }

    @Test
    void testNonStreamingBodyIsByteArray() {
        String requestBody = "non-streaming body type check";
        given()
                .body(requestBody)
                .post("/nonStreamingBodyType")
                .then()
                .statusCode(200)
                .header("BodyIsStreamCache", "false");
    }

    @Configuration
    public static class TestConfiguration {
        @Bean
        public RouteBuilder routeBuilder() {
            return new RouteBuilder() {
                @Override
                public void configure() throws Exception {
                    getCamelContext().setStreamCaching(false);

                    from("platform-http:/streaming?useStreaming=true").transform().simple("Hello ${body}");
                    from("platform-http:/streamingFile?useStreaming=true").log("Done processing request");
                    from("platform-http:/streamingUrlEncoded?useStreaming=true").setBody().simple("foo=${header.foo}");
                    from("platform-http:/streamingSpecificEncoding?useStreaming=true").log("Done echoing back request body as response body");

                    from("platform-http:/nonStreaming").transform().simple("Hello ${body}");
                    from("platform-http:/nonStreamingFile").log("Done processing request");
                    from("platform-http:/nonStreamingUrlEncoded").setBody().simple("foo=${header.foo}");
                    from("platform-http:/headerAndBodyUrlEncoded")
                            .process(exchange ->
                                    exchange.getMessage().setHeader("BodyClass", exchange.getIn().getBody().getClass().getName()))
                            .convertBodyTo(String.class);
                    from("platform-http:/headerUrlEncoded?populateBodyWithForm=false")
                            .process(exchange ->
                                    exchange.getMessage().setHeader("BodyClass", exchange.getIn().getBody().getClass().getName()))
                            .log("Done processing request");

                    from("platform-http:/binaryData").log("Done processing binary request");

                    from("platform-http:/streamingBinaryData?useStreaming=true").log("Done processing streaming binary request");
                    from("platform-http:/streamingBodyType?useStreaming=true")
                            .process(exchange -> exchange.getMessage().setHeader("BodyIsStreamCache",
                                    String.valueOf(exchange.getIn().getBody() instanceof org.apache.camel.StreamCache)))
                            .log("Done processing streaming body type check");
                    from("platform-http:/nonStreamingBodyType")
                            .process(exchange -> exchange.getMessage().setHeader("BodyIsStreamCache",
                                    String.valueOf(exchange.getIn().getBody() instanceof org.apache.camel.StreamCache)))
                            .log("Done processing non-streaming body type check");
                }
            };
        }
    }
}
