content: /*
   *  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.causeway.testdomain.rest;
  
  import java.io.IOException;
  import java.math.BigInteger;
  import java.util.List;
  import java.util.Map;
  import java.util.function.Consumer;
  import java.util.stream.Collectors;
  
  import org.asciidoctor.ast.Document;
  import org.asciidoctor.ast.Row;
  import org.asciidoctor.ast.Table;
  import org.junit.jupiter.api.AfterAll;
  import org.junit.jupiter.api.Assertions;
  import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
  import org.junit.jupiter.api.Order;
  import org.junit.jupiter.api.Test;
  import org.junit.jupiter.api.TestMethodOrder;
  import org.springframework.boot.test.context.SpringBootTest;
  import org.springframework.context.annotation.Import;
  import org.springframework.test.context.TestPropertySource;
  
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertTrue;
  
  import org.apache.causeway.applib.client.RepresentationTypeSimplifiedV2;
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.functional.Try;
  import org.apache.causeway.commons.internal.debug._Probe;
  import org.apache.causeway.core.config.presets.CausewayPresets;
  import org.apache.causeway.testdomain.conf.Configuration_headless;
  import org.apache.causeway.testdomain.rospec.BigComplex;
  import org.apache.causeway.testdomain.rospec.Configuration_usingRoSpec;
  import org.apache.causeway.testdomain.rospec.Customer;
  import org.apache.causeway.testdomain.rospec.RoSpecSampler;
  import org.apache.causeway.testdomain.util.UtilSurefire;
  import org.apache.causeway.testdomain.util.rest.RestEndpointService;
  import org.apache.causeway.tooling.model4adoc.AsciiDocFactory;
  import org.apache.causeway.tooling.model4adoc.AsciiDocWriter;
  import org.apache.causeway.viewer.restfulobjects.client.ActionParameterListBuilder;
  import org.apache.causeway.viewer.restfulobjects.client.log.ClientConversationFilter;
  import org.apache.causeway.viewer.restfulobjects.jaxrsresteasy.CausewayModuleViewerRestfulObjectsJaxrsResteasy;
  
  import static org.apache.causeway.testdomain.util.CollectionAssertions.assertComponentWiseEquals;
  import static org.apache.causeway.testdomain.util.CollectionAssertions.assertComponentWiseNumberEquals;
  import static org.apache.causeway.tooling.model4adoc.AsciiDocFactory.cell;
  import static org.apache.causeway.tooling.model4adoc.AsciiDocFactory.doc;
  import static org.apache.causeway.tooling.model4adoc.AsciiDocFactory.headRow;
  import static org.apache.causeway.tooling.model4adoc.AsciiDocFactory.row;
  import static org.apache.causeway.tooling.model4adoc.AsciiDocFactory.table;
  
  import jakarta.inject.Inject;
  import jakarta.ws.rs.core.GenericType;
  import lombok.SneakyThrows;
  import lombok.val;
  
  @SpringBootTest(
          classes = {
                  RestEndpointService.class
          },
          properties = {
                  //"logging.level.org.apache.causeway.viewer.restfulobjects.rendering.service.RepresentationServiceContentNegotiator=DEBUG",
                  //"logging.level.org.apache.causeway.extensions.restclient.ResponseDigest=DEBUG"
          },
          webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  @TestPropertySource(CausewayPresets.UseLog4j2Test)
  @Import({
      Configuration_headless.class,
      Configuration_usingRoSpec.class,
      CausewayModuleViewerRestfulObjectsJaxrsResteasy.class,
  })
  @TestMethodOrder(OrderAnnotation.class) // run tests in sequence for reporting
  class RestServiceSimpifiedRepresentationTest {
  
      @Inject RestEndpointService restService;
  
      private final RoSpecSampler refSampler = new RoSpecSampler();
      private final Filter4ReprType filter4ReprType = new Filter4ReprType();
      private final Filter4Reporting filter4Reporting = new Filter4Reporting();
      private final Can<ClientConversationFilter> conversationFilters = Can.of(
              filter4ReprType,
              filter4Reporting);
  
      @AfterAll
      static void tearDown() throws IOException {
      	if(UtilSurefire.isRunningWithSurefire()) {
      		return;
      	}
      	// copy paste output to docs
          System.out.println("=========================== ASCII DOC ============================");
          System.out.println(Filter4Reporting.print());
          System.out.println("==================================================================");
      }
  
      // -- VOID
  
      @Test @Order(1)
      void voidResult() {
          val digest = digest("voidResult", void.class);
          assertTrue(digest.getValue().isEmpty());
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VOID);
      }
  
      // -- STRING
  
      @Test @Order(2)
      void string() {
          val digest = digest("string", String.class);
          val returnValue = digest.getValue().orElseThrow();
          assertEquals(refSampler.string(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
      }
  
      @Test @Order(2)
      void stringUsingGet() {
          val digest = digestUsingGet("stringSafe", String.class);
          val returnValue = digest.getValue().orElseThrow();
          assertEquals(refSampler.stringSafe(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
      }
  
      @Test @Order(3)
      void stringNull() {
          val digest = digest("stringNull", String.class);
          assertTrue(digest.getValue().orElseThrow().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      // -- STRING ARRAY
  
      @Test @Order(4)
      void stringArray() {
          val digest = digestArray("stringArray", String.class, new GenericType<List<String>>(){});
          val returnValue = digest.getValue().orElseThrow();
          assertComponentWiseEquals(refSampler.stringArray(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUES);
      }
  
      @Test @Order(5)
      void stringArrayEmpty() {
          val digest = digestArray("stringArrayEmpty", String.class, new GenericType<List<String>>(){});
          assertTrue(digest.getValue().orElseThrow().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      @Test @Order(6)
      void stringArrayNull() {
          val digest = digestArray("stringArrayNull", String.class, new GenericType<List<String>>(){});
          assertTrue(digest.getValue().orElseThrow().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      // -- STRING LIST
  
      @Test @Order(7)
      void stringList() {
          val digest = digestList("stringList", String.class, new GenericType<List<String>>(){});
          val returnValues = digest.getValue().orElseThrow();
          assertComponentWiseEquals(refSampler.stringList(), returnValues);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUES);
      }
  
      @Test @Order(8)
      void stringListEmpty() {
          val digest = digestList("stringListEmpty", String.class, new GenericType<List<String>>(){});
          assertTrue(digest.getValue().orElseThrow().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      @Test @Order(9)
      void stringListNull() {
          val digest = digestList("stringListNull", String.class, new GenericType<List<String>>(){});
          assertTrue(digest.getValue().orElseThrow().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      // -- INT
  
      @Test @Order(10)
      void integer() {
          val digest = digest("integer", Integer.class);
          val returnValue = digest.getValue().orElseThrow();
          assertEquals(refSampler.integer(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
      }
  
      @Test @Order(11)
      void integerNull() {
          val digest = digest("integerNull", Integer.class);
          assertTrue(digest.getValue().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      @Test @Order(12)
      void integerPrimitive() {
          val digest = digest("integerPrimitive", int.class);
          val returnValue = digest.getValue().orElseThrow();
          assertEquals(refSampler.integerPrimitive(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
      }
  
      // -- BIG INT
  
      @Test @Order(13)
      void bigInteger() {
          val digest = digest("bigInteger", BigInteger.class);
          val returnValue = digest.getValue().orElseThrow();
          assertEquals(refSampler.bigInteger(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
      }
  
      @Test @Order(14)
      void bigIntegerNull() {
          val digest = digest("bigIntegerNull", BigInteger.class);
          assertTrue(digest.getValue().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      @Test @Order(15)
      void bigIntegerList() {
          val digest = digestList("bigIntegerList", BigInteger.class, new GenericType<List<BigInteger>>(){});
          val returnValue = digest.getValue().orElseThrow();
          assertComponentWiseNumberEquals(refSampler.bigIntegerList(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUES);
      }
  
      // -- CUSTOMER
  
      @Test @Order(16)
      void customer() {
          val digest = digest("customer", Customer.class);
          val returnValue = digest.getValue().orElseThrow();
          assertEquals(refSampler.customer(), returnValue);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.OBJECT);
      }
  
      @Test @Order(17)
      void customerNull() {
          val digest = digest("customerNull", Customer.class);
          assertTrue(digest.getValue().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      @Test @Order(18)
      void customerList() {
          val digest = digestList("customerList", Customer.class, new GenericType<List<Customer>>(){});
          val returnValues = digest.getValue().orElseThrow();
          assertComponentWiseEquals(refSampler.customerList(), returnValues);
          filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.LIST);
      }
  
      @Test @Order(19)
      void customerListEmpty() {
          val digest = digestList("customerListEmpty", Customer.class, new GenericType<List<Customer>>(){});
          assertTrue(digest.getValue().orElseThrow().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      @Test @Order(20)
      void customerListNull() {
          val digest = digestList("customerListNull", Customer.class, new GenericType<List<Customer>>(){});
          assertTrue(digest.getValue().orElseThrow().isEmpty());
          filter4ReprType.assertHttpReturnCode(404);
          filter4ReprType.assertRepresentationType(null);
      }
  
      // -- COMPOSITE
  
      @Test @Order(21)
      void complexList() {
          val digest = digestList("complexList", BigComplex.class, new GenericType<List<BigComplex>>(){});
          val returnValue = digest.getValue().orElseThrow();
          assertComponentWiseEquals(refSampler.complexList(), returnValue);
          // in practice we might be agnostic to the actual representation type here, as long as we can successfully digest
          //filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUES);
          //filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.LIST);
      }
  
      @Test @Order(22)
      void complexAdd() {
          // given
          val a = BigComplex.of(
                  "1.0000000000000000000000000000000000000001",
                  "-2.0000000000000000000000000000000000000002");
          val b = BigComplex.of("3", "4");
  
          val digest = digest("complexAdd", BigComplex.class, argBuilder->{
              argBuilder.addActionParameter("are", a.getRe().toPlainString());
              argBuilder.addActionParameter("aim", a.getIm().toPlainString());
              argBuilder.addActionParameter("bre", b.getRe().toPlainString());
              argBuilder.addActionParameter("bim", b.getIm().toPlainString());
          });
          val returnValue = digest.getValue().orElseThrow();
          BigComplex.assertEquals(a.add(b), returnValue);
          // in practice we might be agnostic to the actual representation type here, as long as we can successfully digest
          //filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
          //filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.OBJECT);
      }
  
      // -- HELPER
  
      <T> Try<T> digest(final String actionName, final Class<T> entityType) {
          return digest(actionName, entityType, argBuilder->{});
      }
  
      <T> Try<T> digest(final String actionName, final Class<T> entityType, final Consumer<ActionParameterListBuilder> onArgs) {
  
          _Probe.errOut("");
          _Probe.errOut("=== %s", actionName);
          _Probe.errOut("");
  
          assertTrue(restService.getPort()>0);
  
          val useRequestDebugLogging = false;
          val client = restService.newClient(useRequestDebugLogging, conversationFilters);
  
          val request = restService.newInvocationBuilder(client,
                  String.format("services/testdomain.RoSpecSampler/actions/%s/invoke", actionName));
  
          val argBuilder = client.arguments();
          onArgs.accept(argBuilder);
  
          val args = argBuilder
                  .build();
  
          val argsAsJavaSource = argBuilder.getActionParameterTypes().entrySet().stream()
          .map(entry->String.format("\n    %s %s", entry.getValue().getSimpleName(), entry.getKey()))
          .collect(Collectors.joining(", "));
  
          filter4Reporting.next(String.format("@Action\n%s %s(%s) {\n    /*...*/\n}", entityType.getSimpleName(), actionName, argsAsJavaSource));
  
          val response = request.post(args);
          val digest = client.digest(response, entityType)
                  .ifFailure(Assertions::fail);
  
          return digest;
  
      }
  
      <T> Try<T> digestUsingGet(final String actionName, final Class<T> entityType) {
  
          _Probe.errOut("");
          _Probe.errOut("=== %s", actionName);
          _Probe.errOut("");
  
          assertTrue(restService.getPort()>0);
  
          val useRequestDebugLogging = false;
          val client = restService.newClient(useRequestDebugLogging, conversationFilters);
  
          val request = restService.newInvocationBuilder(client,
                  String.format("services/testdomain.RoSpecSampler/actions/%s/invoke", actionName));
  
          filter4Reporting.next(String.format("@Action\n%s %s {\n    /*...*/\n}", entityType.getSimpleName(), actionName));
  
          val response = request.get();
          val digest = client.digest(response, entityType)
                  .ifFailure(Assertions::fail);
  
          return digest;
      }
  
      <T> Try<Can<T>> digestArray(
              final String actionName,
              final Class<T> entityType,
              final GenericType<List<T>> genericType) {
          filter4Reporting.next(String.format("@Action\n%s[] %s() {\n    /*...*/\n}", entityType.getSimpleName(), actionName));
          return digestVector(actionName, entityType, genericType);
      }
  
      <T> Try<Can<T>> digestList(
              final String actionName,
              final Class<T> entityType,
              final GenericType<List<T>> genericType) {
          filter4Reporting.next(String.format("@Action\nList<%s> %s() {\n    /*...*/\n}", entityType.getSimpleName(), actionName));
          return digestVector(actionName, entityType, genericType);
      }
  
      <T> Try<Can<T>> digestVector(
              final String actionName,
              final Class<T> entityType,
              final GenericType<List<T>> genericType) {
  
          _Probe.errOut("");
          _Probe.errOut("=== %s", actionName);
          _Probe.errOut("");
  
          assertTrue(restService.getPort()>0);
  
          val useRequestDebugLogging = false;
          val client = restService.newClient(useRequestDebugLogging, conversationFilters);
  
          val request = restService.newInvocationBuilder(client,
                  String.format("services/testdomain.RoSpecSampler/actions/%s/invoke", actionName));
  
          val args = client.arguments()
                  .build();
  
          val response = request.post(args);
          val digest = client.digestList(response, entityType, genericType)
                  .ifFailure(Assertions::fail);
  
          return digest;
  
      }
  
      // -- FILTER FOR REPRESENTATION TYPE
  
      static class Filter4ReprType implements ClientConversationFilter {
  
          private RepresentationTypeSimplifiedV2 reprType;
          private int httpReturnCode;
  
          @Override
          public void onRequest(final String endpoint, final String method, final String acceptHeaderParsing,
                  final Map<String, List<String>> headers, final String body) {
          }
  
          @Override
          public void onResponse(final int httpReturnCode, final Map<String, List<String>> headers, final String body) {
  
              this.httpReturnCode = httpReturnCode;
  
              val contentTypeHeaderStrings = Can.ofCollection(headers.get("Content-Type"));
              reprType = RepresentationTypeSimplifiedV2.parseContentTypeHeaderString(
                      contentTypeHeaderStrings.getFirst().orElse(null))
                      .orElse(null);
  //            if(reprType==null) {
  //                Assertions.fail(String.format(
  //                        "Invalid REST response, cannot parse header's Content-Type '%s' for the repr-type to use",
  //                        contentTypeHeaderStrings));
  //            }
          }
  
          void assertRepresentationType(final RepresentationTypeSimplifiedV2 expected) {
              assertEquals(expected, reprType);
          }
  
          void assertHttpReturnCode(final int expected) {
              assertEquals(expected, httpReturnCode);
          }
  
      }
  
      // -- FILTER FOR ADOC REPORT
  
      static class Filter4Reporting implements ClientConversationFilter {
  
          private static Document doc = doc();
          private static Table table;
  
          static {
  
              table = table(doc);
              table.setTitle(String.format("Autogenerated by %s", RestServiceSimpifiedRepresentationTest.class.getSimpleName()));
              table.setAttribute("cols", "a,a", true);
              table.setAttribute("header-option", "", true);
  
              val headRow = headRow(table);
  
              cell(table, headRow, "Action");
              cell(table, headRow, "Request and Response");
          }
  
          private Row row;
          private StringBuilder requestAndResponseCellContent;
  
          void next(final String javaSource) {
  
              row = row(table);
              cell(table, row, AsciiDocFactory.toString(doc->
                  AsciiDocFactory.SourceFactory.java(doc, javaSource, null)));
              requestAndResponseCellContent = new StringBuilder();
          }
  
          @Override
          public void onRequest(final String endpoint, final String method, final String acceptHeaderParsing,
                  final Map<String, List<String>> headers, final String body) {
  
              requestAndResponseCellContent.append("==== REQUEST\n\n");
              requestAndResponseCellContent.append(
                      AsciiDocFactory.toString(doc->
                          AsciiDocFactory.SourceFactory.json(doc, body, null)));
          }
  
          @Override
          public void onResponse(final int httpReturnCode, final Map<String, List<String>> headers, final String body) {
              val contentTypeHeaderStrings = Can.ofCollection(headers.get("Content-Type"));
              val reprType = RepresentationTypeSimplifiedV2.parseContentTypeHeaderString(
                      contentTypeHeaderStrings.getFirst().orElse(null))
                      .orElse(null);
  
              requestAndResponseCellContent
              .append("\n==== RESPONSE\n\n")
              .append(String.format("- HTTP return code: %d\n", httpReturnCode))
              .append(String.format("- Representation Type: %s\n\n", reprType))
              .append(AsciiDocFactory.toString(doc->
                  AsciiDocFactory.SourceFactory.json(doc, body, null)));
  
              cell(table, row, requestAndResponseCellContent.toString());
          }
  
          @SneakyThrows
          public static String print() {
              return AsciiDocWriter.toString(doc);
          }
  
      }
  
  }
  