/**
 * Copyright (c) 2017 Contributors to the Eclipse Foundation
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.eclipse.microprofile.openapi.tck;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.beans.Introspector;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.Constructible;
import org.eclipse.microprofile.openapi.models.Extensible;
import org.eclipse.microprofile.openapi.models.ExternalDocumentation;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.PathItem;
import org.eclipse.microprofile.openapi.models.Paths;
import org.eclipse.microprofile.openapi.models.Reference;
import org.eclipse.microprofile.openapi.models.callbacks.Callback;
import org.eclipse.microprofile.openapi.models.examples.Example;
import org.eclipse.microprofile.openapi.models.headers.Header;
import org.eclipse.microprofile.openapi.models.info.Contact;
import org.eclipse.microprofile.openapi.models.info.Info;
import org.eclipse.microprofile.openapi.models.info.License;
import org.eclipse.microprofile.openapi.models.links.Link;
import org.eclipse.microprofile.openapi.models.media.Content;
import org.eclipse.microprofile.openapi.models.media.Discriminator;
import org.eclipse.microprofile.openapi.models.media.Encoding;
import org.eclipse.microprofile.openapi.models.media.MediaType;
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.media.XML;
import org.eclipse.microprofile.openapi.models.parameters.Parameter;
import org.eclipse.microprofile.openapi.models.parameters.RequestBody;
import org.eclipse.microprofile.openapi.models.responses.APIResponse;
import org.eclipse.microprofile.openapi.models.responses.APIResponses;
import org.eclipse.microprofile.openapi.models.security.OAuthFlow;
import org.eclipse.microprofile.openapi.models.security.OAuthFlows;
import org.eclipse.microprofile.openapi.models.security.Scopes;
import org.eclipse.microprofile.openapi.models.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.models.security.SecurityScheme;
import org.eclipse.microprofile.openapi.models.servers.Server;
import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
import org.eclipse.microprofile.openapi.models.servers.ServerVariables;
import org.eclipse.microprofile.openapi.models.tags.Tag;
import org.testng.annotations.Test;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;

/**
 * This test covers construction of the OpenAPI model. It verifies that the implementation can
 * create instances of all of the Constructible interfaces and then invokes methods (including
 * getters, setters and builders) on those instances to verify that they behave correctly.
 */
public class ModelConstructionTest extends Arquillian {
    
    @Deployment
    public static WebArchive createDeployment() {
        return ShrinkWrap.create(WebArchive.class);
    }

    // Container for matched getter, setter and builder methods
    static final class Property {
        private final String name;
        private final Class<?> type;
        private Method getter;
        private Method setter;
        private Method builder;
        
        public Property(String name, Class<?> type) {
            this.name = name;
            this.type = type;
        }
        public void addGetter(Method getter) {
            this.getter = getter;
        }
        public void addSetter(Method setter) {
            this.setter = setter;
        }
        public void addBuilder(Method builder) {
            this.builder = builder;
        }
        public String getName() {
            return name;
        }
        public Class<?> getType() {
            return type;
        }
        public boolean hasBuilder() {
            return builder != null;
        }
        public Object invokeGetter(Object target) {
            try {
                return getter.invoke(target);
            }
            catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                fail("Invocation of getter method \"" + getter.getName() + "\" failed: " + e.getMessage());
                throw new RuntimeException(e);
            }
        }
        public void invokeSetter(Object target, Object value) {
            try {
                setter.invoke(target, value);
            }
            catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                fail("Invocation of setter method \"" + setter.getName() + "\" failed: " + e.getMessage());
                throw new RuntimeException(e);
            }
        }
        public Object invokeBuilder(Object target, Object value) {
            try {
                return builder.invoke(target, value);
            }
            catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                fail("Invocation of builder method \"" + builder.getName() + "\" failed: " + e.getMessage());
                throw new RuntimeException(e);
            }
        }
        public boolean isCompatible(Class<?> type) {
            return this.type == type;
        }
        public boolean isPrimitive() {
            return type.isPrimitive();
        }
        public boolean isComplete() {
            return getter != null && setter != null;
        }
    }

    @Test
    public void componentsTest() {
        final Components c = processConstructible(Components.class);
        
        final String callbackKey = "myCallback";
        final Callback callbackValue = createConstructibleInstance(Callback.class);
        checkSameObject(c, c.addCallback(callbackKey, callbackValue));
        checkMapEntry(c.getCallbacks(), callbackKey, callbackValue);
        assertEquals(c.getCallbacks().size(), 1, "The list is expected to contain one entry.");
        c.removeCallback(callbackKey);
        assertEquals(c.getCallbacks().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getCallbacks, c::addCallback, "someCallback", callbackValue);
        
        final String exampleKey = "myExample";
        final Example exampleValue = createConstructibleInstance(Example.class);
        checkSameObject(c, c.addExample(exampleKey, exampleValue));
        checkMapEntry(c.getExamples(), exampleKey, exampleValue);
        assertEquals(c.getExamples().size(), 1, "The list is expected to contain one entry.");
        c.removeExample(exampleKey);
        assertEquals(c.getExamples().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getExamples, c::addExample, "someExample", exampleValue);
        
        final String headerKey = "myHeader";
        final Header headerValue = createConstructibleInstance(Header.class);
        checkSameObject(c, c.addHeader(headerKey, headerValue));
        checkMapEntry(c.getHeaders(), headerKey, headerValue);
        assertEquals(c.getHeaders().size(), 1, "The list is expected to contain one entry.");
        c.removeHeader(headerKey);
        assertEquals(c.getHeaders().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getHeaders, c::addHeader, "some-header", headerValue);
        
        final String linkKey = "myLink";
        final Link linkValue = createConstructibleInstance(Link.class);
        checkSameObject(c, c.addLink(linkKey, linkValue));
        checkMapEntry(c.getLinks(), linkKey, linkValue);
        assertEquals(c.getLinks().size(), 1, "The list is expected to contain one entry.");
        c.removeLink(linkKey);
        assertEquals(c.getLinks().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getLinks, c::addLink, "someLink", linkValue);
        
        final String parameterKey = "myParameter";
        final Parameter parameterValue = createConstructibleInstance(Parameter.class);
        checkSameObject(c, c.addParameter(parameterKey, parameterValue));
        checkMapEntry(c.getParameters(), parameterKey, parameterValue);
        assertEquals(c.getParameters().size(), 1, "The list is expected to contain one entry.");
        c.removeParameter(parameterKey);
        assertEquals(c.getParameters().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getParameters, c::addParameter, "someParameter", parameterValue);
        
        final String requestBodyKey = "myRequestBody";
        final RequestBody requestBodyValue = createConstructibleInstance(RequestBody.class);
        checkSameObject(c, c.addRequestBody(requestBodyKey, requestBodyValue));
        checkMapEntry(c.getRequestBodies(), requestBodyKey, requestBodyValue);
        assertEquals(c.getRequestBodies().size(), 1, "The list is expected to contain one entry.");
        c.removeRequestBody(requestBodyKey);
        assertEquals(c.getRequestBodies().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getRequestBodies, c::addRequestBody, "someRequestBody", requestBodyValue);
        
        final String responseKey = "myResponse";
        final APIResponse responseValue = createConstructibleInstance(APIResponse.class);
        checkSameObject(c, c.addResponse(responseKey, responseValue));
        checkMapEntry(c.getResponses(), responseKey, responseValue);
        assertEquals(c.getResponses().size(), 1, "The list is expected to contain one entry.");
        c.removeResponse(responseKey);
        assertEquals(c.getResponses().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getResponses, c::addResponse, "someResponse", responseValue);
        
        final String schemaKey = "mySchema";
        final Schema schemaValue = createConstructibleInstance(Schema.class);
        checkSameObject(c, c.addSchema(schemaKey, schemaValue));
        checkMapEntry(c.getSchemas(), schemaKey, schemaValue);
        assertEquals(c.getSchemas().size(), 1, "The list is expected to contain one entry.");
        c.removeSchema(schemaKey);
        assertEquals(c.getSchemas().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getSchemas, c::addSchema, "someSchema", schemaValue);
        
        final String securitySchemeKey = "mySecurityScheme";
        final SecurityScheme securitySchemeValue = createConstructibleInstance(SecurityScheme.class);
        checkSameObject(c, c.addSecurityScheme(securitySchemeKey, securitySchemeValue));
        checkMapEntry(c.getSecuritySchemes(), securitySchemeKey, securitySchemeValue);
        assertEquals(c.getSecuritySchemes().size(), 1, "The list is expected to contain one entry.");
        c.removeSecurityScheme(securitySchemeKey);
        assertEquals(c.getSecuritySchemes().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(c::getSecuritySchemes, c::addSecurityScheme, "someSecurityScheme", securitySchemeValue);
    }
    
    @Test
    public void externalDocumentationTest() {
        processConstructible(ExternalDocumentation.class);
    }
    
    @Test
    public void openAPITest() {
        final OpenAPI o = processConstructible(OpenAPI.class);
        
        final SecurityRequirement sr = createConstructibleInstance(SecurityRequirement.class);
        sr.addScheme("BasicAuth");
        checkSameObject(o, o.addSecurityRequirement(sr));
        checkListEntry(o.getSecurity(), sr);
        assertEquals(o.getSecurity().size(), 1, "The list is expected to contain one entry.");
        o.removeSecurityRequirement(sr);
        assertEquals(o.getSecurity().size(), 0, "The list is expected to be empty.");
        
        final Server s = createConstructibleInstance(Server.class);
        checkSameObject(o, o.addServer(s));
        checkListEntry(o.getServers(), s);
        assertEquals(o.getServers().size(), 1, "The list is expected to contain one entry.");
        o.removeServer(s);
        assertEquals(o.getServers().size(), 0, "The list is expected to be empty.");
        
        final Tag t = createConstructibleInstance(Tag.class);
        checkSameObject(o, o.addTag(t));
        checkListEntry(o.getTags(), t);
        assertEquals(o.getTags().size(), 1, "The list is expected to contain one entry.");
        o.removeTag(t);
        assertEquals(o.getTags().size(), 0, "The list is expected to be empty.");
    }
    
    @Test
    public void operationTest() {
        final Operation o = processConstructible(Operation.class);
        
        final Parameter p = createConstructibleInstance(Parameter.class);
        checkSameObject(o, o.addParameter(p));
        checkListEntry(o.getParameters(), p);
        assertEquals(o.getParameters().size(), 1, "The list is expected to contain one entry.");
        o.removeParameter(p);
        assertEquals(o.getParameters().size(), 0, "The list is expected to be empty.");
        
        final SecurityRequirement sr = createConstructibleInstance(SecurityRequirement.class);
        sr.addScheme("OAuth2", Arrays.asList("read", "write"));
        checkSameObject(o, o.addSecurityRequirement(sr));
        checkListEntry(o.getSecurity(), sr);
        assertEquals(o.getSecurity().size(), 1, "The list is expected to contain one entry.");
        o.removeSecurityRequirement(sr);
        assertEquals(o.getSecurity().size(), 0, "The list is expected to be empty.");
        
        final Server s = createConstructibleInstance(Server.class);
        checkSameObject(o, o.addServer(s));
        checkListEntry(o.getServers(), s);
        assertEquals(o.getServers().size(), 1, "The list is expected to contain one entry.");
        o.removeServer(s);
        assertEquals(o.getServers().size(), 0, "The list is expected to be empty.");
        
        final String tag = new String("myTag");
        checkSameObject(o, o.addTag(tag));
        checkListEntry(o.getTags(), tag);
        assertEquals(o.getTags().size(), 1, "The list is expected to contain one entry.");
        o.removeTag(tag);
        assertEquals(o.getTags().size(), 0, "The list is expected to be empty.");
        
        final String callbackKey = "myCallback";
        final Callback callbackValue = createConstructibleInstance(Callback.class);
        checkSameObject(o, o.addCallback(callbackKey, callbackValue));
        checkMapEntry(o.getCallbacks(), callbackKey, callbackValue);
        assertEquals(o.getCallbacks().size(), 1, "The list is expected to contain one entry.");
        o.removeCallback(callbackKey);
        assertEquals(o.getCallbacks().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(o::getCallbacks, o::addCallback, "someCallback", callbackValue);
    }
    
    @Test
    public void pathItemTest() {
        final PathItem pi = processConstructible(PathItem.class);
        
        final Parameter p = createConstructibleInstance(Parameter.class);
        checkSameObject(pi, pi.addParameter(p));
        checkListEntry(pi.getParameters(), p);
        assertEquals(pi.getParameters().size(), 1, "The list is expected to contain one entry.");
        pi.removeParameter(p);
        assertEquals(pi.getParameters().size(), 0, "The list is expected to be empty.");
        
        final Server s = createConstructibleInstance(Server.class);
        checkSameObject(pi, pi.addServer(s));
        checkListEntry(pi.getServers(), s);
        assertEquals(pi.getServers().size(), 1, "The list is expected to contain one entry.");
        pi.removeServer(s);
        assertEquals(pi.getServers().size(), 0, "The list is expected to be empty.");
        
        final Operation o1 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.GET(o1));
        checkSameObject(o1, pi.getGET());
        
        final Operation o2 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.PUT(o2));
        checkSameObject(o2, pi.getPUT());
        
        final Operation o3 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.POST(o3));
        checkSameObject(o3, pi.getPOST());
        
        final Operation o4 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.DELETE(o4));
        checkSameObject(o4, pi.getDELETE());
        
        final Operation o5 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.OPTIONS(o5));
        checkSameObject(o5, pi.getOPTIONS());
        
        final Operation o6 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.HEAD(o6));
        checkSameObject(o6, pi.getHEAD());
        
        final Operation o7 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.PATCH(o7));
        checkSameObject(o7, pi.getPATCH());
        
        final Operation o8 = createConstructibleInstance(Operation.class);
        checkSameObject(pi, pi.TRACE(o8));
        checkSameObject(o8, pi.getTRACE());
        
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.GET, o1);
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.PUT, o2);
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.POST, o3);
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.DELETE, o4);
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.OPTIONS, o5);
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.HEAD, o6);
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.PATCH, o7);
        checkMapEntry(pi.getOperations(), PathItem.HttpMethod.TRACE, o8);
    }
    
    @Test
    public void pathsTest() {
        final Paths p = processConstructible(Paths.class);
        
        final String pathItemKey = "/myPathItem";
        assertFalse(p.hasPathItem(pathItemKey), pathItemKey + " is absent in the map");
        final PathItem pathItemValue = createConstructibleInstance(PathItem.class);
        checkSameObject(p, p.addPathItem(pathItemKey, pathItemValue));
        assertTrue(p.hasPathItem(pathItemKey), pathItemKey + " is present in the map");
        assertSame(p.getPathItem(pathItemKey), pathItemValue, 
                "The value associated with the key: " + pathItemKey + " is expected to be the same one that was added.");
        checkMapEntry(p, pathItemKey, pathItemValue);
        checkMapEntry(p.getPathItems(), pathItemKey, pathItemValue);
        
        final String pathItemKey2 = "/myPathItem2";
        assertFalse(p.hasPathItem(pathItemKey2), pathItemKey2 + " is absent in the map");
        final PathItem pathItemValue2 = createConstructibleInstance(PathItem.class);
        assertNull(p.put(pathItemKey2, pathItemValue2), "No previous mapping expected.");
        assertTrue(p.hasPathItem(pathItemKey2), pathItemKey2 + " is present in the map");
        assertSame(p.getPathItem(pathItemKey2), pathItemValue2, 
                "The value associated with the key: " + pathItemKey2 + " is expected to be the same one that was added.");
        checkMapEntry(p, pathItemKey2, pathItemValue2);
        checkMapEntry(p.getPathItems(), pathItemKey2, pathItemValue2);
        
        assertEquals(p.size(), 2, "The map is expected to contain two entries.");
        assertEquals(p.getPathItems().size(), 2, "The map is expected to contain two entries.");
        
        p.removePathItem(pathItemKey);
        assertFalse(p.hasPathItem(pathItemKey), pathItemKey + " is absent in the map");
        assertEquals(p.size(), 1, "The map is expected to contain one entry.");
        assertEquals(p.getPathItems().size(), 1, "The map is expected to contain one entry.");
        
        p.remove(pathItemKey2);
        assertFalse(p.hasPathItem(pathItemKey2), pathItemKey + " is absent in the map");
        assertEquals(p.size(), 0, "The map is expected to contain 0 entries.");
        assertEquals(p.getPathItems().size(), 0, "The map is expected to contain 0 entries.");
        
        final PathItem otherValue = createConstructibleInstance(PathItem.class);
        checkMapImmutable(p, Paths::getPathItems, "/otherPathItem", otherValue);
        checkNullValueInAdd(p::getPathItems, p::addPathItem, "/other", otherValue);
    }
    
    @Test
    public void callbackTest() {
        final Callback c = processConstructible(Callback.class);
        
        final String pathItemKey = "myPathItem";
        assertFalse(c.hasPathItem(pathItemKey), pathItemKey + " is absent in the map");
        final PathItem pathItemValue = createConstructibleInstance(PathItem.class);
        checkSameObject(c, c.addPathItem(pathItemKey, pathItemValue));
        assertTrue(c.hasPathItem(pathItemKey), pathItemKey + " is present in the map");
        assertSame(c.getPathItem(pathItemKey), pathItemValue, 
                "The value associated with the key: " + pathItemKey + " is expected to be the same one that was added.");
        checkMapEntry(c, pathItemKey, pathItemValue);
        checkMapEntry(c.getPathItems(), pathItemKey, pathItemValue);
        
        final String pathItemKey2 = "myPathItem2";
        assertFalse(c.hasPathItem(pathItemKey2), pathItemKey2 + " is absent in the map");
        final PathItem pathItemValue2 = createConstructibleInstance(PathItem.class);
        assertNull(c.put(pathItemKey2, pathItemValue2), "No previous mapping expected.");
        assertTrue(c.hasPathItem(pathItemKey2), pathItemKey2 + " is present in the map");
        assertSame(c.getPathItem(pathItemKey2), pathItemValue2, 
                "The value associated with the key: " + pathItemKey2 + " is expected to be the same one that was added.");
        checkMapEntry(c, pathItemKey2, pathItemValue2);
        checkMapEntry(c.getPathItems(), pathItemKey2, pathItemValue2);
        
        assertEquals(c.size(), 2, "The map is expected to contain two entries.");
        assertEquals(c.getPathItems().size(), 2, "The map is expected to contain two entries.");
        
        c.removePathItem(pathItemKey);
        assertFalse(c.hasPathItem(pathItemKey), pathItemKey + " is absent in the map");
        assertEquals(c.size(), 1, "The map is expected to contain one entry.");
        assertEquals(c.getPathItems().size(), 1, "The map is expected to contain one entry.");
        
        c.remove(pathItemKey2);
        assertFalse(c.hasPathItem(pathItemKey2), pathItemKey + " is absent in the map");
        assertEquals(c.size(), 0, "The map is expected to contain 0 entries.");
        assertEquals(c.getPathItems().size(), 0, "The map is expected to contain 0 entries.");
        
        final PathItem otherValue = createConstructibleInstance(PathItem.class);
        checkMapImmutable(c, Callback::getPathItems, "otherPathItem", otherValue);
        checkNullValueInAdd(c::getPathItems, c::addPathItem, "other", otherValue);
    }
    
    @Test
    public void exampleTest() {
        processConstructible(Example.class);
    }
    
    @Test
    public void headerTest() {
        final Header h = processConstructible(Header.class);
        
        final String exampleKey = "myExample";
        final Example exampleValue = createConstructibleInstance(Example.class);
        checkSameObject(h, h.addExample(exampleKey, exampleValue));
        checkMapEntry(h.getExamples(), exampleKey, exampleValue);
        assertEquals(h.getExamples().size(), 1, "The list is expected to contain one entry.");
        h.removeExample(exampleKey);
        assertEquals(h.getExamples().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(h::getExamples, h::addExample, "otherExample", exampleValue);
    }
    
    @Test
    public void contactTest() {
        processConstructible(Contact.class);
    }
    
    @Test
    public void infoTest() {
        processConstructible(Info.class);
    }
    
    @Test
    public void licenseTest() {
        processConstructible(License.class);
    }
    
    @Test
    public void linkTest() {
        final Link l = processConstructible(Link.class);
        
        final String parameterKey = "myParameter";
        final String parameterValue = "$request.parameter.id";
        checkSameObject(l, l.addParameter(parameterKey, parameterValue));
        checkMapEntry(l.getParameters(), parameterKey, parameterValue);
        assertEquals(l.getParameters().size(), 1, "The list is expected to contain one entry.");
        l.removeParameter(parameterKey);
        assertEquals(l.getParameters().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(l::getParameters, l::addParameter, "otherParameter", parameterValue);
    }
    
    @Test
    public void contentTest() {
        final Content c = processConstructible(Content.class);
        
        final String mediaTypeKey = "application/json";
        assertFalse(c.hasMediaType(mediaTypeKey), mediaTypeKey + " is absent in the map");
        final MediaType mediaTypeValue = createConstructibleInstance(MediaType.class);
        checkSameObject(c, c.addMediaType(mediaTypeKey, mediaTypeValue));
        assertTrue(c.hasMediaType(mediaTypeKey), mediaTypeKey + " is present in the map");
        assertSame(c.getMediaType(mediaTypeKey), mediaTypeValue, 
                "The value associated with the key: " + mediaTypeKey + " is expected to be the same one that was added.");
        checkMapEntry(c, mediaTypeKey, mediaTypeValue);
        checkMapEntry(c.getMediaTypes(), mediaTypeKey, mediaTypeValue);
        
        final String mediaTypeKey2 = "*/*";
        assertFalse(c.hasMediaType(mediaTypeKey2), mediaTypeKey2 + " is absent in the map");
        final MediaType mediaTypeValue2 = createConstructibleInstance(MediaType.class);
        assertNull(c.put(mediaTypeKey2, mediaTypeValue2), "No previous mapping expected.");
        assertTrue(c.hasMediaType(mediaTypeKey2), mediaTypeKey2 + " is present in the map");
        assertSame(c.getMediaType(mediaTypeKey2), mediaTypeValue2, 
                "The value associated with the key: " + mediaTypeKey2 + " is expected to be the same one that was added.");
        checkMapEntry(c, mediaTypeKey2, mediaTypeValue2);
        checkMapEntry(c.getMediaTypes(), mediaTypeKey2, mediaTypeValue2);
        
        assertEquals(c.size(), 2, "The map is expected to contain two entries.");
        assertEquals(c.getMediaTypes().size(), 2, "The map is expected to contain two entries.");
        
        c.removeMediaType(mediaTypeKey);
        assertFalse(c.hasMediaType(mediaTypeKey), mediaTypeKey + " is absent in the map");
        assertEquals(c.size(), 1, "The map is expected to contain one entry.");
        assertEquals(c.getMediaTypes().size(), 1, "The map is expected to contain one entry.");
        
        c.remove(mediaTypeKey2);
        assertFalse(c.hasMediaType(mediaTypeKey2), mediaTypeKey + " is absent in the map");
        assertEquals(c.size(), 0, "The map is expected to contain 0 entries.");
        assertEquals(c.getMediaTypes().size(), 0, "The map is expected to contain 0 entries.");
        
        final MediaType otherValue = createConstructibleInstance(MediaType.class);
        checkMapImmutable(c, Content::getMediaTypes, "application/txt", otherValue);
    }
    
    @Test
    public void discriminatorTest() {
        final Discriminator d = processConstructible(Discriminator.class);
        
        final String key = "myKey";
        final String value = new String("myValue");
        checkSameObject(d, d.addMapping(key, value));
        checkMapEntry(d.getMapping(), key, value);
        assertEquals(d.getMapping().size(), 1, "The list is expected to contain one entry.");
        d.removeMapping(key);
        assertEquals(d.getMapping().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(d::getMapping, d::addMapping, "otherKey", value);
    }
    
    @Test
    public void encodingTest() {
        Encoding e = processConstructible(Encoding.class);
        
        final String headerKey = "myHeaderKey";
        final Header headerValue = createConstructibleInstance(Header.class);
        checkSameObject(e, e.addHeader(headerKey, headerValue));
        checkMapEntry(e.getHeaders(), headerKey, headerValue);
        assertEquals(e.getHeaders().size(), 1, "The list is expected to contain one entry.");
        e.removeHeader(headerKey);
        assertEquals(e.getHeaders().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(e::getHeaders, e::addHeader, "otherHeaderKey", headerValue);
    }
    
    @Test
    public void mediaTypeTest() {
        final MediaType mt = processConstructible(MediaType.class);
        
        final String encodingKey = "myEncoding";
        final Encoding encodingValue = createConstructibleInstance(Encoding.class);
        checkSameObject(mt, mt.addEncoding(encodingKey, encodingValue));
        checkMapEntry(mt.getEncoding(), encodingKey, encodingValue);
        assertEquals(mt.getEncoding().size(), 1, "The list is expected to contain one entry.");
        mt.removeEncoding(encodingKey);
        assertEquals(mt.getEncoding().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(mt::getEncoding, mt::addEncoding, "otherEncoding", encodingValue);
        
        final String exampleKey = "myExample";
        final Example exampleValue = createConstructibleInstance(Example.class);
        checkSameObject(mt, mt.addExample(exampleKey, exampleValue));
        checkMapEntry(mt.getExamples(), exampleKey, exampleValue);
        assertEquals(mt.getExamples().size(), 1, "The list is expected to contain one entry.");
        mt.removeExample(exampleKey);
        assertEquals(mt.getExamples().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(mt::getExamples, mt::addExample, "otherExample", exampleValue);
    }
    
    @Test
    public void schemaTest() {
        final Schema s = processConstructible(Schema.class);
        
        final Schema ap = createConstructibleInstance(Schema.class);
        checkSameObject(s, s.additionalPropertiesSchema(ap));
        checkSameObject(ap, s.getAdditionalPropertiesSchema());
        assertEquals(s.getAdditionalPropertiesBoolean(), null, "AdditionalProperties (Boolean type) is expected to be null");
        checkSameObject(s, s.additionalPropertiesBoolean(Boolean.TRUE));
        assertEquals(s.getAdditionalPropertiesBoolean(), Boolean.TRUE, "AdditionalProperties (Boolean type) is expected to be true");
        assertEquals(s.getAdditionalPropertiesSchema(), null, "AdditionalProperties (Schema type) is expected to be null");
        s.setAdditionalPropertiesBoolean(Boolean.FALSE);
        assertEquals(s.getAdditionalPropertiesBoolean(), Boolean.FALSE, "AdditionalProperties (Boolean type) is expected to be false");
        assertEquals(s.getAdditionalPropertiesSchema(), null, "AdditionalProperties (Schema type) is expected to be null");
        s.setAdditionalPropertiesSchema(null);
        assertEquals(s.getAdditionalPropertiesBoolean(), null, "AdditionalProperties (Boolean type) is expected to be null");
        assertEquals(s.getAdditionalPropertiesSchema(), null, "AdditionalProperties (Schema type) is expected to be null");
        
        final Schema allOf = createConstructibleInstance(Schema.class);
        checkSameObject(s, s.addAllOf(allOf));
        checkListEntry(s.getAllOf(), allOf);
        assertEquals(s.getAllOf().size(), 1, "The list is expected to contain one entry.");
        s.removeAllOf(allOf);
        assertEquals(s.getAllOf().size(), 0, "The list is expected to be empty.");
        
        final Schema anyOf = createConstructibleInstance(Schema.class);
        checkSameObject(s, s.addAnyOf(anyOf));
        checkListEntry(s.getAnyOf(), anyOf);
        assertEquals(s.getAnyOf().size(), 1, "The list is expected to contain one entry.");
        s.removeAnyOf(anyOf);
        assertEquals(s.getAnyOf().size(), 0, "The list is expected to be empty.");
        
        final String enumeration = new String("enumValue");
        checkSameObject(s, s.addEnumeration(enumeration));
        checkListEntry(s.getEnumeration(), enumeration);
        assertEquals(s.getEnumeration().size(), 1, "The list is expected to contain one entry.");
        s.removeEnumeration(enumeration);
        assertEquals(s.getEnumeration().size(), 0, "The list is expected to be empty.");
        
        final Schema oneOf = createConstructibleInstance(Schema.class);
        checkSameObject(s, s.addOneOf(oneOf));
        checkListEntry(s.getOneOf(), oneOf);
        assertEquals(s.getOneOf().size(), 1, "The list is expected to contain one entry.");
        s.removeOneOf(oneOf);
        assertEquals(s.getOneOf().size(), 0, "The list is expected to be empty.");
        
        final String propertySchemaKey = "myPropertySchemaKey";
        final Schema propertySchemaValue = createConstructibleInstance(Schema.class);
        checkSameObject(s, s.addProperty(propertySchemaKey, propertySchemaValue));
        checkMapEntry(s.getProperties(), propertySchemaKey, propertySchemaValue);
        assertEquals(s.getProperties().size(), 1, "The list is expected to contain one entry.");
        s.removeProperty(propertySchemaKey);
        assertEquals(s.getProperties().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(s::getProperties, s::addProperty, "otherProperty", propertySchemaValue);
        
        final String required = new String("required");
        checkSameObject(s, s.addRequired(required));
        checkListEntry(s.getRequired(), required);
        assertEquals(s.getRequired().size(), 1, "The list is expected to contain one entry.");
        s.removeRequired(required);
        assertEquals(s.getRequired().size(), 0, "The list is expected to be empty.");
    }
    
    @Test
    public void xmlTest() {
        processConstructible(XML.class);
    }
    
    @Test
    public void parameterTest() {
        final Parameter p = processConstructible(Parameter.class);
        
        final String exampleKey = "myExample";
        final Example exampleValue = createConstructibleInstance(Example.class);
        checkSameObject(p, p.addExample(exampleKey, exampleValue));
        checkMapEntry(p.getExamples(), exampleKey, exampleValue);
        assertEquals(p.getExamples().size(), 1, "The list is expected to contain one entry.");
        p.removeExample(exampleKey);
        assertEquals(p.getExamples().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(p::getExamples, p::addExample, "otherExample", exampleValue);
    }
    
    @Test
    public void requestBodyTest() {
        processConstructible(RequestBody.class);
    }
    
    @Test
    public void apiResponseTest() {
        final APIResponse response = processConstructible(APIResponse.class);
        
        final String headerKey = "myHeaderKey";
        final Header headerValue = createConstructibleInstance(Header.class);
        checkSameObject(response, response.addHeader(headerKey, headerValue));
        checkMapEntry(response.getHeaders(), headerKey, headerValue);
        assertEquals(response.getHeaders().size(), 1, "The list is expected to contain one entry.");
        response.removeHeader(headerKey);
        assertEquals(response.getHeaders().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(response::getHeaders, response::addHeader, "some-header", headerValue);
        
        final String linkKey = "myLinkKey";
        final Link linkValue = createConstructibleInstance(Link.class);
        checkSameObject(response, response.addLink(linkKey, linkValue));
        checkMapEntry(response.getLinks(), linkKey, linkValue);
        assertEquals(response.getLinks().size(), 1, "The list is expected to contain one entry.");
        response.removeLink(linkKey);
        assertEquals(response.getLinks().size(), 0, "The list is expected to be empty.");
        checkNullValueInAdd(response::getLinks, response::addLink, "someLinkKey", linkValue);
    }
    
    @Test
    public void apiResponsesTest() {
        final APIResponses responses = processConstructible(APIResponses.class);
        
        responses.remove(APIResponses.DEFAULT);
        assertEquals(responses.size(), 0, "The map is expected to contain two entries.");
        
        final String responseKey = "200";
        assertFalse(responses.hasAPIResponse(responseKey), responseKey + " is absent in the map");
        final APIResponse pathItemValue = createConstructibleInstance(APIResponse.class);
        checkSameObject(responses, responses.addAPIResponse(responseKey, pathItemValue));
        assertTrue(responses.hasAPIResponse(responseKey), responseKey + " is present in the map");
        assertSame(responses.getAPIResponse(responseKey), pathItemValue, 
                "The value associated with the key: " + responseKey + " is expected to be the same one that was added.");
        checkMapEntry(responses, responseKey, pathItemValue);
        checkMapEntry(responses.getAPIResponses(), responseKey, pathItemValue);
        
        final String responseKey2 = "4XX";
        assertFalse(responses.hasAPIResponse(responseKey2), responseKey2 + " is absent in the map");
        final APIResponse pathItemValue2 = createConstructibleInstance(APIResponse.class);
        assertNull(responses.put(responseKey2, pathItemValue2), "No previous mapping expected.");
        assertTrue(responses.hasAPIResponse(responseKey2), responseKey2 + " is present in the map");
        assertSame(responses.getAPIResponse(responseKey2), pathItemValue2, 
                "The value associated with the key: " + responseKey2 + " is expected to be the same one that was added.");
        checkMapEntry(responses, responseKey2, pathItemValue2);
        checkMapEntry(responses.getAPIResponses(), responseKey2, pathItemValue2);
        
        assertEquals(responses.size(), 2, "The map is expected to contain two entries.");
        assertEquals(responses.getAPIResponses().size(), 2, "The map is expected to contain two entries.");
        
        responses.removeAPIResponse(responseKey);
        assertFalse(responses.hasAPIResponse(responseKey), responseKey + " is absent in the map");
        assertEquals(responses.size(), 1, "The map is expected to contain one entry.");
        assertEquals(responses.getAPIResponses().size(), 1, "The map is expected to contain one entry.");
        
        responses.remove(responseKey2);
        assertFalse(responses.hasAPIResponse(responseKey2), responseKey + " is absent in the map");
        assertEquals(responses.size(), 0, "The map is expected to contain 0 entries.");
        assertEquals(responses.getAPIResponses().size(), 0, "The map is expected to contain 0 entries.");
        
        final APIResponse otherValue = createConstructibleInstance(APIResponse.class);
        checkMapImmutable(responses, APIResponses::getAPIResponses, "500", otherValue);
        
        assertNull(responses.getDefaultValue(), "No default value expected.");
        final String responseKey3 = APIResponses.DEFAULT;
        final APIResponse responseValue3 = createConstructibleInstance(APIResponse.class);
        assertNull(responses.put(responseKey3, responseValue3), "No previous mapping expected.");
        checkMapEntry(responses, responseKey3, responseValue3);
        checkSameObject(responseValue3, responses.getDefaultValue());
        
        assertEquals(responses.size(), 1, "The map is expected to contain one entry.");
        
        responses.setDefaultValue(null);
        assertNull(responses.get(APIResponses.DEFAULT), "No default value expected.");
        assertNull(responses.getDefaultValue(), "No default value expected.");
        
        final APIResponse responseValue4 = createConstructibleInstance(APIResponse.class);
        responses.setDefaultValue(responseValue4);
        checkMapEntry(responses, APIResponses.DEFAULT, responseValue4);
        checkSameObject(responseValue4, responses.getDefaultValue());
        
        checkNullValueInAdd(responses::getAPIResponses, responses::addAPIResponse, "4XX", otherValue);
    }
    
    @Test
    public void oAuthFlowTest() {
        processConstructible(OAuthFlow.class);
    }
    
    @Test
    public void oAuthFlowsTest() {
        processConstructible(OAuthFlows.class);
    }
    
    @Test
    public void scopesTest() {
        final Scopes s = processConstructible(Scopes.class);
        
        final String scopeKey = "myScope";
        assertFalse(s.hasScope(scopeKey), scopeKey + " is absent in the map");
        final String scopeValue = new String("myDescription");
        checkSameObject(s, s.addScope(scopeKey, scopeValue));
        assertTrue(s.hasScope(scopeKey), scopeKey + " is present in the map");
        assertSame(s.getScope(scopeKey), scopeValue, 
                "The value associated with the key: " + scopeKey + " is expected to be the same one that was added.");
        checkMapEntry(s, scopeKey, scopeValue);
        checkMapEntry(s.getScopes(), scopeKey, scopeValue);
        
        final String scopeKey2 = "myScope2";
        assertFalse(s.hasScope(scopeKey2), scopeKey2 + " is absent in the map");
        final String scopeValue2 = new String("myDescription2");
        assertNull(s.put(scopeKey2, scopeValue2), "No previous mapping expected.");
        assertTrue(s.hasScope(scopeKey2), scopeKey2 + " is present in the map");
        assertSame(s.getScope(scopeKey2), scopeValue2, 
                "The value associated with the key: " + scopeKey2 + " is expected to be the same one that was added.");
        checkMapEntry(s, scopeKey2, scopeValue2);
        checkMapEntry(s.getScopes(), scopeKey2, scopeValue2);
        
        assertEquals(s.size(), 2, "The map is expected to contain two entries.");
        assertEquals(s.getScopes().size(), 2, "The map is expected to contain two entries.");
        
        s.removeScope(scopeKey);
        assertFalse(s.hasScope(scopeKey), scopeKey + " is absent in the map");
        assertEquals(s.size(), 1, "The map is expected to contain one entry.");
        assertEquals(s.getScopes().size(), 1, "The map is expected to contain one entry.");
        
        s.remove(scopeKey2);
        assertFalse(s.hasScope(scopeKey2), scopeKey + " is absent in the map");
        assertEquals(s.size(), 0, "The map is expected to contain 0 entries.");
        assertEquals(s.getScopes().size(), 0, "The map is expected to contain 0 entries.");
        
        final String otherValue = new String("otherDescription");
        checkMapImmutable(s, Scopes::getScopes, "otherScope", otherValue);
        
        String scopeKey3 = "myScope3";
        s.addScope(scopeKey3, null);
        assertTrue(s.hasScope(scopeKey3), "Expected " + scopeKey3 + " to be present in the map");
        assertNull(s.getScope(scopeKey3), "Expected the value of " + scopeKey3 + " to be null.");
    }
    
    @Test
    public void securityRequirementTest() {
        final SecurityRequirement sr = processConstructible(SecurityRequirement.class);
        
        final String schemeKey = "myScheme";
        assertFalse(sr.hasScheme(schemeKey), schemeKey + " is absent in the map");
        final List<String> schemeValue = new ArrayList<String>();
        checkSameObject(sr, sr.addScheme(schemeKey, schemeValue));
        assertTrue(sr.hasScheme(schemeKey), schemeKey + " is present in the map");
        assertSame(sr.getScheme(schemeKey), schemeValue, 
                "The value associated with the key: " + schemeKey + " is expected to be the same one that was added.");
        checkMapEntry(sr, schemeKey, schemeValue);
        checkMapEntry(sr.getSchemes(), schemeKey, schemeValue);
        
        final String schemeKey2 = "myScheme2";
        assertFalse(sr.hasScheme(schemeKey2), schemeKey2 + " is absent in the map");
        final List<String> schemeValue2 = new ArrayList<String>();
        assertNull(sr.put(schemeKey2, schemeValue2), "No previous mapping expected.");
        assertTrue(sr.hasScheme(schemeKey2), schemeKey2 + " is present in the map");
        assertSame(sr.getScheme(schemeKey2), schemeValue2, 
                "The value associated with the key: " + schemeKey2 + " is expected to be the same one that was added.");
        checkMapEntry(sr, schemeKey2, schemeValue2);
        checkMapEntry(sr.getSchemes(), schemeKey2, schemeValue2);
        
        assertEquals(sr.size(), 2, "The map is expected to contain two entries.");
        assertEquals(sr.getSchemes().size(), 2, "The map is expected to contain two entries.");
        
        sr.removeScheme(schemeKey);
        assertFalse(sr.hasScheme(schemeKey), schemeKey + " is absent in the map");
        assertEquals(sr.size(), 1, "The map is expected to contain one entry.");
        assertEquals(sr.getSchemes().size(), 1, "The map is expected to contain one entry.");
        
        sr.remove(schemeKey2);
        assertFalse(sr.hasScheme(schemeKey2), schemeKey + " is absent in the map");
        assertEquals(sr.size(), 0, "The map is expected to contain 0 entries.");
        assertEquals(sr.getSchemes().size(), 0, "The map is expected to contain 0 entries.");
        
        final List<String> otherValue = new ArrayList<String>();
        checkMapImmutable(sr, SecurityRequirement::getSchemes, "otherScheme", otherValue);
        
        final String schemeKey3 = "myScheme3";
        sr.addScheme(schemeKey3, (String) null);
        assertTrue(sr.hasScheme(schemeKey3), "Expected " + schemeKey3 + " to be present");
        final List<String> schemeValue3 = Collections.emptyList();
        assertEquals(sr.getScheme(schemeKey3), schemeValue3, 
                "The value associated with the key: " + schemeKey3 + " is expected to be an empty list.");

        final String schemeKey4 = "myScheme3";
        sr.addScheme(schemeKey4, (List<String>) null);
        assertTrue(sr.hasScheme(schemeKey4), "Expected " + schemeKey4 + " to be present");
        final List<String> schemeValue4 = Collections.emptyList();
        assertEquals(sr.getScheme(schemeKey4), schemeValue4, 
                "The value associated with the key: " + schemeKey4 + " is expected to be an empty list.");
    }
    
    @Test
    public void securitySchemeTest() {
        processConstructible(SecurityScheme.class);
    }
    
    @Test
    public void serverTest() {
        Server server = processConstructible(Server.class);
        
        final ServerVariable sv1 = createConstructibleInstance(ServerVariable.class);
        server.setVariables(Collections.singletonMap("var1", sv1));
        Map<String,ServerVariable> variables = server.getVariables();
        assertEquals(variables.size(), 1, "The map is expected to contain one entry.");
        assertTrue(variables.containsKey("var1"), "The map is expected to contain a 'var1' entry.");
        assertEquals(variables.get("var1"), sv1, "The value corresponding to the 'var1' is wrong.");
        
        server.setVariables((Map<String, ServerVariable>) null);
        assertNull(server.getVariables(), "The value is expected to be null.");
    }
    
    @Test
    public void serverVariableTest() {
        final ServerVariable sv = processConstructible(ServerVariable.class);
        
        final String enumeration = new String("enumValue");
        checkSameObject(sv, sv.addEnumeration(enumeration));
        checkListEntry(sv.getEnumeration(), enumeration);
        assertEquals(sv.getEnumeration().size(), 1, "The list is expected to contain one entry.");
        sv.removeEnumeration(enumeration);
        assertEquals(sv.getEnumeration().size(), 0, "The list is expected to be empty.");
    }
    
    @Test
    @Deprecated
    public void serverVariablesTest() {
        final ServerVariables svs = processConstructible(ServerVariables.class);
        
        final String varKey = "myServerVariable";
        assertFalse(svs.hasServerVariable(varKey), varKey + " is absent in the map");
        final ServerVariable varValue = createConstructibleInstance(ServerVariable.class);
        checkSameObject(svs, svs.addServerVariable(varKey, varValue));
        assertTrue(svs.hasServerVariable(varKey), varKey + " is present in the map");
        assertSame(svs.getServerVariable(varKey), varValue, 
                "The value associated with the key: " + varKey + " is expected to be the same one that was added.");
        checkMapEntry(svs, varKey, varValue);
        checkMapEntry(svs.getServerVariables(), varKey, varValue);
        
        final String varKey2 = "myServerVariable2";
        assertFalse(svs.hasServerVariable(varKey2), varKey2 + " is absent in the map");
        final ServerVariable varValue2 = createConstructibleInstance(ServerVariable.class);
        assertNull(svs.put(varKey2, varValue2), "No previous mapping expected.");
        assertTrue(svs.hasServerVariable(varKey2), varKey2 + " is present in the map");
        assertSame(svs.getServerVariable(varKey2), varValue2, 
                "The value associated with the key: " + varKey2 + " is expected to be the same one that was added.");
        checkMapEntry(svs, varKey2, varValue2);
        checkMapEntry(svs.getServerVariables(), varKey2, varValue2);
        
        assertEquals(svs.size(), 2, "The map is expected to contain two entries.");
        assertEquals(svs.getServerVariables().size(), 2, "The map is expected to contain two entries.");
        
        svs.removeServerVariable(varKey);
        assertFalse(svs.hasServerVariable(varKey), varKey + " is absent in the map");
        assertEquals(svs.size(), 1, "The map is expected to contain one entry.");
        assertEquals(svs.getServerVariables().size(), 1, "The map is expected to contain one entry.");
        
        svs.remove(varKey2);
        assertFalse(svs.hasServerVariable(varKey2), varKey + " is absent in the map");
        assertEquals(svs.size(), 0, "The map is expected to contain 0 entries.");
        assertEquals(svs.getServerVariables().size(), 0, "The map is expected to contain 0 entries.");
        
        final ServerVariable otherValue = createConstructibleInstance(ServerVariable.class);
        checkMapImmutable(svs, ServerVariables::getServerVariables, "otherServerVariable", otherValue);
        checkNullValueInAdd(svs::getServerVariables, svs::addServerVariable, "other", otherValue);
    }
    
    @Test
    public void tagTest() {
        processConstructible(Tag.class);
    }

    private <T extends Constructible> T processConstructible(Class<T> clazz) {
        final T o = createConstructibleInstance(clazz);
        if (o instanceof Extensible && Extensible.class.isAssignableFrom(clazz)) {
            processExtensible((Extensible<?>) o);
        }
        if (o instanceof Reference && Reference.class.isAssignableFrom(clazz)) {
            processReference((Reference<?>) o);
        }
        final Map<String,Property> properties = collectProperties(clazz);
        properties.values().stream().filter((p) -> p.isComplete()).forEach((p) -> {
            processConstructibleProperty(o, p, clazz);
        });
        return o;
    }
    
    private <T extends Constructible> T createConstructibleInstance(Class<T> clazz) {
        // Check that the OASFactory is able to create an instance of the given Class.
        final T o1 = OASFactory.createObject(clazz);
        assertNotNull(o1, "The return value of OASFactory.createObject(" + clazz.getName() + ") must not be null.");
        assertTrue(clazz.isInstance(o1), "The return value of OASFactory.createObject() is expected to be an instance of: " + clazz.getName());
        final T o2 = OASFactory.createObject(clazz);
        assertNotNull(o2, "The return value of OASFactory.createObject(" + clazz.getName() + ") must not be null.");
        assertTrue(clazz.isInstance(o2), "The return value of OASFactory.createObject() is expected to be an instance of: " + clazz.getName());
        assertNotSame(o2, o1, "OASFactory.createObject(" + clazz.getName() + ") is expected to create a new object on each invocation.");
        return o1;
    }
    
    private void processExtensible(Extensible<?> e) {
        final String extensionName1 = "x-" + e.getClass().getName() + "-1";
        final Object obj1 = new Object();
        final String extensionName2 = "x-" + e.getClass().getName() + "-2";
        final Object obj2 = new Object();
        // Check that extensions can be added to and retrieved from the map.
        e.addExtension(extensionName1, obj1);
        e.addExtension(extensionName2, obj2);
        final Map<String, Object> map = e.getExtensions();
        assertEquals(map.size(), 2, "The extensions map is expected to contain two entries.");
        assertTrue(map.containsKey(extensionName1), "The extensions map is expected to contain the key: " + extensionName1);
        assertTrue(map.containsKey(extensionName2), "The extensions map is expected to contain the key: " + extensionName2);
        assertSame(map.get(extensionName1), obj1,
                "The value associated with the key: " + extensionName1 + " is expected to be the same one that was added.");
        assertSame(map.get(extensionName2), obj2,
                "The value associated with the key: " + extensionName2 + " is expected to be the same one that was added.");
        e.removeExtension(extensionName1);
        assertEquals(e.getExtensions().size(), 1, "The extensions map is expected to contain one entry.");
        // Check that the extension map can be replaced with the setter and that it is returned by the getter.
        final Map<String, Object> newMap = new HashMap<>();
        e.setExtensions(newMap);
        final Map<String, Object> map2 = e.getExtensions();
        assertEquals(map2.size(), 0, "The extensions map is expected to contain no entries.");
        assertEquals(map2, newMap, "The return value of getExtensions() is expected to be the same value that was set.");
        // Check that the extension map can be replaced with the builder method and that it is returned by the getter.
        final Map<String, Object> newOtherMap = new HashMap<>();
        newOtherMap.put("x-test", 42);
        e.setExtensions(newOtherMap);
        final Map<String, Object> map3 = e.getExtensions();
        assertEquals(map3.size(), 1, "The extensions map is expected to contain one entry.");
        assertEquals(map3, newOtherMap, "The return value of getExtensions() is expected to be the same value that was set.");
    }
    
    private void processReference(Reference<?> r) {
        // Check that the ref value can be set using the setter method and that the getter method returns the same value.
        final String myRef1 = createReference(r, "myRef1");
        r.setRef(myRef1);
        assertEquals(r.getRef(), myRef1, "The return value of getRef() is expected to be equal to the value that was set.");
        // Check that the short name ref value can be set using the setter method and that the getter method returns the expanded value.
        if (!(r instanceof PathItem)) {
            final String shortName = "myRef2";
            final String myRef2 = createReference(r, shortName);
            r.setRef(shortName);
            assertEquals(r.getRef(), myRef2, "The return value of getRef() is expected to be a fully expanded name.");
        }
        // Check that the ref value can be set using the builder method and that the getter method returns the same value.
        final String myRef3 = createReference(r, "myRef3");
        final Reference<?> self = r.ref(myRef3);
        assertSame(self, r, "The return value of ref() is expected to return the current instance.");
        assertEquals(r.getRef(), myRef3, "The return value of getRef() is expected to be equal to the value that was set.");
        // Check that the short name ref value can be set using the builder method and that the getter method returns the expanded value.
        if (!(r instanceof PathItem)) {
            final String shortName = "myRef4";
            final String myRef4 = createReference(r, shortName);
            final Reference<?> self2 = r.ref(shortName);
            assertSame(self2, r, "The return value of ref() is expected to return the current instance.");
            assertEquals(r.getRef(), myRef4, "The return value of getRef() is expected to be a fully expanded name.");
        }
    }
    
    private void processConstructibleProperty(Constructible o, Property p, Class<?> enclosingInterface) {
        final Object value1 = getInstanceOf(p.getType(), false);
        p.invokeSetter(o, value1);
        if (!p.isPrimitive() && !p.isCompatible(Map.class) && !p.isCompatible(List.class)) {
            assertSame(p.invokeGetter(o), value1, "The return value of the getter method for property \"" + 
                    p.getName() + "\" of interface \"" + enclosingInterface.getName() +
                    "\" is expected to be the same as the value that was set.");
        }
        else {
            assertEquals(p.invokeGetter(o), value1, "The return value of the getter method for property \"" + 
                    p.getName() + "\" of interface \"" + enclosingInterface.getName() +
                    "\" is expected to be equal to the value that was set.");
        }
        if (p.hasBuilder()) {
            final Object value2 = getInstanceOf(p.getType(), true);
            final Object self = p.invokeBuilder(o, value2);
            assertSame(self, o, "The return value of the builder method for property \"" + 
                    p.getName() + "\" of interface \"" + enclosingInterface.getName() +
                    "\" is expected to be the same as the value that was set.");
            if (!p.isPrimitive() && !p.isCompatible(Map.class) && !p.isCompatible(List.class)) {
                assertSame(p.invokeGetter(o), value2, "The return value of the getter method for property \"" + 
                        p.getName() + "\" of interface \"" + enclosingInterface.getName() +
                        "\" is expected to be the same as the value that was set.");
            }
            else {
                assertEquals(p.invokeGetter(o), value2, "The return value of the getter method for property \"" + 
                        p.getName() + "\" of interface \"" + enclosingInterface.getName() +
                        "\" is expected to be equal to the value that was set.");
            }
        }
    }
    
    // Returns instances for testing getter, setter and builder methods.
    @SuppressWarnings("unchecked")
    private Object getInstanceOf(Class<?> clazz, boolean alternateEnumValue) {
        if (Constructible.class.isAssignableFrom(clazz)) {
            return createConstructibleInstance((Class<Constructible>) clazz);
        }
        else if (Enum.class.isAssignableFrom(clazz)) {
            final Object[] enumConstants = clazz.getEnumConstants();
            if (enumConstants != null && enumConstants.length > 0) {
                if (alternateEnumValue && enumConstants.length > 1) {
                    return enumConstants[1];
                }
                return enumConstants[0];
            }  
        }
        else if (clazz == List.class) {
            return new ArrayList<Object>();
        }
        else if (clazz == Map.class) {
            return new HashMap<Object,Object>();
        }
        else if (clazz == String.class) {
            return new String("value");
        }
        else if (clazz == Boolean.class || clazz == Boolean.TYPE) {
            return new Boolean(true);
        }
        else if (clazz == Byte.class || clazz == Byte.TYPE) {
            return new Byte((byte) 1);
        }
        else if (clazz == Short.class || clazz == Short.TYPE) {
            return new Short((short) 1);
        }
        else if (clazz == Integer.class || clazz == Integer.TYPE) {
            return new Integer(1);
        }
        else if (clazz == Long.class || clazz == Long.TYPE) {
            return new Long(1L);
        }
        else if (clazz == Float.class || clazz == Float.TYPE) {
            return new Float(1);
        }
        else if (clazz == Double.class || clazz == Double.TYPE) {
            return new Double(1);
        }
        else if (clazz == Character.class || clazz == Character.TYPE) {
            return new Character('a');
        }
        else if (clazz == BigInteger.class) {
            return new BigInteger("1");
        }
        else if (clazz == BigDecimal.class) {
            return new BigDecimal("1.0");
        }
        else if (clazz == Object.class) {
            return new String("object");
        }
        return null;
    }
    
    private String createReference(Reference<?> r, String v) {
        final StringBuilder sb = new StringBuilder();
        if (r instanceof APIResponse) {
            sb.append("#/components/responses/");
        }
        else if (r instanceof Callback) {
            sb.append("#/components/callbacks/");
        }
        else if (r instanceof Example) {
            sb.append("#/components/examples/");
        }
        else if (r instanceof Header) {
            sb.append("#/components/headers/");
        }
        else if (r instanceof Link) {
            sb.append("#/components/links/");
        }
        else if (r instanceof Parameter) {
            sb.append("#/components/parameters/");
        }
        else if (r instanceof PathItem) {
            sb.append("http://www.abc.def.ghi/");
        }
        else if (r instanceof RequestBody) {
            sb.append("#/components/requestBodies/");
        }
        else if (r instanceof Schema) {
            sb.append("#/components/schemas/");
        }
        else if (r instanceof SecurityScheme) {
            sb.append("#/components/securitySchemes/");
        }
        sb.append(v);
        return sb.toString();
    }
      
    private Map<String,Property> collectProperties(Class<?> clazz) {
        final Map<String,Property> properties = new HashMap<>();
        final Method[] methods = clazz.getDeclaredMethods();
        Arrays.stream(methods).forEach(m -> {
            Class<?> returnType = m.getReturnType();
            int parameterCount = m.getParameterCount();
            String name = m.getName();
            Property p;
            Class<?> type;
            // Possible builder method
            if (returnType == clazz) {
                if (parameterCount == 1) {
                    type = m.getParameterTypes()[0];
                    p = properties.get(name);
                    if (p == null) {
                        p = new Property(name, type);
                        properties.put(name, p);
                    }
                    if (p.isCompatible(type)) {
                        p.addBuilder(m);
                    }
                }
            }
            // Possible setter method
            else if (returnType == Void.TYPE) {
                if (name.startsWith("set") && parameterCount == 1) {
                    name = Introspector.decapitalize(name.substring(3));
                    type = m.getParameterTypes()[0];
                    p = properties.get(name);
                    if (p == null) {
                        p = new Property(name, type);
                        properties.put(name, p);
                    }
                    if (p.isCompatible(type)) {
                        p.addSetter(m);
                    }
                }
            }
            // Possible getter method
            else {
                if (name.startsWith("get") && parameterCount == 0) {
                    name = Introspector.decapitalize(name.substring(3));
                    type = returnType;
                    p = properties.get(name);
                    if (p == null) {
                        p = new Property(name, type);
                        properties.put(name, p);
                    }
                    if (p.isCompatible(type)) {
                        p.addGetter(m);
                    }
                }
            }
        });
        return properties;
    }
    
    private <K, T> void checkMapEntry(Map<K,T> map, K key, T value) {
        assertNotNull(map, "The map must not be null.");
        assertTrue(map.containsKey(key), "The map is expected to contain the key: " + key);
        assertSame(map.get(key), value, "The value associated with the key: " + key + " is expected to be the same one that was added.");
    }

    private <O, K, T> void checkMapImmutable(O container, Function<O, Map<K,T>> mapGetter, K key, T otherValue) {
        Map<K,T> map = mapGetter.apply(container);
        assertNotNull(map, "The map must not be null.");
        assertFalse(map.containsKey(key), "The map is expected to not contain the key: " + key);
        int originalSize = map.size();
        try {
            map.put(key, otherValue);
        }
        catch (Exception e) {
            //It is allowed to throw an exception
        }
        Map<K,T> map2 = mapGetter.apply(container);
        assertNotNull(map2, "The map must not be null.");
        assertFalse(map2.containsKey(key), "The map is expected to not contain the key: " + key);
        assertEquals(map2.size(), originalSize, "The map is expected to have a size of " + originalSize);
    }
    
    private <O, T> void checkNullValueInAdd(Supplier<Map<String, T>> mapGetter, BiFunction<String, T, O> mapAdd, String key, T value) {
        // add null as value for 'key'
        try {
            mapAdd.apply(key, null);
        }
        catch (Exception e) {
            //It is allowed to throw an exception
        }
        assertFalse(mapGetter.get().containsKey(key), "The map is expected to not contain the key: " + key);
        
        // add value as value for 'key'
        mapAdd.apply(key, value);
        assertTrue(mapGetter.get().containsKey(key), "The map is expected to contain the key: " + key);
        
        // add null again as value for 'key'
        try {
            mapAdd.apply(key, null);
        }
        catch (Exception e) {
            //It is allowed to throw an exception
        }
        assertTrue(mapGetter.get().containsKey(key), "The map is expected to contain the key: " + key);
    }
    
    private <T> void checkListEntry(List<T> list, T value) {
        assertNotNull(list, "The list must not be null.");
        assertTrue(list.stream().anyMatch((v) -> v == value), "The list is expected to contain the value: " + value);
    }
    
    private <T> void checkSameObject(T expected, T actual) {
        assertSame(actual, expected ,"Expecting same object.");
    }
}
