/*
 * 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
 *
 *     https://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.avro.specific;

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

import java.util.ArrayList;
import java.util.Optional;
import org.apache.avro.AvroMissingFieldException;
import org.apache.avro.test.http.HttpMethod;
import org.apache.avro.test.http.HttpRequest;
import org.apache.avro.test.http.HttpURI;
import org.apache.avro.test.http.NetworkType;
import org.apache.avro.test.http.QueryParameter;
import org.apache.avro.test.http.Request;
import org.apache.avro.test.http.UserAgent;
import org.apache.avro.test.nullable.RecordWithNullables;
import org.junit.jupiter.api.Test;

public class TestSpecificBuilderTree {

  private Request.Builder createPartialBuilder() {
    Request.Builder requestBuilder = Request.newBuilder();
    requestBuilder.setTimestamp(1234567890);

    requestBuilder.getConnectionBuilder().setNetworkType(NetworkType.IPv4);

    requestBuilder.getHttpRequestBuilder().getUserAgentBuilder().setUseragent("Chrome 123").setId("Foo");

    requestBuilder.getHttpRequestBuilder().getURIBuilder().setMethod(HttpMethod.GET).setPath("/index.html");

    if (!requestBuilder.getHttpRequestBuilder().getURIBuilder().hasParameters()) {
      requestBuilder.getHttpRequestBuilder().getURIBuilder().setParameters(new ArrayList<>());
    }

    requestBuilder.getHttpRequestBuilder().getURIBuilder().getParameters()
        .add(QueryParameter.newBuilder().setName("Foo").setValue("Bar").build());

    return requestBuilder;
  }

  @Test
  void failOnIncompleteTree() {
    assertThrows(AvroMissingFieldException.class, () -> {
      try {
        createPartialBuilder().build();
      } catch (AvroMissingFieldException amfe) {
        assertEquals("Field networkAddress type:STRING pos:1 not set and has no default value", amfe.getMessage());
        assertEquals("Path in schema: --> connection --> networkAddress", amfe.toString());
        throw amfe;
      }
      fail("Should NEVER get here");
    });
  }

  @Test
  void copyBuilder() {
    Request.Builder requestBuilder1 = createPartialBuilder();

    Request.Builder requestBuilder2 = Request.newBuilder(requestBuilder1);

    requestBuilder1.getConnectionBuilder().setNetworkAddress("1.1.1.1");

    requestBuilder2.getConnectionBuilder().setNetworkAddress("2.2.2.2");

    requestBuilder2.getHttpRequestBuilder().getUserAgentBuilder().setId("Bar");

    Request request1 = requestBuilder1.build();
    Request request2 = requestBuilder2.build();

    assertEquals(NetworkType.IPv4, request1.getConnection().getNetworkType());
    assertEquals("1.1.1.1", request1.getConnection().getNetworkAddress());
    assertEquals("Chrome 123", request1.getHttpRequest().getUserAgent().getUseragent());
    assertEquals("Foo", request1.getHttpRequest().getUserAgent().getId());
    assertEquals(HttpMethod.GET, request1.getHttpRequest().getURI().getMethod());
    assertEquals("/index.html", request1.getHttpRequest().getURI().getPath());
    assertEquals(1, request1.getHttpRequest().getURI().getParameters().size());
    assertEquals("Foo", request1.getHttpRequest().getURI().getParameters().get(0).getName());
    assertEquals("Bar", request1.getHttpRequest().getURI().getParameters().get(0).getValue());

    assertEquals(NetworkType.IPv4, request2.getConnection().getNetworkType());
    assertEquals("2.2.2.2", request2.getConnection().getNetworkAddress());
    assertEquals("Chrome 123", request2.getHttpRequest().getUserAgent().getUseragent());
    assertEquals("Bar", request2.getHttpRequest().getUserAgent().getId());
    assertEquals(HttpMethod.GET, request2.getHttpRequest().getURI().getMethod());
    assertEquals("/index.html", request2.getHttpRequest().getURI().getPath());
    assertEquals(1, request2.getHttpRequest().getURI().getParameters().size());
    assertEquals("Foo", request2.getHttpRequest().getURI().getParameters().get(0).getName());
    assertEquals("Bar", request2.getHttpRequest().getURI().getParameters().get(0).getValue());
  }

  @Test
  void createBuilderFromInstance() {
    Request.Builder requestBuilder1 = createPartialBuilder();
    requestBuilder1.getConnectionBuilder().setNetworkAddress("1.1.1.1");

    Request request1 = requestBuilder1.build();

    Request.Builder requestBuilder2 = Request.newBuilder(request1);

    requestBuilder2.getConnectionBuilder().setNetworkAddress("2.2.2.2");

    requestBuilder2.getHttpRequestBuilder().getUserAgentBuilder().setId("Bar");

    requestBuilder2.getHttpRequestBuilder().getURIBuilder().setMethod(HttpMethod.POST);

    requestBuilder2.getHttpRequestBuilder().getUserAgentBuilder().setUseragent("Firefox 456");

    Request request2 = requestBuilder2.build();

    assertEquals(NetworkType.IPv4, request1.getConnection().getNetworkType());
    assertEquals("1.1.1.1", request1.getConnection().getNetworkAddress());
    assertEquals("Chrome 123", request1.getHttpRequest().getUserAgent().getUseragent());
    assertEquals("Foo", request1.getHttpRequest().getUserAgent().getId());
    assertEquals(HttpMethod.GET, request1.getHttpRequest().getURI().getMethod());
    assertEquals("/index.html", request1.getHttpRequest().getURI().getPath());
    assertEquals(1, request1.getHttpRequest().getURI().getParameters().size());
    assertEquals("Foo", request1.getHttpRequest().getURI().getParameters().get(0).getName());
    assertEquals("Bar", request1.getHttpRequest().getURI().getParameters().get(0).getValue());

    assertEquals(NetworkType.IPv4, request2.getConnection().getNetworkType());
    assertEquals("2.2.2.2", request2.getConnection().getNetworkAddress());
    assertEquals("Firefox 456", request2.getHttpRequest().getUserAgent().getUseragent());
    assertEquals("Bar", request2.getHttpRequest().getUserAgent().getId());
    assertEquals(HttpMethod.POST, request2.getHttpRequest().getURI().getMethod());
    assertEquals("/index.html", request2.getHttpRequest().getURI().getPath());
    assertEquals(1, request2.getHttpRequest().getURI().getParameters().size());
    assertEquals("Foo", request2.getHttpRequest().getURI().getParameters().get(0).getName());
    assertEquals("Bar", request2.getHttpRequest().getURI().getParameters().get(0).getValue());
  }

  private Request.Builder createLastOneTestsBuilder() {
    Request.Builder requestBuilder = Request.newBuilder();
    requestBuilder.setTimestamp(1234567890);

    requestBuilder.getConnectionBuilder().setNetworkType(NetworkType.IPv4).setNetworkAddress("1.1.1.1");

    return requestBuilder;
  }

  @Test
  void lastOneWins_Setter() {
    Request.Builder requestBuilder = createLastOneTestsBuilder();

    requestBuilder.getHttpRequestBuilder().getURIBuilder().setMethod(HttpMethod.GET).setPath("/index.html");

    requestBuilder.getHttpRequestBuilder().getUserAgentBuilder().setUseragent("Chrome 123").setId("Foo");

    HttpRequest httpRequest = HttpRequest.newBuilder().setUserAgent(new UserAgent("Bar", "Firefox 321"))
        .setURI(HttpURI.newBuilder().setMethod(HttpMethod.POST).setPath("/login.php").build()).build();

    Request request = requestBuilder.setHttpRequest(httpRequest).build();

    assertEquals(NetworkType.IPv4, request.getConnection().getNetworkType());
    assertEquals("1.1.1.1", request.getConnection().getNetworkAddress());
    assertEquals(0, request.getHttpRequest().getURI().getParameters().size());

    assertEquals("Firefox 321", request.getHttpRequest().getUserAgent().getUseragent());
    assertEquals("Bar", request.getHttpRequest().getUserAgent().getId());
    assertEquals(HttpMethod.POST, request.getHttpRequest().getURI().getMethod());
    assertEquals("/login.php", request.getHttpRequest().getURI().getPath());
  }

  @Test
  void lastOneWins_Builder() {
    Request.Builder requestBuilder = createLastOneTestsBuilder();

    HttpRequest httpRequest = HttpRequest.newBuilder().setUserAgent(new UserAgent("Bar", "Firefox 321"))
        .setURI(HttpURI.newBuilder().setMethod(HttpMethod.POST).setPath("/login.php").build()).build();
    requestBuilder.setHttpRequest(httpRequest);

    requestBuilder.getHttpRequestBuilder().getURIBuilder().setMethod(HttpMethod.GET).setPath("/index.html");

    requestBuilder.getHttpRequestBuilder().getUserAgentBuilder().setUseragent("Chrome 123").setId("Foo");

    Request request = requestBuilder.build();

    assertEquals(NetworkType.IPv4, request.getConnection().getNetworkType());
    assertEquals("1.1.1.1", request.getConnection().getNetworkAddress());
    assertEquals("Chrome 123", request.getHttpRequest().getUserAgent().getUseragent());
    assertEquals("Foo", request.getHttpRequest().getUserAgent().getId());
    assertEquals(0, request.getHttpRequest().getURI().getParameters().size());

    assertEquals(HttpMethod.GET, request.getHttpRequest().getURI().getMethod());
    assertEquals("/index.html", request.getHttpRequest().getURI().getPath());
  }

  @Test
  void copyBuilderWithNullables() {
    RecordWithNullables.Builder builder = RecordWithNullables.newBuilder();

    assertFalse(builder.hasNullableRecordBuilder());
    assertFalse(builder.hasNullableRecord());
    assertFalse(builder.hasNullableString());
    assertFalse(builder.hasNullableLong());
    assertFalse(builder.hasNullableInt());
    assertFalse(builder.hasNullableMap());
    assertFalse(builder.hasNullableArray());

    RecordWithNullables.Builder builderCopy = RecordWithNullables.newBuilder(builder);

    assertFalse(builderCopy.hasNullableRecordBuilder());
    assertFalse(builderCopy.hasNullableRecord());
    assertFalse(builderCopy.hasNullableString());
    assertFalse(builderCopy.hasNullableLong());
    assertFalse(builderCopy.hasNullableInt());
    assertFalse(builderCopy.hasNullableMap());
    assertFalse(builderCopy.hasNullableArray());

    builderCopy.getNullableRecordBuilder();
  }

  @Test
  void copyBuilderWithNullablesAndSetToNull() {
    // Create builder with all values default to null, yet unset.
    RecordWithNullables.Builder builder = RecordWithNullables.newBuilder();

    // Ensure all values have not been set
    assertFalse(builder.hasNullableRecordBuilder());
    assertFalse(builder.hasNullableRecord());
    assertFalse(builder.hasNullableString());
    assertFalse(builder.hasNullableLong());
    assertFalse(builder.hasNullableInt());
    assertFalse(builder.hasNullableMap());
    assertFalse(builder.hasNullableArray());

    // Set all values to null
    builder.setNullableRecordBuilder(null);
    builder.setNullableRecord(null);
    builder.setNullableString(null);
    builder.setNullableLong(null);
    builder.setNullableInt(null);
    builder.setNullableMap(null);
    builder.setNullableArray(null);

    // A Builder remains False because it is null
    assertFalse(builder.hasNullableRecordBuilder());

    // Ensure all values have been set
    assertTrue(builder.hasNullableRecord());
    assertTrue(builder.hasNullableString());
    assertTrue(builder.hasNullableLong());
    assertTrue(builder.hasNullableInt());
    assertTrue(builder.hasNullableMap());
    assertTrue(builder.hasNullableArray());

    // Implicitly create a builder instance and clear the actual value.
    builder.getNullableRecordBuilder();
    assertTrue(builder.hasNullableRecordBuilder());
    assertFalse(builder.hasNullableRecord());

    // Create a copy of this builder.
    RecordWithNullables.Builder builderCopy = RecordWithNullables.newBuilder(builder);

    // Ensure all values are still the same
    assertTrue(builder.hasNullableRecordBuilder());
    assertFalse(builder.hasNullableRecord());
    assertTrue(builder.hasNullableString());
    assertTrue(builder.hasNullableLong());
    assertTrue(builder.hasNullableInt());
    assertTrue(builder.hasNullableMap());
    assertTrue(builder.hasNullableArray());
  }

  @Test
  void getBuilderForRecordWithNullRecord() {
    // Create a record with all nullable fields set to the default value : null
    RecordWithNullables recordWithNullables = RecordWithNullables.newBuilder().build();

    // Now create a Builder using this record as the base
    RecordWithNullables.Builder builder = RecordWithNullables.newBuilder(recordWithNullables);

    // In the past this caused an NPE
    builder.getNullableRecordBuilder();
  }

  @Test
  void getBuilderForNullRecord() {
    // In the past this caused an NPE
    RecordWithNullables.newBuilder((RecordWithNullables) null);
  }

  @Test
  void getBuilderForNullBuilder() {
    // In the past this caused an NPE
    RecordWithNullables.newBuilder((RecordWithNullables.Builder) null);
  }

  @Test
  void validateBrowsingOptionals() {
    Request.Builder requestBuilder = Request.newBuilder();
    requestBuilder.setTimestamp(1234567890);

    requestBuilder.getHttpRequestBuilder().getUserAgentBuilder().setUseragent("Chrome 123");

    requestBuilder.getHttpRequestBuilder().getURIBuilder().setMethod(HttpMethod.GET).setPath("/index.html");

    Request request = requestBuilder.build();

    assertEquals("Chrome 123", Optional.of(request).flatMap(Request::getOptionalHttpRequest)
        .flatMap(HttpRequest::getOptionalUserAgent).flatMap(UserAgent::getOptionalUseragent).orElse("UNKNOWN"));

    assertFalse(Optional.of(request).flatMap(Request::getOptionalHttpRequest).flatMap(HttpRequest::getOptionalUserAgent)
        .flatMap(UserAgent::getOptionalId).isPresent());

    assertEquals(HttpMethod.GET, Optional.of(request).flatMap(Request::getOptionalHttpRequest)
        .flatMap(HttpRequest::getOptionalURI).flatMap(HttpURI::getOptionalMethod).orElse(null));

    assertEquals("/index.html", Optional.of(request).flatMap(Request::getOptionalHttpRequest)
        .flatMap(HttpRequest::getOptionalURI).flatMap(HttpURI::getOptionalPath).orElse(null));

  }

}
