package org.mockserver.mock;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableMap;
import org.hamcrest.CoreMatchers;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.InjectMocks;
import org.mockserver.configuration.Configuration;
import org.mockserver.file.FilePath;
import org.mockserver.file.FileReader;
import org.mockserver.log.MockServerEventLog;
import org.mockserver.time.EpochService;
import org.mockserver.time.GlobalFixedTime;
import org.mockserver.time.TimeService;
import org.mockserver.log.model.LogEntry;
import org.mockserver.logging.MockServerLogger;
import org.mockserver.matchers.TimeToLive;
import org.mockserver.matchers.Times;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.mockserver.model.MediaType;
import org.mockserver.model.RetrieveType;
import org.mockserver.responsewriter.ResponseWriter;
import org.mockserver.scheduler.Scheduler;
import org.mockserver.serialization.*;
import org.mockserver.serialization.java.ExpectationToJavaSerializer;
import org.mockserver.verify.Verification;
import org.mockserver.verify.VerificationSequence;
import org.slf4j.event.Level;

import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static junit.framework.TestCase.fail;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.MockitoAnnotations.openMocks;
import static org.mockserver.character.Character.NEW_LINE;
import static org.mockserver.configuration.Configuration.configuration;
import static org.mockserver.log.model.LogEntry.LOG_DATE_FORMAT;
import static org.mockserver.log.model.LogEntry.LogMessageType.*;
import static org.mockserver.log.model.LogEntryMessages.RECEIVED_REQUEST_MESSAGE_FORMAT;
import static org.mockserver.mock.Expectation.when;
import static org.mockserver.mock.OpenAPIExpectation.openAPIExpectation;
import static org.mockserver.model.ExpectationId.expectationId;
import static org.mockserver.model.Format.LOG_ENTRIES;
import static org.mockserver.model.HttpError.error;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.notFoundResponse;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;
import static org.mockserver.model.PortBinding.portBinding;
import static org.mockserver.model.RetrieveType.REQUEST_RESPONSES;
import static org.slf4j.event.Level.INFO;

/**
 * @author jamesdbloom
 */
public class HttpStateTest {

    @Rule
    public final ExpectedException exception = ExpectedException.none();
    private final RequestDefinitionSerializer requestDefinitionSerializer = new RequestDefinitionSerializer(new MockServerLogger());
    private final ExpectationIdSerializer expectationIdSerializer = new ExpectationIdSerializer(new MockServerLogger());
    private final ExpectationSerializer expectationSerializer = new ExpectationSerializer(new MockServerLogger());
    private final ExpectationSerializer expectationSerializerWithDefaultFields = new ExpectationSerializer(new MockServerLogger(), true);
    private final OpenAPIExpectationSerializer openAPIExpectationSerializer = new OpenAPIExpectationSerializer(new MockServerLogger());
    private final ExpectationToJavaSerializer expectationToJavaSerializer = new ExpectationToJavaSerializer();
    private final PortBindingSerializer portBindingSerializer = new PortBindingSerializer(new MockServerLogger());
    private final VerificationSerializer verificationSerializer = new VerificationSerializer(new MockServerLogger());
    private final VerificationSequenceSerializer verificationSequenceSerializer = new VerificationSequenceSerializer(new MockServerLogger());

    private final Configuration configuration = configuration();
    @InjectMocks
    private HttpState httpState;

    @ClassRule
    public static final GlobalFixedTime fixedTime = new GlobalFixedTime();

    @Before
    public void prepareTestFixture() {
        configuration.detailedVerificationFailures(false);
        Scheduler scheduler = mock(Scheduler.class);
        httpState = new HttpState(configuration, new MockServerLogger(configuration, MockServerLogger.class), scheduler);
        openMocks(this);
    }

    @After
    public void resetClock() {
        TimeService.reset();
    }

    private static class FakeResponseWriter extends ResponseWriter {
        public HttpResponse response;

        protected FakeResponseWriter() {
            super(configuration(), new MockServerLogger());
        }

        @Override
        public void sendResponse(HttpRequest request, HttpResponse response) {
            this.response = response;
        }
    }

    @Test
    public void shouldHandleRetrieveRequestsRequest() {
        // given
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setType(RECEIVED_REQUEST)
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        HttpRequest expectationRetrieveRequestsRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withBody(
                requestDefinitionSerializer.serialize(request("request_one"))
            );
        boolean handle = httpState.handle(expectationRetrieveRequestsRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        assertThat(responseWriter.response.getBodyAsString(), is(requestDefinitionSerializer.serialize(true, Collections.singletonList(
            request("request_one")
        ))));
    }

    @Test
    public void shouldHandleClearRequest() {
        // given
        httpState.add(new Expectation(request("request_one")).thenRespond(response("response_one")));
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setType(EXPECTATION_MATCHED)
        );
        HttpRequest clearRequest = request("/mockserver/clear")
            .withMethod("PUT")
            .withBody(
                requestDefinitionSerializer.serialize(request("request_one"))
            );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(clearRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        assertThat(responseWriter.response.getBodyAsString(), is(""));
        assertThat(httpState.firstMatchingExpectation(request("request_one")), is(nullValue()));
        assertThat(httpState.retrieve(request("/mockserver/retrieve")
            .withMethod("PUT")
            .withBody(
                requestDefinitionSerializer.serialize(request("request_one"))
            )), is(response().withBody("[]", MediaType.JSON_UTF_8).withStatusCode(200)));
    }

    @Test
    public void shouldHandleReturnStatusRequest() {
        // given
        HttpRequest statusRequest = request("/mockserver/status").withMethod("PUT");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(statusRequest, responseWriter, false);

        // then
        assertThat(handle, is(false));
        assertThat(responseWriter.response, is(nullValue()));
    }

    @Test
    public void shouldHandleBindNewPortsRequest() {
        // given
        HttpRequest statusRequest = request("/mockserver/bind")
            .withMethod("PUT")
            .withBody(portBindingSerializer.serialize(
                portBinding(1090, 1090)
            ));
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(statusRequest, responseWriter, false);

        // then
        assertThat(handle, is(false));
        assertThat(responseWriter.response, is(nullValue()));
    }

    @Test
    public void shouldHandleStopRequest() {
        // given
        HttpRequest statusRequest = request("/mockserver/stop")
            .withMethod("PUT");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(statusRequest, responseWriter, false);

        // then
        assertThat(handle, is(false));
        assertThat(responseWriter.response, is(nullValue()));
    }

    @Test
    public void shouldHandleRetrieveRecordedExpectationsRequest() {
        // given
        httpState.log(
            new LogEntry()
                .setType(FORWARDED_REQUEST)
                .setHttpRequest(request("request_one"))
                .setHttpResponse(response("response_one"))
                .setExpectation(new Expectation(request("request_one"), Times.once(), TimeToLive.unlimited(), 0).withId("key_one").thenRespond(response("response_one")))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        HttpRequest expectationRetrieveExpectationsRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withQueryStringParameter("type", RetrieveType.RECORDED_EXPECTATIONS.name())
            .withBody(
                requestDefinitionSerializer.serialize(request("request_one"))
            );
        boolean handle = httpState.handle(expectationRetrieveExpectationsRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        assertThat(responseWriter.response.getBodyAsString(), is(expectationSerializerWithDefaultFields.serialize(Collections.singletonList(
            new Expectation(request("request_one"), Times.once(), TimeToLive.unlimited(), 0).withId("key_one").thenRespond(response("response_one"))
        ))));
    }

    @Test
    public void shouldHandleRetrieveRequestResponsesAsHar() {
        // given
        httpState.log(
            new LogEntry()
                .setType(FORWARDED_REQUEST)
                .setHttpRequest(request("/api/test").withMethod("GET").withHeader("host", "example.com"))
                .setHttpResponse(response().withStatusCode(200).withReasonPhrase("OK").withBody("hello"))
                .setExpectation(new Expectation(request("/api/test"), Times.once(), TimeToLive.unlimited(), 0).withId("key_one").thenRespond(response().withStatusCode(200).withReasonPhrase("OK").withBody("hello")))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        HttpRequest retrieveRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withQueryStringParameter("type", REQUEST_RESPONSES.name())
            .withQueryStringParameter("format", "HAR")
            .withBody(
                requestDefinitionSerializer.serialize(request("/api/test"))
            );
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("\"version\" : \"1.2\""));
        assertThat(body, containsString("\"creator\""));
        assertThat(body, containsString("\"MockServer\""));
        assertThat(body, containsString("\"entries\""));
        assertThat(body, containsString("\"method\" : \"GET\""));
        assertThat(body, containsString("http://example.com/api/test"));
        assertThat(body, containsString("\"status\" : 200"));
        assertThat(body, containsString("\"statusText\" : \"OK\""));
    }

    @Test
    public void shouldHandleRetrieveRequestResponsesAsCurl() {
        // given
        httpState.log(
            new LogEntry()
                .setType(FORWARDED_REQUEST)
                .setHttpRequest(request("/api/test").withMethod("GET").withHeader("host", "example.com"))
                .setHttpResponse(response().withStatusCode(200).withReasonPhrase("OK").withBody("hello"))
                .setExpectation(new Expectation(request("/api/test"), Times.once(), TimeToLive.unlimited(), 0).withId("key_one").thenRespond(response().withStatusCode(200).withReasonPhrase("OK").withBody("hello")))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        HttpRequest retrieveRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withQueryStringParameter("type", REQUEST_RESPONSES.name())
            .withQueryStringParameter("format", "CURL")
            .withBody(
                requestDefinitionSerializer.serialize(request("/api/test"))
            );
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("curl -v"));
        assertThat(body, containsString("http://example.com/api/test"));
    }

    @Test
    public void shouldRejectCurlForActiveExpectations() {
        // given
        httpState.add(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_one")));
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        HttpRequest retrieveRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withQueryStringParameter("type", RetrieveType.ACTIVE_EXPECTATIONS.name())
            .withQueryStringParameter("format", "CURL");
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then — completes cleanly with a clear message rather than hanging
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getBodyAsString(), containsString("CURL not supported for ACTIVE_EXPECTATIONS"));
    }

    @Test
    public void shouldHandleRetrieveRequestsAsCurl() {
        // given
        httpState.log(
            new LogEntry()
                .setType(RECEIVED_REQUEST)
                .setHttpRequest(request("/api/test").withMethod("GET").withHeader("host", "example.com"))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        HttpRequest retrieveRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withQueryStringParameter("type", RetrieveType.REQUESTS.name())
            .withQueryStringParameter("format", "CURL")
            .withBody(requestDefinitionSerializer.serialize(request("/api/test")));
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("curl -v"));
        assertThat(body, containsString("http://example.com/api/test"));
    }

    @Test
    public void shouldRejectCurlForRecordedExpectations() {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        HttpRequest retrieveRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withQueryStringParameter("type", RetrieveType.RECORDED_EXPECTATIONS.name())
            .withQueryStringParameter("format", "CURL");
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then — completes cleanly with a clear message rather than hanging
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getBodyAsString(), containsString("CURL not supported for RECORDED_EXPECTATIONS"));
    }

    @Test
    public void shouldHandleRetrieveLogMessagesRequest() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.add(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_one")));
        FakeResponseWriter responseWriter = new FakeResponseWriter();

            // when
            HttpRequest retrieveLogRequest = request("/mockserver/retrieve")
                .withMethod("PUT")
                .withQueryStringParameter("type", RetrieveType.LOGS.name())
                .withBody(
                    requestDefinitionSerializer.serialize(request("request_one"))
                );
            boolean handle = httpState.handle(retrieveLogRequest, responseWriter, false);

            // then
            assertThat(handle, is(true));
            assertThat(responseWriter.response.getStatusCode(), is(200));
            assertThat(
                responseWriter.response.getBodyAsString(),
                is(endsWith(LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - creating expectation:" + NEW_LINE +
                    NEW_LINE +
                    "  {" + NEW_LINE +
                    "    \"httpRequest\" : {" + NEW_LINE +
                    "      \"path\" : \"request_one\"" + NEW_LINE +
                    "    }," + NEW_LINE +
                    "    \"httpResponse\" : {" + NEW_LINE +
                    "      \"statusCode\" : 200," + NEW_LINE +
                    "      \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                    "      \"body\" : \"response_one\"" + NEW_LINE +
                    "    }," + NEW_LINE +
                    "    \"id\" : \"key_one\"," + NEW_LINE +
                    "    \"priority\" : 0," + NEW_LINE +
                    "    \"timeToLive\" : {" + NEW_LINE +
                    "      \"unlimited\" : true" + NEW_LINE +
                    "    }," + NEW_LINE +
                    "    \"times\" : {" + NEW_LINE +
                    "      \"unlimited\" : true" + NEW_LINE +
                    "    }" + NEW_LINE +
                    "  }" + NEW_LINE +
                    NEW_LINE +
                    " with id:" + NEW_LINE +
                    NEW_LINE +
                    "  key_one" + NEW_LINE +
                    NEW_LINE))
            );
    }

    @Test
    public void shouldHandleAddExpectationRequest() {
        // given
        Expectation expectationOne = new Expectation(request("request_one")).thenRespond(response("response_one"));
        HttpRequest request = request("/mockserver/expectation").withMethod("PUT").withBody(
            expectationSerializer.serialize(expectationOne)
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(request, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        assertThat(responseWriter.response.getBodyAsString(), CoreMatchers.containsString("[ {" + NEW_LINE +
            "  \"httpRequest\" : {" + NEW_LINE +
            "    \"path\" : \"request_one\"" + NEW_LINE +
            "  }," + NEW_LINE +
            "  \"httpResponse\" : {" + NEW_LINE +
            "    \"statusCode\" : 200," + NEW_LINE +
            "    \"reasonPhrase\" : \"OK\"," + NEW_LINE +
            "    \"body\" : \"response_one\"" + NEW_LINE +
            "  }," + NEW_LINE +
            "  \"id\" : \""));
        assertThat(responseWriter.response.getBodyAsString(), CoreMatchers.containsString("\"," + NEW_LINE +
            "  \"priority\" : 0," + NEW_LINE +
            "  \"timeToLive\" : {" + NEW_LINE +
            "    \"unlimited\" : true" + NEW_LINE +
            "  }," + NEW_LINE +
            "  \"times\" : {" + NEW_LINE +
            "    \"unlimited\" : true" + NEW_LINE +
            "  }" + NEW_LINE +
            "} ]"));
        assertThat(httpState.firstMatchingExpectation(request("request_one")), is(expectationOne));
    }

    @Test
    public void shouldHandleAddOpenAPIJsonRequest() throws JsonProcessingException {
        // given
        HttpRequest request = request("/mockserver/openapi").withMethod("PUT").withBody(
            openAPIExpectationSerializer.serialize(openAPIExpectation(
                FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.json")
            ))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(request, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        List<Expectation> actualExpectations = Arrays.asList(expectationSerializer.deserializeArray(responseWriter.response.getBodyAsString(), true));
        shouldBuildPetStoreExpectations(ObjectMapperFactory.createObjectMapper().readTree(FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.json")).toPrettyString(), actualExpectations);
    }

    @Test
    public void shouldHandleAddOpenAPIJsonRequestWithSpecificResponses() throws JsonProcessingException {
        // given
        HttpRequest request = request("/mockserver/openapi").withMethod("PUT").withBody(
            openAPIExpectationSerializer.serialize(openAPIExpectation(
                FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.json"), ImmutableMap.of(
                    "listPets", "500",
                    "createPets", "default",
                    "showPetById", "200"
                )
            ))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(request, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        List<Expectation> actualExpectations = Arrays.asList(expectationSerializer.deserializeArray(responseWriter.response.getBodyAsString(), true));
        shouldBuildPetStoreExpectationsWithSpecificResponses(ObjectMapperFactory.createObjectMapper().readTree(FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.json")).toPrettyString(), actualExpectations);
    }

    @Test
    public void shouldHandleInvalidOpenAPIJsonRequest() {
        // given
        HttpRequest request = request("/mockserver/openapi").withMethod("PUT").withBody(
            openAPIExpectationSerializer.serialize(openAPIExpectation("" +
                "\"openapi\": \"3.0.0\"," + NEW_LINE +
                "  \"info\": {" + NEW_LINE +
                "    \"version\": \"1.0.0\"," + NEW_LINE +
                "    \"title\": \"Swagger Petstore\""
            ))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(request, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), is("Unable to load API spec, while parsing a block mapping" + NEW_LINE +
            " in 'reader', line 1, column 1:" + NEW_LINE +
            "    \"openapi\": \"3.0.0\"," + NEW_LINE +
            "    ^" + NEW_LINE +
            "expected <block end>, but found ','" + NEW_LINE +
            " in 'reader', line 1, column 19:" + NEW_LINE +
            "    \"openapi\": \"3.0.0\"," + NEW_LINE +
            "                      ^"));
    }

    @Test
    public void shouldHandleAddOpenAPIYamlRequest() {
        // given
        HttpRequest request = request("/mockserver/openapi").withMethod("PUT").withBody(
            openAPIExpectationSerializer.serialize(openAPIExpectation(
                FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.yaml")
            ))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(request, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        List<Expectation> actualExpectations = Arrays.asList(expectationSerializer.deserializeArray(responseWriter.response.getBodyAsString(), true));
        shouldBuildPetStoreExpectations(FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.yaml"), actualExpectations);
    }

    @Test
    public void shouldHandleAddOpenAPIYamlRequestWithSpecificResponses() {
        // given
        HttpRequest request = request("/mockserver/openapi").withMethod("PUT").withBody(
            openAPIExpectationSerializer.serialize(openAPIExpectation(
                FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.yaml"), ImmutableMap.of(
                    "listPets", "500",
                    "createPets", "default",
                    "showPetById", "200"
                )
            ))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(request, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        List<Expectation> actualExpectations = Arrays.asList(expectationSerializer.deserializeArray(responseWriter.response.getBodyAsString(), true));
        shouldBuildPetStoreExpectationsWithSpecificResponses(FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.yaml"), actualExpectations);
    }

    @Test
    public void shouldHandleInvalidOpenAPIYamlRequest() {
        // given
        HttpRequest request = request("/mockserver/openapi").withMethod("PUT").withBody(
            openAPIExpectationSerializer.serialize(openAPIExpectation(
                FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.yaml").substring(0, 100)
            ))
        );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(request, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), is("Unable to load API spec, while scanning a simple key" + NEW_LINE +
            " in 'reader', line 8, column 1:" + NEW_LINE +
            "    servers" + NEW_LINE +
            "    ^" + NEW_LINE +
            "could not find expected ':'" + NEW_LINE +
            " in 'reader', line 8, column 8:" + NEW_LINE +
            "    servers" + NEW_LINE +
            "           ^"));
    }

    @Test
    public void shouldHandleRetrieveActiveExpectationsRequest() {
        // given
        Expectation expectationOne = new Expectation(request("request_one")).thenRespond(response("response_one"));
        httpState.add(expectationOne);
        HttpRequest expectationRetrieveExpectationsRequest = request("/mockserver/retrieve")
            .withMethod("PUT")
            .withQueryStringParameter("type", RetrieveType.ACTIVE_EXPECTATIONS.name())
            .withBody(
                requestDefinitionSerializer.serialize(request("request_one"))
            );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(expectationRetrieveExpectationsRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        assertThat(responseWriter.response.getBodyAsString(), is(expectationSerializer.serialize(Collections.singletonList(
            expectationOne
        ))));
    }

    @Test
    public void shouldHandleVerifyRequest() {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );
        HttpRequest expectationRetrieveExpectationsRequest = request("/mockserver/verify")
            .withMethod("PUT")
            .withBody(
                verificationSerializer.serialize(
                    new Verification()
                        .withRequest(request("two"))
                )
            );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(expectationRetrieveExpectationsRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(202));
        assertThat(responseWriter.response.getBodyAsString(), is(""));
    }

    @Test
    public void shouldHandleVerifyFailureRequest() {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        HttpRequest expectationRetrieveExpectationsRequest = request("/mockserver/verify")
            .withMethod("PUT")
            .withBody(
                verificationSerializer.serialize(
                    new Verification()
                        .withRequest(request("two"))
                )
            );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(expectationRetrieveExpectationsRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(406));
        assertThat(responseWriter.response.getBodyAsString(), is("Request not found at least once, expected:<{" + NEW_LINE +
            "  \"path\" : \"two\"" + NEW_LINE +
            "}> but was:<{" + NEW_LINE +
            "  \"path\" : \"one\"" + NEW_LINE +
            "}>"));
    }

    @Test
    public void shouldHandleVerifySequenceRequest() {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );
        HttpRequest expectationRetrieveExpectationsRequest = request("/mockserver/verifySequence")
            .withMethod("PUT")
            .withBody(
                verificationSequenceSerializer.serialize(
                    new VerificationSequence()
                        .withRequests(
                            request("one"),
                            request("three")
                        )
                )
            );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(expectationRetrieveExpectationsRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(202));
        assertThat(responseWriter.response.getBodyAsString(), is(""));
    }

    @Test
    public void shouldHandleVerifySequenceFailureRequest() {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );
        HttpRequest expectationRetrieveExpectationsRequest = request("/mockserver/verifySequence")
            .withMethod("PUT")
            .withBody(
                verificationSequenceSerializer.serialize(
                    new VerificationSequence()
                        .withRequests(
                            request("three"),
                            request("one")
                        )
                )
            );
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(expectationRetrieveExpectationsRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(406));
        assertThat(responseWriter.response.getBodyAsString(), is("Request sequence not found, expected:<[ {" + NEW_LINE +
            "  \"path\" : \"three\"" + NEW_LINE +
            "}, {" + NEW_LINE +
            "  \"path\" : \"one\"" + NEW_LINE +
            "} ]> but was:<[ {" + NEW_LINE +
            "  \"path\" : \"one\"" + NEW_LINE +
            "}, {" + NEW_LINE +
            "  \"path\" : \"two\"" + NEW_LINE +
            "}, {" + NEW_LINE +
            "  \"path\" : \"three\"" + NEW_LINE +
            "} ]>"));
    }

    @Test
    public void shouldAddExceptionsWithNullFields() {
        // given - some existing expectations
        Expectation expectationOne = new Expectation(null).thenRespond(response("response_one"));
        Expectation expectationTwo = new Expectation(request("request_two")).thenRespond((HttpResponse) null);

        // when
        List<Expectation> actualExpectationsOne = httpState.add(expectationOne);
        List<Expectation> actualExpectationsTwo = httpState.add(expectationTwo);

        // then - correct expectations exist
        assertThat(actualExpectationsOne.size(), is(1));
        assertThat(actualExpectationsOne.get(0), is(expectationOne));
        assertThat(actualExpectationsTwo.size(), is(1));
        assertThat(actualExpectationsTwo.get(0), is(expectationTwo));
        assertThat(httpState.firstMatchingExpectation(null), is(expectationOne));
        assertThat(httpState.firstMatchingExpectation(request("request_two")), is(expectationOne));
    }

    @Test
    public void shouldAddExceptionViaOpenApiClasspath() {
        // when
        List<Expectation> actualExpectations = httpState.add(openAPIExpectation("org/mockserver/openapi/openapi_petstore_example.json"));

        // then
        shouldBuildPetStoreExpectations("org/mockserver/openapi/openapi_petstore_example.json", actualExpectations);
    }

    @Test
    public void shouldAddExceptionViaOpenApiUrl() {
        // given
        URL schemaUrl = FilePath.getURL("org/mockserver/openapi/openapi_petstore_example.json");

        // when
        List<Expectation> actualExpectations = httpState.add(openAPIExpectation(String.valueOf(schemaUrl)));

        // then
        shouldBuildPetStoreExpectations(String.valueOf(schemaUrl), actualExpectations);
    }

    @Test
    public void shouldAddExceptionViaOpenApiSpec() {
        // given
        String schema = FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.json");

        // when
        List<Expectation> actualExpectations = httpState.add(openAPIExpectation(schema));

        // then
        shouldBuildPetStoreExpectations(schema, actualExpectations);
    }

    private void shouldBuildPetStoreExpectations(String specUrlOrPayload, List<Expectation> actualExpectations) {
        assertThat(actualExpectations.size(), is(4));
        assertThat(actualExpectations.get(0), is(
            when(specUrlOrPayload, "listPets")
                .thenRespond(
                    response()
                        .withStatusCode(200)
                        .withHeader("x-next", "some_string_value")
                        .withHeader("content-type", "application/json")
                        .withBody(json("[ {" + NEW_LINE +
                            "  \"id\" : 0," + NEW_LINE +
                            "  \"name\" : \"some_string_value\"," + NEW_LINE +
                            "  \"tag\" : \"some_string_value\"" + NEW_LINE +
                            "} ]"))
                )
        ));
        assertThat(actualExpectations.get(1), is(
            when(specUrlOrPayload, "createPets")
                .thenRespond(
                    response()
                        .withStatusCode(201)
                )
        ));
        assertThat(actualExpectations.get(2), is(
            when(specUrlOrPayload, "showPetById")
                .thenRespond(
                    response()
                        .withStatusCode(200)
                        .withHeader("content-type", "application/json")
                        .withBody(json("{" + NEW_LINE +
                            "  \"id\" : 0," + NEW_LINE +
                            "  \"name\" : \"some_string_value\"," + NEW_LINE +
                            "  \"tag\" : \"some_string_value\"" + NEW_LINE +
                            "}"))
                )
        ));
        assertThat(actualExpectations.get(3), is(
            when(specUrlOrPayload, "somePath")
                .thenRespond(
                    response()
                        .withStatusCode(200)
                        .withHeader("content-type", "application/json")
                        .withBody(json("{" + NEW_LINE +
                            "  \"id\" : 0," + NEW_LINE +
                            "  \"name\" : \"some_string_value\"," + NEW_LINE +
                            "  \"tag\" : \"some_string_value\"" + NEW_LINE +
                            "}"))
                )
        ));
    }

    @Test
    public void shouldAddExceptionViaOpenApiClasspathWithSpecificResponses() {
        // when
        List<Expectation> actualExpectations = httpState.add(openAPIExpectation("org/mockserver/openapi/openapi_petstore_example.json", ImmutableMap.of(
            "listPets", "500",
            "createPets", "default",
            "showPetById", "200"
        )));

        // then
        shouldBuildPetStoreExpectationsWithSpecificResponses("org/mockserver/openapi/openapi_petstore_example.json", actualExpectations);
    }

    @Test
    public void shouldAddExceptionViaOpenApiUrlWithSpecificResponses() {
        // given
        URL schemaUrl = FilePath.getURL("org/mockserver/openapi/openapi_petstore_example.json");

        // when
        List<Expectation> actualExpectations = httpState.add(openAPIExpectation(String.valueOf(schemaUrl), ImmutableMap.of(
            "listPets", "500",
            "createPets", "default",
            "showPetById", "200"
        )));

        // then
        shouldBuildPetStoreExpectationsWithSpecificResponses(String.valueOf(schemaUrl), actualExpectations);
    }

    @Test
    public void shouldAddExceptionViaOpenApiSpecWithSpecificResponses() {
        // given
        String schema = FileReader.readFileFromClassPathOrPath("org/mockserver/openapi/openapi_petstore_example.json");

        // when
        List<Expectation> actualExpectations = httpState.add(openAPIExpectation(schema, ImmutableMap.of(
            "listPets", "500",
            "createPets", "default",
            "showPetById", "200"
        )));

        // then
        shouldBuildPetStoreExpectationsWithSpecificResponses(schema, actualExpectations);
    }

    private void shouldBuildPetStoreExpectationsWithSpecificResponses(String specUrlOrPayload, List<Expectation> actualExpectations) {
        assertThat(actualExpectations.size(), is(3));
        assertThat(actualExpectations.get(0), is(
            when(specUrlOrPayload, "listPets")
                .thenRespond(
                    response()
                        .withStatusCode(500)
                        .withHeader("content-type", "application/json")
                        .withBody(json("{" + NEW_LINE +
                            "  \"code\" : 0," + NEW_LINE +
                            "  \"message\" : \"some_string_value\"" + NEW_LINE +
                            "}"))
                )
        ));
        assertThat(actualExpectations.get(1), is(
            when(specUrlOrPayload, "createPets")
                .thenRespond(
                    response()
                        .withHeader("content-type", "application/json")
                        .withBody(json("{" + NEW_LINE +
                            "  \"code\" : 0," + NEW_LINE +
                            "  \"message\" : \"some_string_value\"" + NEW_LINE +
                            "}"))
                )
        ));
        assertThat(actualExpectations.get(2), is(
            when(specUrlOrPayload, "showPetById")
                .thenRespond(
                    response()
                        .withStatusCode(200)
                        .withHeader("content-type", "application/json")
                        .withBody(json("{" + NEW_LINE +
                            "  \"id\" : 0," + NEW_LINE +
                            "  \"name\" : \"some_string_value\"," + NEW_LINE +
                            "  \"tag\" : \"some_string_value\"" + NEW_LINE +
                            "}"))
                )
        ));
    }

    @Test
    public void shouldClearLogsAndExpectations() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.add(
                new Expectation(request("request_one"))
                    .withId("one")
                    .thenRespond(response("response_one"))
            );
            // given - some log entries
            httpState.log(
                new LogEntry()
                    .setType(TRACE)
                    .setHttpRequest(request("request_four"))
                    .setExpectation(new Expectation(request("request_four")).thenRespond(response("response_four")))
                    .setMessageFormat("some random{}message")
                    .setArguments("argument_one")
            );

            // when
            httpState
                .clear(
                    request()
                        .withQueryStringParameter("type", "all")
                );

            // then - retrieves correct state
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "logs")
                    ),
                is(response().withBody("" +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - cleared logs that match:" + NEW_LINE +
                        "" + NEW_LINE +
                        "  {}" + NEW_LINE +
                        "" + NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - removed expectation:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"httpRequest\" : {" + NEW_LINE +
                        "      \"path\" : \"request_one\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"httpResponse\" : {" + NEW_LINE +
                        "      \"statusCode\" : 200," + NEW_LINE +
                        "      \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "      \"body\" : \"response_one\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"id\" : \"one\"," + NEW_LINE +
                        "    \"priority\" : 0," + NEW_LINE +
                        "    \"timeToLive\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"times\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " with id:" + NEW_LINE +
                        NEW_LINE +
                        "  one" + NEW_LINE +
                        NEW_LINE,
                    MediaType.PLAIN_TEXT_UTF_8).withStatusCode(200))
            );
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "active_expectations")
                    ),
                is(response().withBody("[]", MediaType.JSON_UTF_8).withStatusCode(200))
            );
            assertThat(httpState.firstMatchingExpectation(request("request_one")), nullValue());
    }

    @Test
    public void shouldClearLogsAndExpectationsWithRequestMatcher() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.add(
                new Expectation(request("request_one"))
                    .withId("key_one")
                    .thenRespond(response("response_one"))
            );
            httpState.add(
                new Expectation(request("request_four"))
                    .withId("key_four")
                    .thenRespond(response("response_four"))
            );
            // given - some log entries
            httpState.log(
                new LogEntry()
                    .setType(TRACE)
                    .setLogLevel(INFO)
                    .setHttpRequest(request("request_one"))
                    .setMessageFormat("some random{}message")
                    .setArguments("argument_one")
            );
            httpState.log(
                new LogEntry()
                    .setType(TRACE)
                    .setLogLevel(INFO)
                    .setHttpRequest(request("request_four"))
                    .setMessageFormat("some random{}message")
                    .setArguments("argument_four")
            );

            // when
            httpState
                .clear(
                    request()
                        .withQueryStringParameter("type", "all")
                        .withBody(requestDefinitionSerializer.serialize(request("request_four")))
                );

            // then - retrieves correct state
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "logs")
                    ),
                is(response().withBody("" +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - creating expectation:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"httpRequest\" : {" + NEW_LINE +
                        "      \"path\" : \"request_one\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"httpResponse\" : {" + NEW_LINE +
                        "      \"statusCode\" : 200," + NEW_LINE +
                        "      \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "      \"body\" : \"response_one\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"id\" : \"key_one\"," + NEW_LINE +
                        "    \"priority\" : 0," + NEW_LINE +
                        "    \"timeToLive\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"times\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " with id:" + NEW_LINE +
                        NEW_LINE +
                        "  key_one" + NEW_LINE +
                        NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - some random" + NEW_LINE +
                        NEW_LINE +
                        "  argument_one" + NEW_LINE +
                        NEW_LINE +
                        " message" + NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - cleared logs that match:" + NEW_LINE +
                        "" + NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_four\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        "" + NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - removed expectation:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"httpRequest\" : {" + NEW_LINE +
                        "      \"path\" : \"request_four\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"httpResponse\" : {" + NEW_LINE +
                        "      \"statusCode\" : 200," + NEW_LINE +
                        "      \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "      \"body\" : \"response_four\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"id\" : \"key_four\"," + NEW_LINE +
                        "    \"priority\" : 0," + NEW_LINE +
                        "    \"timeToLive\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"times\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " with id:" + NEW_LINE +
                        NEW_LINE +
                        "  key_four" + NEW_LINE +
                        NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - cleared expectations that match:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_four\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE,
                    MediaType.PLAIN_TEXT_UTF_8).withStatusCode(200))
            );
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "active_expectations")
                    ),
                is(response().withBody("" +
                        "[ {" + NEW_LINE +
                        "  \"httpRequest\" : {" + NEW_LINE +
                        "    \"path\" : \"request_one\"" + NEW_LINE +
                        "  }," + NEW_LINE +
                        "  \"httpResponse\" : {" + NEW_LINE +
                        "    \"statusCode\" : 200," + NEW_LINE +
                        "    \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "    \"body\" : \"response_one\"" + NEW_LINE +
                        "  }," + NEW_LINE +
                        "  \"id\" : \"key_one\"," + NEW_LINE +
                        "  \"priority\" : 0," + NEW_LINE +
                        "  \"timeToLive\" : {" + NEW_LINE +
                        "    \"unlimited\" : true" + NEW_LINE +
                        "  }," + NEW_LINE +
                        "  \"times\" : {" + NEW_LINE +
                        "    \"unlimited\" : true" + NEW_LINE +
                        "  }" + NEW_LINE +
                        "} ]",
                    MediaType.JSON_UTF_8).withStatusCode(200))
            );
            assertThat(
                httpState.firstMatchingExpectation(request("request_one")),
                is(
                    new Expectation(request("request_one"))
                        .thenRespond(response("response_one"))
                )
            );
            assertThat(
                httpState.firstMatchingExpectation(request("request_four")),
                nullValue()
            );
    }

    @Test
    public void shouldClearLogsOnly() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.add(
                new Expectation(request("request_one"))
                    .withId("key_one")
                    .thenRespond(response("response_one"))
            );
            // given - some log entries
            httpState.log(
                new LogEntry()
                    .setType(TRACE)
                    .setHttpRequest(request("request_four"))
                    .setExpectation(new Expectation(request("request_four")).withId("key_four").thenRespond(response("response_four")))
                    .setMessageFormat("some random{}message")
                    .setArguments("argument_one")
            );

            // when
            httpState
                .clear(
                    request()
                        .withQueryStringParameter("type", "log")
                );

            // then - retrieves correct state
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "logs")
                    ),
                is(response().withBody("" +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - cleared logs that match:" + NEW_LINE +
                        NEW_LINE +
                        "  {}" + NEW_LINE +
                        NEW_LINE,
                    MediaType.PLAIN_TEXT_UTF_8).withStatusCode(200))
            );
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "active_expectations")
                    ),
                is(response().withBody("" +
                        "[ {" + NEW_LINE +
                        "  \"httpRequest\" : {" + NEW_LINE +
                        "    \"path\" : \"request_one\"" + NEW_LINE +
                        "  }," + NEW_LINE +
                        "  \"httpResponse\" : {" + NEW_LINE +
                        "    \"statusCode\" : 200," + NEW_LINE +
                        "    \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "    \"body\" : \"response_one\"" + NEW_LINE +
                        "  }," + NEW_LINE +
                        "  \"id\" : \"key_one\"," + NEW_LINE +
                        "  \"priority\" : 0," + NEW_LINE +
                        "  \"timeToLive\" : {" + NEW_LINE +
                        "    \"unlimited\" : true" + NEW_LINE +
                        "  }," + NEW_LINE +
                        "  \"times\" : {" + NEW_LINE +
                        "    \"unlimited\" : true" + NEW_LINE +
                        "  }" + NEW_LINE +
                        "} ]",
                    MediaType.JSON_UTF_8).withStatusCode(200))
            );
            assertThat(
                httpState.firstMatchingExpectation(request("request_one")),
                is(
                    new Expectation(request("request_one"))
                        .thenRespond(response("response_one"))
                )
            );
    }

    @Test
    public void shouldClearExpectationsOnly() {
        // given
        httpState.add(new Expectation(request("request_one")).thenRespond(response("response_one")));
        httpState.add(new Expectation(request("request_two")).thenRespond(response("response_two")));
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setHttpResponse(response("response_one"))
                .setType(EXPECTATION_RESPONSE)
        );
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_two"))
                .setHttpError(error().withResponseBytes("response_two".getBytes(UTF_8)))
                .setType(EXPECTATION_RESPONSE)
        );

        // when
        httpState
            .clear(
                request()
                    .withQueryStringParameter("type", "expectations")
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then - correct log entries not removed
        HttpResponse retrieve = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", REQUEST_RESPONSES.name())
                    .withQueryStringParameter("format", LOG_ENTRIES.name())
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );
        assertThat(
            retrieve.getBodyAsString(),
            is(new LogEntrySerializer(new MockServerLogger()).serialize(Collections.singletonList(
                new LogEntry()
                    .setHttpRequest(request("request_one"))
                    .setHttpResponse(response("response_one"))
                    .setType(EXPECTATION_RESPONSE)
            )))
        );
        // then - correct expectations removed
        assertThat(httpState.firstMatchingExpectation(request("request_one")), nullValue());
        assertThat(httpState.firstMatchingExpectation(request("request_two")), is(new Expectation(request("request_two")).thenRespond(response("response_two"))));
    }

    @Test
    public void shouldClearExpectationsById() {
        // given
        httpState.add(new Expectation(request("request.*")).withId("one").thenRespond(response("response_one")));
        httpState.add(new Expectation(request("request.*")).withId("two").thenRespond(response("response_two")));
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setHttpResponse(response("response_one"))
                .setType(EXPECTATION_RESPONSE)
        );
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_two"))
                .setHttpError(error().withResponseBytes("response_two".getBytes(UTF_8)))
                .setType(EXPECTATION_RESPONSE)
        );

        // then
        assertThat(httpState.allMatchingExpectation(request()), containsInAnyOrder(
            new Expectation(request("request.*")).thenRespond(response("response_one")),
            new Expectation(request("request.*")).withId("two").thenRespond(response("response_two"))
        ));

        // when
        httpState
            .clear(
                request()
                    .withQueryStringParameter("type", "expectations")
                    .withBody(expectationIdSerializer.serialize(expectationId("one")))
            );

        // then - correct log entries not removed
        HttpResponse retrieve = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", REQUEST_RESPONSES.name())
                    .withQueryStringParameter("format", LOG_ENTRIES.name())
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );
        assertThat(
            retrieve.getBodyAsString(),
            is(new LogEntrySerializer(new MockServerLogger()).serialize(Collections.singletonList(
                new LogEntry()
                    .setHttpRequest(request("request_one"))
                    .setHttpResponse(response("response_one"))
                    .setType(EXPECTATION_RESPONSE)
            )))
        );
        // then - correct expectations removed
        assertThat(httpState.allMatchingExpectation(request()), containsInAnyOrder(
            new Expectation(request("request.*")).withId("two").thenRespond(response("response_two"))
        ));
    }

    @Test
    public void shouldClearAllExpectationsByRequestMatcher() {
        // given
        httpState.add(new Expectation(request("request.*")).withId("one").thenRespond(response("response_one")));
        httpState.add(new Expectation(request("request.*")).withId("two").thenRespond(response("response_two")));
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setHttpResponse(response("response_one"))
                .setType(EXPECTATION_RESPONSE)
        );
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_two"))
                .setHttpError(error().withResponseBytes("response_two".getBytes(UTF_8)))
                .setType(EXPECTATION_RESPONSE)
        );

        // then
        assertThat(httpState.allMatchingExpectation(request()), containsInAnyOrder(
            new Expectation(request("request.*")).thenRespond(response("response_one")),
            new Expectation(request("request.*")).withId("two").thenRespond(response("response_two"))
        ));

        // when
        httpState
            .clear(
                request()
                    .withQueryStringParameter("type", "expectations")
                    .withBody(requestDefinitionSerializer.serialize(request("request.*")))
            );

        // then - correct log entries not removed
        HttpResponse retrieve = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", REQUEST_RESPONSES.name())
                    .withQueryStringParameter("format", LOG_ENTRIES.name())
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );
        assertThat(
            retrieve.getBodyAsString(),
            is(new LogEntrySerializer(new MockServerLogger()).serialize(Collections.singletonList(
                new LogEntry()
                    .setHttpRequest(request("request_one"))
                    .setHttpResponse(response("response_one"))
                    .setType(EXPECTATION_RESPONSE)
            )))
        );
        // then - correct expectations removed
        assertThat(httpState.allMatchingExpectation(request()), emptyIterable());
    }

    @Test
    public void shouldThrowExceptionForInvalidClearType() {
        // given
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage(containsString("\"invalid\" is not a valid value for \"type\" parameter, only the following values are supported [log, expectations, all]"));

        // when
        httpState.clear(request().withQueryStringParameter("type", "invalid"));
    }

    @Test
    public void shouldRetrieveRecordedRequestsAsJson() {
        // given
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setType(RECEIVED_REQUEST)
        );
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_two"))
                .setType(RECEIVED_REQUEST)
        );
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setType(RECEIVED_REQUEST)
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then
        assertThat(response,
            is(response().withBody(requestDefinitionSerializer.serialize(Arrays.asList(
                request("request_one"),
                request("request_one")
            )), MediaType.JSON_UTF_8).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveRecordedRequestsAsLogEntries() {
        // given
        httpState
            .log(
                new LogEntry()
                    .setType(RECEIVED_REQUEST)
                    .setLogLevel(Level.INFO)
                    .setHttpRequest(request("request_one"))
                    .setMessageFormat(RECEIVED_REQUEST_MESSAGE_FORMAT)
                    .setArguments(request("request_one"))
            );
        httpState
            .log(
                new LogEntry()
                    .setType(RECEIVED_REQUEST)
                    .setLogLevel(Level.INFO)
                    .setHttpRequest(request("request_two"))
                    .setMessageFormat(RECEIVED_REQUEST_MESSAGE_FORMAT)
                    .setArguments(request("request_two"))
            );
        httpState
            .log(
                new LogEntry()
                    .setType(RECEIVED_REQUEST)
                    .setLogLevel(Level.INFO)
                    .setHttpRequest(request("request_one"))
                    .setMessageFormat(RECEIVED_REQUEST_MESSAGE_FORMAT)
                    .setArguments(request("request_one"))
            );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("format", "log_entries")
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then
        assertThat(response,
            is(response().withBody(new LogEntrySerializer(new MockServerLogger()).serialize(Arrays.asList(
                new LogEntry()
                    .setType(RECEIVED_REQUEST)
                    .setLogLevel(Level.INFO)
                    .setHttpRequest(request("request_one"))
                    .setMessageFormat(RECEIVED_REQUEST_MESSAGE_FORMAT)
                    .setArguments(request("request_one")),
                new LogEntry()
                    .setType(RECEIVED_REQUEST)
                    .setLogLevel(Level.INFO)
                    .setHttpRequest(request("request_one"))
                    .setMessageFormat(RECEIVED_REQUEST_MESSAGE_FORMAT)
                    .setArguments(request("request_one"))
            )), MediaType.JSON_UTF_8).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveRecordedRequestsAsJava() {
        // given
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setType(RECEIVED_REQUEST)
        );
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_two"))
                .setType(RECEIVED_REQUEST)
        );
        httpState.log(
            new LogEntry()
                .setHttpRequest(request("request_one"))
                .setType(RECEIVED_REQUEST)
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("format", "java")
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then
        assertThat(response,
            is(response().withBody(requestDefinitionSerializer.serialize(Arrays.asList(
                request("request_one"),
                request("request_one")
            )), MediaType.create("application", "java").withCharset(UTF_8)).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveRecordedRequestResponsesAsJson() {
        // given
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(NO_MATCH_RESPONSE)
                .setHttpRequest(request("request_one"))
                .setExpectation(new Expectation(request("request_one")).thenRespond(response("response_two")))
                .setMessageFormat("no expectation for:{}returning response:{}")
                .setArguments(request("request_one"), notFoundResponse())
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(EXPECTATION_RESPONSE)
                .setHttpRequest(request("request_two"))
                .setHttpResponse(response("response_two"))
                .setMessageFormat("returning error:{}for request:{}for action:{}")
                .setArguments(request("request_two"), response("response_two"), response("response_two"))
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "request_responses")
                    .withBody(requestDefinitionSerializer.serialize(request("request_.*")))
            );

        // then
        assertThat(response,
            is(response().withBody("[ {" + NEW_LINE +
                "  \"httpRequest\" : {" + NEW_LINE +
                "    \"path\" : \"request_one\"" + NEW_LINE +
                "  }," + NEW_LINE +
                "  \"timestamp\" : \"" + LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + "\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"httpRequest\" : {" + NEW_LINE +
                "    \"path\" : \"request_two\"" + NEW_LINE +
                "  }," + NEW_LINE +
                "  \"httpResponse\" : {" + NEW_LINE +
                "    \"statusCode\" : 200," + NEW_LINE +
                "    \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                "    \"body\" : \"response_two\"" + NEW_LINE +
                "  }," + NEW_LINE +
                "  \"timestamp\" : \"" + LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + "\"" + NEW_LINE +
                "} ]", MediaType.JSON_UTF_8).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveRecordedRequestsResponsesAsLogEntries() {
        // given
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(NO_MATCH_RESPONSE)
                .setHttpRequest(request("request_one"))
                .setExpectation(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_two")))
                .setMessageFormat("no expectation for:{}returning response:{}")
                .setArguments(request("request_one"), notFoundResponse())
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(EXPECTATION_RESPONSE)
                .setHttpRequest(request("request_two"))
                .setHttpResponse(response("response_two"))
                .setMessageFormat("returning error:{}for request:{}for action:{}")
                .setArguments(request("request_two"), response("response_two"), response("response_two"))
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "request_responses")
                    .withQueryStringParameter("format", "log_entries")
                    .withBody(requestDefinitionSerializer.serialize(request("request_.*")))
            );

        // then
        assertThat(response.getStatusCode(), is(200));
        String body = response.getBodyAsString();
        assertThat(body, containsString("\"type\" : \"NO_MATCH_RESPONSE\""));
        assertThat(body, containsString("\"type\" : \"EXPECTATION_RESPONSE\""));
        assertThat(body, containsString("\"epochTime\" :"));
        assertThat(body, containsString("\"messageFormat\" : \"no expectation for:{}returning response:{}\""));
        assertThat(body, containsString("\"messageFormat\" : \"returning error:{}for request:{}for action:{}\""));
        assertThat(body, containsString("\"arguments\" :"));
        assertThat(body, containsString("\"logLevel\" : \"INFO\""));
        assertThat(body, containsString("\"path\" : \"request_one\""));
        assertThat(body, containsString("\"path\" : \"request_two\""));
    }

    @Test
    public void shouldRetrieveRecordedRequestsResponsesAsJava() {
        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("format", "java")
                    .withQueryStringParameter("type", "request_responses")
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then
        assertThat(response.getBodyAsString(), is("JAVA not supported for REQUEST_RESPONSES"));
    }

    @Test
    public void shouldRetrieveRecordedExpectationsAsJson() {
        // given
        httpState.log(
            new LogEntry()
                .setType(FORWARDED_REQUEST)
                .setLogLevel(Level.INFO)
                .setHttpRequest(request("request_one"))
                .setHttpResponse(response("response_one"))
                .setExpectation(new Expectation(request("request_one"), Times.once(), TimeToLive.unlimited(), 0).withId("key_one").thenRespond(response("response_one")))
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(FORWARDED_REQUEST)
                .setHttpRequest(request("request_two"))
                .setHttpResponse(response("response_two"))
                .setExpectation(new Expectation(request("request_two"), Times.once(), TimeToLive.unlimited(), 0).withId("key_two").thenRespond(response("response_two")))
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "recorded_expectations")
                    .withQueryStringParameter("format", "json")
            );

        // then
        assertThat(response,
            is(response().withBody(expectationSerializerWithDefaultFields.serialize(Arrays.asList(
                new Expectation(request("request_one"), Times.once(), TimeToLive.unlimited(), 0).withId("key_one").thenRespond(response("response_one")),
                new Expectation(request("request_two"), Times.once(), TimeToLive.unlimited(), 0).withId("key_two").thenRespond(response("response_two"))
            )), MediaType.JSON_UTF_8).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveRecordedExpectationsAsJava() {
        // given
        httpState.log(
            new LogEntry()
                .setType(FORWARDED_REQUEST)
                .setLogLevel(Level.INFO)
                .setHttpRequest(request("request_one"))
                .setHttpResponse(response("response_one"))
                .setExpectation(request("request_one"), response("response_one"))
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(FORWARDED_REQUEST)
                .setHttpRequest(request("request_two"))
                .setHttpResponse(response("response_two"))
                .setExpectation(request("request_two"), response("response_two"))
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "recorded_expectations")
                    .withQueryStringParameter("format", "java")
            );

        // then
        assertThat(response,
            is(response().withBody(expectationToJavaSerializer.serialize(Arrays.asList(
                new Expectation(request("request_one"), Times.once(), TimeToLive.unlimited(), 0).thenRespond(response("response_one")),
                new Expectation(request("request_two"), Times.once(), TimeToLive.unlimited(), 0).thenRespond(response("response_two"))
            )), MediaType.create("application", "java").withCharset(UTF_8)).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveRecordedExpectationsAsJavaWithRequestMatcher() {
        // given
        httpState.log(
            new LogEntry()
                .setType(FORWARDED_REQUEST)
                .setLogLevel(Level.INFO)
                .setHttpRequest(request("request_one"))
                .setHttpResponse(response("response_one"))
                .setExpectation(request("request_one"), response("response_one"))
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(FORWARDED_REQUEST)
                .setHttpRequest(request("request_two"))
                .setHttpResponse(response("response_two"))
                .setExpectation(request("request_two"), response("response_two"))
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "recorded_expectations")
                    .withQueryStringParameter("format", "java")
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then
        assertThat(response,
            is(response().withBody(expectationToJavaSerializer.serialize(Collections.singletonList(
                new Expectation(request("request_one"), Times.once(), TimeToLive.unlimited(), 0).thenRespond(response("response_one"))
            )), MediaType.create("application", "java").withCharset(UTF_8)).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveActiveExpectationsAsJson() {
        // given
        Expectation expectationOne = new Expectation(request("request_one")).thenRespond(response("response_one"));
        httpState.add(expectationOne);
        Expectation expectationTwo = new Expectation(request("request_two")).thenRespond(response("response_two"));
        httpState.add(expectationTwo);
        Expectation expectationThree = new Expectation(request("request_one")).thenRespond(response("request_three"));
        httpState.add(expectationThree);

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "active_expectations")
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then
        assertThat(response,
            is(response().withBody(expectationSerializer.serialize(Arrays.asList(
                expectationOne,
                expectationThree
            )), MediaType.JSON_UTF_8).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveActiveExpectationsAsJava() {
        // given
        Expectation expectationOne = new Expectation(request("request_one")).thenRespond(response("response_one"));
        httpState.add(expectationOne);
        Expectation expectationTwo = new Expectation(request("request_two")).thenRespond(response("response_two"));
        httpState.add(expectationTwo);
        Expectation expectationThree = new Expectation(request("request_one")).thenRespond(response("request_three"));
        httpState.add(expectationThree);

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "active_expectations")
                    .withQueryStringParameter("format", "java")
                    .withBody(requestDefinitionSerializer.serialize(request("request_one")))
            );

        // then
        assertThat(response,
            is(response().withBody(expectationToJavaSerializer.serialize(Arrays.asList(
                expectationOne,
                expectationThree
            )), MediaType.create("application", "java").withCharset(UTF_8)).withStatusCode(200))
        );
    }

    @Test
    public void shouldRetrieveLogEntries() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.log(
                new LogEntry()
                    .setLogLevel(INFO)
                    .setType(NO_MATCH_RESPONSE)
                    .setHttpRequest(request("request_one"))
                    .setExpectation(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_two")))
                    .setMessageFormat("no expectation for:{}returning response:{}")
                    .setArguments(request("request_one"), notFoundResponse())
            );
            httpState.log(
                new LogEntry()
                    .setLogLevel(INFO)
                    .setType(EXPECTATION_RESPONSE)
                    .setHttpRequest(request("request_two"))
                    .setHttpResponse(response("response_two"))
                    .setMessageFormat("returning error:{}for request:{}for action:{}")
                    .setArguments(request("request_two"), response("response_two"), response("response_two"))
            );
            httpState.log(
                new LogEntry()
                    .setLogLevel(INFO)
                    .setType(EXPECTATION_MATCHED)
                    .setHttpRequest(request("request_one"))
                    .setExpectation(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_two")))
                    .setMessageFormat("request:{}matched expectation:{}")
                    .setArguments(request("request_one"), new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_two")))
            );
            httpState.log(
                new LogEntry()
                    .setLogLevel(INFO)
                    .setType(EXPECTATION_MATCHED)
                    .setHttpRequest(request("request_two"))
                    .setExpectation(new Expectation(request("request_two")).withId("key_two").thenRespond(response("response_two")))
                    .setMessageFormat("request:{}matched expectation:{}")
                    .setArguments(request("request_two"), new Expectation(request("request_two")).withId("key_two").thenRespond(response("response_two")))
            );
            httpState.log(
                new LogEntry()
                    .setType(TRACE)
                    .setLogLevel(INFO)
                    .setHttpRequest(request("request_four"))
                    .setExpectation(new Expectation(request("request_four")).withId("key_four").thenRespond(response("response_four")))
                    .setMessageFormat("some random{}message")
                    .setArguments("argument_one")
            );

            // when
            HttpResponse response = httpState
                .retrieve(
                    request()
                        .withQueryStringParameter("type", "logs")
                );

            // then
            assertThat(response,
                is(response().withBody("" +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - no expectation for:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_one\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " returning response:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"statusCode\" : 404," + NEW_LINE +
                        "    \"reasonPhrase\" : \"Not Found\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - returning error:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_two\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " for request:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"statusCode\" : 200," + NEW_LINE +
                        "    \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "    \"body\" : \"response_two\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " for action:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"statusCode\" : 200," + NEW_LINE +
                        "    \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "    \"body\" : \"response_two\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - request:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_one\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " matched expectation:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"httpRequest\" : {" + NEW_LINE +
                        "      \"path\" : \"request_one\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"httpResponse\" : {" + NEW_LINE +
                        "      \"statusCode\" : 200," + NEW_LINE +
                        "      \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "      \"body\" : \"response_two\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"id\" : \"key_one\"," + NEW_LINE +
                        "    \"priority\" : 0," + NEW_LINE +
                        "    \"timeToLive\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"times\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - request:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_two\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " matched expectation:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"httpRequest\" : {" + NEW_LINE +
                        "      \"path\" : \"request_two\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"httpResponse\" : {" + NEW_LINE +
                        "      \"statusCode\" : 200," + NEW_LINE +
                        "      \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "      \"body\" : \"response_two\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"id\" : \"key_two\"," + NEW_LINE +
                        "    \"priority\" : 0," + NEW_LINE +
                        "    \"timeToLive\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"times\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - some random" + NEW_LINE +
                        NEW_LINE +
                        "  argument_one" + NEW_LINE +
                        NEW_LINE +
                        " message" + NEW_LINE,
                    MediaType.PLAIN_TEXT_UTF_8).withStatusCode(200))
            );
    }

    @Test
    public void shouldRetrieveLogEntriesWithRequestMatcher() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.log(
                new LogEntry()
                    .setLogLevel(INFO)
                    .setType(NO_MATCH_RESPONSE)
                    .setHttpRequest(request("request_one"))
                    .setExpectation(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_two")))
                    .setMessageFormat("no expectation for:{}returning response:{}")
                    .setArguments(request("request_one"), notFoundResponse())
            );
            httpState.log(
                new LogEntry()
                    .setLogLevel(INFO)
                    .setType(EXPECTATION_RESPONSE)
                    .setHttpRequest(request("request_two"))
                    .setHttpResponse(response("response_two"))
                    .setMessageFormat("returning error:{}for request:{}for action:{}")
                    .setArguments(request("request_two"), response("response_two"), response("response_two"))
            );
            httpState.log(
                new LogEntry()
                    .setType(EXPECTATION_MATCHED)
                    .setLogLevel(INFO)
                    .setHttpRequest(request("request_one"))
                    .setExpectation(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_two")))
                    .setMessageFormat("request:{}matched expectation:{}")
                    .setArguments(request("request_one"), new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_two")))
            );
            httpState.log(
                new LogEntry()
                    .setType(EXPECTATION_MATCHED)
                    .setLogLevel(INFO)
                    .setHttpRequest(request("request_two"))
                    .setExpectation(new Expectation(request("request_two")).withId("key_two").thenRespond(response("response_two")))
                    .setMessageFormat("request:{}matched expectation:{}")
                    .setArguments(request("request_two"), new Expectation(request("request_two")).withId("key_two").thenRespond(response("response_two")))
            );
            httpState.log(
                new LogEntry()
                    .setType(TRACE)
                    .setHttpRequest(request("request_four"))
                    .setExpectation(new Expectation(request("request_four")).withId("key_four").thenRespond(response("response_four")))
                    .setMessageFormat("some random{}message")
                    .setArguments("argument_one")
            );

            // when
            HttpResponse response = httpState
                .retrieve(
                    request()
                        .withQueryStringParameter("type", "logs")
                        .withBody(requestDefinitionSerializer.serialize(request("request_one")))
                );

            // then
            assertThat(response,
                is(response().withBody("" +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - no expectation for:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_one\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " returning response:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"statusCode\" : 404," + NEW_LINE +
                        "    \"reasonPhrase\" : \"Not Found\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        "------------------------------------" + NEW_LINE +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - request:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"path\" : \"request_one\"" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE +
                        " matched expectation:" + NEW_LINE +
                        NEW_LINE +
                        "  {" + NEW_LINE +
                        "    \"httpRequest\" : {" + NEW_LINE +
                        "      \"path\" : \"request_one\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"httpResponse\" : {" + NEW_LINE +
                        "      \"statusCode\" : 200," + NEW_LINE +
                        "      \"reasonPhrase\" : \"OK\"," + NEW_LINE +
                        "      \"body\" : \"response_two\"" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"id\" : \"key_one\"," + NEW_LINE +
                        "    \"priority\" : 0," + NEW_LINE +
                        "    \"timeToLive\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }," + NEW_LINE +
                        "    \"times\" : {" + NEW_LINE +
                        "      \"unlimited\" : true" + NEW_LINE +
                        "    }" + NEW_LINE +
                        "  }" + NEW_LINE +
                        NEW_LINE,
                    MediaType.PLAIN_TEXT_UTF_8).withStatusCode(200))
            );
    }

    @Test
    public void shouldRetrieveMetrics() {
        // given
        Scheduler scheduler = mock(Scheduler.class);
        HttpState metricsEnabledState = new HttpState(configuration().metricsEnabled(true), new MockServerLogger(), scheduler);

        // when
        HttpResponse response = metricsEnabledState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "metrics")
            );

        // then
        assertThat(response.getStatusCode(), is(200));
        assertThat(response.getBodyAsString(), containsString("REQUESTS_RECEIVED_COUNT"));
        assertThat(response.getBodyAsString(), containsString("EXPECTATIONS_NOT_MATCHED_COUNT"));
    }

    @Test
    public void shouldRetrieveEmptyMetricsWhenDisabled() {
        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "metrics")
            );

        // then
        assertThat(response.getStatusCode(), is(200));
        assertThat(response.getBodyAsString(), is("{}"));
    }

    @Test
    public void shouldThrowExceptionForInvalidRetrieveType() {
        try {
            // when
            httpState.retrieve(request().withQueryStringParameter("type", "invalid"));
            fail("expected exception to be thrown");
        } catch (Throwable throwable) {
            // then
            assertThat(throwable, instanceOf(IllegalArgumentException.class));
            assertThat(throwable.getMessage(), is("\"invalid\" is not a valid value for \"type\" parameter, only the following values are supported [logs, requests, request_responses, recorded_expectations, active_expectations, metrics]"));
        }
    }

    @Test
    public void shouldThrowExceptionForInvalidRetrieveFormat() {
        try {
            // when
            httpState.retrieve(request().withQueryStringParameter("format", "invalid"));
            fail("expected exception to be thrown");
        } catch (Throwable throwable) {
            // then
            assertThat(throwable, instanceOf(IllegalArgumentException.class));
            assertThat(throwable.getMessage(), is("\"invalid\" is not a valid value for \"format\" parameter, only the following values are supported [java, json, log_entries, har, openapi, postman, bruno, curl]"));
        }
    }

    @Test
    public void shouldReset() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.add(
                new Expectation(request("request_one"))
                    .thenRespond(response("response_one"))
            );
            // given - some log entries
            httpState.log(
                new LogEntry()
                    .setType(TRACE)
                    .setHttpRequest(request("request_four"))
                    .setExpectation(new Expectation(request("request_four")).thenRespond(response("response_four")))
                    .setMessageFormat("some random{}message")
                    .setArguments("argument_one")
            );

            // when
            httpState.reset();

            // then - retrieves correct state
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "logs")
                    ),
                is(response().withBody("" +
                        LOG_DATE_FORMAT.format(new Date(EpochService.currentTimeMillis())) + " - resetting all expectations and request logs" + NEW_LINE,
                    MediaType.PLAIN_TEXT_UTF_8).withStatusCode(200))
            );
            assertThat(
                httpState
                    .retrieve(
                        request()
                            .withQueryStringParameter("type", "active_expectations")
                    ),
                is(response().withBody("[]", MediaType.JSON_UTF_8).withStatusCode(200))
            );
            assertThat(httpState.firstMatchingExpectation(request("request_one")), nullValue());
    }

    @Test
    public void shouldVerifyWithFuture() throws Exception {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );

        // when
        String result = httpState.verify(
            new Verification()
                .withRequest(request("two"))
        ).get(5, SECONDS);

        // then
        assertThat(result, is(""));
    }

    @Test
    public void shouldVerifyWithCallback() throws Exception {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );
        CompletableFuture<String> verificationResult = new CompletableFuture<>();

        // when
        httpState.verify(
            new Verification()
                .withRequest(request("two")),
            verificationResult::complete
        );

        // then
        assertThat(verificationResult.get(5, SECONDS), is(""));
    }

    @Test
    public void shouldVerifyFailureWithCallback() throws Exception {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        CompletableFuture<String> verificationResult = new CompletableFuture<>();

        // when
        httpState.verify(
            new Verification()
                .withRequest(request("two")),
            verificationResult::complete
        );

        // then
        assertThat(verificationResult.get(5, SECONDS), is("Request not found at least once, expected:<{" + NEW_LINE +
            "  \"path\" : \"two\"" + NEW_LINE +
            "}> but was:<{" + NEW_LINE +
            "  \"path\" : \"one\"" + NEW_LINE +
            "}>"));
    }

    @Test
    public void shouldVerifySequenceWithFuture() throws Exception {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );

        // when
        String result = httpState.verify(
            new VerificationSequence()
                .withRequests(
                    request("one"),
                    request("three")
                )
        ).get(5, SECONDS);

        // then
        assertThat(result, is(""));
    }

    @Test
    public void shouldVerifySequenceWithCallback() throws Exception {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );
        CompletableFuture<String> verificationResult = new CompletableFuture<>();

        // when
        httpState.verify(
            new VerificationSequence()
                .withRequests(
                    request("one"),
                    request("three")
                ),
            verificationResult::complete
        );

        // then
        assertThat(verificationResult.get(5, SECONDS), is(""));
    }

    @Test
    public void shouldVerifySequenceFailureWithCallback() throws Exception {
        // given
        MockServerEventLog mockServerEventLog = httpState.getMockServerLog();
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("one"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("two"))
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(request("three"))
                .setType(RECEIVED_REQUEST)
        );
        CompletableFuture<String> verificationResult = new CompletableFuture<>();

        // when
        httpState.verify(
            new VerificationSequence()
                .withRequests(
                    request("three"),
                    request("one")
                ),
            verificationResult::complete
        );

        // then
        assertThat(verificationResult.get(5, SECONDS), is("Request sequence not found, expected:<[ {" + NEW_LINE +
            "  \"path\" : \"three\"" + NEW_LINE +
            "}, {" + NEW_LINE +
            "  \"path\" : \"one\"" + NEW_LINE +
            "} ]> but was:<[ {" + NEW_LINE +
            "  \"path\" : \"one\"" + NEW_LINE +
            "}, {" + NEW_LINE +
            "  \"path\" : \"two\"" + NEW_LINE +
            "}, {" + NEW_LINE +
            "  \"path\" : \"three\"" + NEW_LINE +
            "} ]>"));
    }

    @Test
    public void shouldRetrieveLogsAsLogEntries() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(RECEIVED_REQUEST)
                .setHttpRequest(request("request_one"))
                .setMessageFormat(RECEIVED_REQUEST_MESSAGE_FORMAT)
                .setArguments(request("request_one"))
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(EXPECTATION_MATCHED)
                .setHttpRequest(request("request_one"))
                .setExpectation(new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_one")))
                .setMessageFormat("request:{}matched expectation:{}")
                .setArguments(request("request_one"), new Expectation(request("request_one")).withId("key_one").thenRespond(response("response_one")))
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "logs")
                    .withQueryStringParameter("format", "log_entries")
            );

        // then
        assertThat(response.getStatusCode(), is(200));
        String body = response.getBodyAsString();
        assertThat(body, containsString("RECEIVED_REQUEST"));
        assertThat(body, containsString("EXPECTATION_MATCHED"));
        LogEntrySerializer serializer = new LogEntrySerializer(new MockServerLogger());
        LogEntry[] deserialized = serializer.deserializeArray(body);
        assertThat(deserialized.length, is(2));
        assertThat(deserialized[0].getType(), is(RECEIVED_REQUEST));
        assertThat(deserialized[1].getType(), is(EXPECTATION_MATCHED));
        assertThat(deserialized[0].getHttpRequest(), is(nullValue()));
        assertThat(deserialized[1].getExpectation(), is(nullValue()));
    }

    @Test
    public void shouldRetrieveLogsAsLogEntriesByCorrelationId() {
        // given
        configuration.logLevel(Level.INFO);
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(RECEIVED_REQUEST)
                .setCorrelationId("test-corr-id-1")
                .setHttpRequest(request("request_one"))
                .setMessageFormat("received request:{}")
                .setArguments("request_one")
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(EXPECTATION_MATCHED)
                .setCorrelationId("test-corr-id-1")
                .setHttpRequest(request("request_one"))
                .setMessageFormat("matched:{}")
                .setArguments("key_one")
        );
        httpState.log(
            new LogEntry()
                .setLogLevel(INFO)
                .setType(RECEIVED_REQUEST)
                .setCorrelationId("test-corr-id-2")
                .setHttpRequest(request("request_two"))
                .setMessageFormat("received request:{}")
                .setArguments("request_two")
        );

        // when
        HttpResponse response = httpState
            .retrieve(
                request()
                    .withQueryStringParameter("type", "logs")
                    .withQueryStringParameter("format", "log_entries")
                    .withQueryStringParameter("correlationId", "test-corr-id-1")
            );

        // then
        assertThat(response.getStatusCode(), is(200));
        LogEntrySerializer serializer = new LogEntrySerializer(new MockServerLogger());
        LogEntry[] deserialized = serializer.deserializeArray(response.getBodyAsString());
        assertThat(deserialized.length, is(2));
        assertThat(deserialized[0].getType(), is(RECEIVED_REQUEST));
        assertThat(deserialized[0].getCorrelationId(), is("test-corr-id-1"));
        assertThat(deserialized[1].getType(), is(EXPECTATION_MATCHED));
        assertThat(deserialized[1].getCorrelationId(), is("test-corr-id-1"));
    }

    @Test
    public void shouldRoundTripLogEntriesThroughSerializer() {
        // given
        LogEntrySerializer serializer = new LogEntrySerializer(new MockServerLogger());
        LogEntry entry = new LogEntry()
            .setLogLevel(INFO)
            .setType(EXPECTATION_NOT_MATCHED)
            .setCorrelationId("round-trip-test")
            .setMessageFormat("test message:{}")
            .setArguments("arg1");

        // when
        String json = serializer.serialize(Collections.singletonList(entry));
        LogEntry[] deserialized = serializer.deserializeArray(json);

        // then
        assertThat(deserialized.length, is(1));
        assertThat(deserialized[0].getType(), is(EXPECTATION_NOT_MATCHED));
        assertThat(deserialized[0].getCorrelationId(), is("round-trip-test"));
        assertThat(deserialized[0].getLogLevel(), is(INFO));
        assertThat(deserialized[0].getMessageFormat(), is("test message:{}"));
    }

    @Test
    public void shouldDeserializeEmptyLogEntryArray() {
        // given
        LogEntrySerializer serializer = new LogEntrySerializer(new MockServerLogger());

        // when / then
        assertThat(serializer.deserializeArray("[]").length, is(0));
        assertThat(serializer.deserializeArray("").length, is(0));
        assertThat(serializer.deserializeArray(null).length, is(0));
    }

    @Test
    public void shouldHandleFileStore() {
        // given
        HttpRequest storeRequest = request("/mockserver/files/store")
            .withMethod("PUT")
            .withBody("{\"name\":\"test.txt\",\"content\":\"hello world\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(storeRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        assertThat(responseWriter.response.getBodyAsString(), is("{\"name\":\"test.txt\",\"size\":11}"));
    }

    @Test
    public void shouldHandleFileStoreBase64() {
        // given
        String base64Content = java.util.Base64.getEncoder().encodeToString("binary data".getBytes(UTF_8));
        HttpRequest storeRequest = request("/mockserver/files/store")
            .withMethod("PUT")
            .withBody("{\"name\":\"binary.dat\",\"content\":\"" + base64Content + "\",\"base64\":true}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(storeRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        assertThat(responseWriter.response.getBodyAsString(), is("{\"name\":\"binary.dat\",\"size\":11}"));
    }

    @Test
    public void shouldHandleFileStoreWithEmptyBody() {
        // given
        HttpRequest storeRequest = request("/mockserver/files/store")
            .withMethod("PUT");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(storeRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

    @Test
    public void shouldHandleFileStoreMissingFields() {
        // given
        HttpRequest storeRequest = request("/mockserver/files/store")
            .withMethod("PUT")
            .withBody("{\"name\":\"test.txt\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(storeRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

    @Test
    public void shouldHandleFileRetrieve() {
        // given
        httpState.getFileStore().store("test.txt", "hello world".getBytes(UTF_8));
        HttpRequest retrieveRequest = request("/mockserver/files/retrieve")
            .withMethod("PUT")
            .withBody("{\"name\":\"test.txt\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        assertThat(new String(responseWriter.response.getBodyAsRawBytes(), UTF_8), is("hello world"));
    }

    @Test
    public void shouldHandleFileRetrieveNotFound() {
        // given
        HttpRequest retrieveRequest = request("/mockserver/files/retrieve")
            .withMethod("PUT")
            .withBody("{\"name\":\"nonexistent.txt\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(404));
    }

    @Test
    public void shouldHandleFileRetrieveMissingName() {
        // given
        HttpRequest retrieveRequest = request("/mockserver/files/retrieve")
            .withMethod("PUT")
            .withBody("{\"other\":\"value\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(retrieveRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

    @Test
    public void shouldHandleFileList() {
        // given
        httpState.getFileStore().store("file1.txt", "data1".getBytes(UTF_8));
        httpState.getFileStore().store("file2.txt", "data2".getBytes(UTF_8));
        HttpRequest listRequest = request("/mockserver/files/list")
            .withMethod("PUT");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(listRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("file1.txt"));
        assertThat(body, containsString("file2.txt"));
    }

    @Test
    public void shouldHandleFileListEmpty() {
        // given
        HttpRequest listRequest = request("/mockserver/files/list")
            .withMethod("PUT");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(listRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        assertThat(responseWriter.response.getBodyAsString(), containsString("[ ]"));
    }

    @Test
    public void shouldHandleFileDelete() {
        // given
        httpState.getFileStore().store("test.txt", "data".getBytes(UTF_8));
        HttpRequest deleteRequest = request("/mockserver/files/delete")
            .withMethod("PUT")
            .withBody("{\"name\":\"test.txt\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(deleteRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        assertThat(httpState.getFileStore().exists("test.txt"), is(false));
    }

    @Test
    public void shouldHandleFileDeleteNotFound() {
        // given
        HttpRequest deleteRequest = request("/mockserver/files/delete")
            .withMethod("PUT")
            .withBody("{\"name\":\"nonexistent.txt\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(deleteRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(404));
    }

    @Test
    public void shouldHandleFileDeleteMissingName() {
        // given
        HttpRequest deleteRequest = request("/mockserver/files/delete")
            .withMethod("PUT")
            .withBody("{\"other\":\"value\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(deleteRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

    @Test
    public void shouldReturnEarlyMatchingExpectationWhenRespondBeforeBodyIsTrue() {
        // given
        Expectation expectation = new Expectation(
            request().withMethod("POST").withPath("/upload").withRespondBeforeBody(true)
        ).thenRespond(response().withStatusCode(403));
        httpState.add(expectation);

        // when
        Expectation matched = httpState.firstMatchingEarlyExpectation(
            request().withMethod("POST").withPath("/upload")
        );

        // then
        assertThat(matched, is(expectation));
    }

    @Test
    public void shouldReturnNullEarlyMatchingExpectationWhenRespondBeforeBodyIsNotSet() {
        // given
        httpState.add(new Expectation(
            request().withMethod("POST").withPath("/upload")
        ).thenRespond(response().withStatusCode(200)));

        // when
        Expectation matched = httpState.firstMatchingEarlyExpectation(
            request().withMethod("POST").withPath("/upload")
        );

        // then
        assertThat(matched, is(nullValue()));
    }

    @Test
    public void shouldRejectRespondBeforeBodyWithBodyMatcher() {
        // given
        Expectation expectation = new Expectation(
            request().withMethod("POST").withPath("/upload").withRespondBeforeBody(true).withBody("some-body")
        ).thenRespond(response().withStatusCode(403));

        // when / then
        try {
            httpState.add(expectation);
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException expected) {
            assertThat(expected.getMessage(), CoreMatchers.containsString("respondBeforeBody"));
            assertThat(expected.getMessage(), CoreMatchers.containsString("body matcher"));
        }
    }

    @Test
    public void shouldRejectRespondBeforeBodyWithNullAction() {
        // given an Expectation with respondBeforeBody=true but no configured action
        Expectation expectation = new Expectation(
            request().withMethod("POST").withPath("/upload").withRespondBeforeBody(true)
        );

        // when / then
        try {
            httpState.add(expectation);
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException expected) {
            assertThat(expected.getMessage(), CoreMatchers.containsString("respondBeforeBody"));
            assertThat(expected.getMessage(), CoreMatchers.containsString("RESPONSE or ERROR action"));
        }
    }

    @Test
    public void shouldRejectRespondBeforeBodyWithForwardAction() {
        // given
        Expectation expectation = new Expectation(
            request().withMethod("POST").withPath("/upload").withRespondBeforeBody(true)
        ).thenForward(org.mockserver.model.HttpForward.forward().withHost("example.com").withPort(80));

        // when / then
        try {
            httpState.add(expectation);
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException expected) {
            assertThat(expected.getMessage(), CoreMatchers.containsString("respondBeforeBody"));
            assertThat(expected.getMessage(), CoreMatchers.containsString("RESPONSE"));
        }
    }

    @Test
    public void shouldAllowRespondBeforeBodyWithErrorAction() {
        // given
        Expectation expectation = new Expectation(
            request().withMethod("POST").withPath("/upload").withRespondBeforeBody(true)
        ).thenError(org.mockserver.model.HttpError.error().withDropConnection(true));

        // when (no exception)
        httpState.add(expectation);

        // then
        Expectation matched = httpState.firstMatchingEarlyExpectation(
            request().withMethod("POST").withPath("/upload")
        );
        assertThat(matched, is(expectation));
    }

    @Test
    public void shouldResetFileStore() {
        // given
        httpState.getFileStore().store("test.txt", "data".getBytes(UTF_8));

        // when
        httpState.reset();

        // then
        assertThat(httpState.getFileStore().size(), is(0));
    }

    // --- Clock endpoint tests ---

    @Test
    public void shouldHandleClockFreezeWithInstant() throws Exception {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"action\":\"freeze\",\"instant\":\"2024-01-01T00:00:00Z\"}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("\"status\" : \"freeze\""));
        assertThat(body, containsString("\"currentInstant\" : \"2024-01-01T00:00:00Z\""));
        assertThat(body, containsString("\"currentEpochMillis\""));
    }

    @Test
    public void shouldHandleClockFreezeWithoutInstant() throws Exception {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"action\":\"freeze\"}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("\"status\" : \"freeze\""));
        assertThat(body, containsString("\"currentInstant\""));
    }

    @Test
    public void shouldHandleClockAdvance() throws Exception {
        // given
        TimeService.freeze(java.time.Instant.parse("2024-01-01T00:00:00Z"));
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"action\":\"advance\",\"durationMillis\":60000}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("\"status\" : \"advance\""));
        assertThat(body, containsString("\"currentInstant\" : \"2024-01-01T00:01:00Z\""));
    }

    @Test
    public void shouldHandleClockReset() throws Exception {
        // given
        TimeService.freeze(java.time.Instant.parse("2024-01-01T00:00:00Z"));
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"action\":\"reset\"}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("\"status\" : \"reset\""));
        assertThat(TimeService.isFrozen(), is(false));
    }

    @Test
    public void shouldRejectClockWithUnknownAction() throws Exception {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"action\":\"rewind\"}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("unknown action 'rewind'"));
    }

    @Test
    public void shouldRejectClockWithMissingAction() throws Exception {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"instant\":\"2024-01-01T00:00:00Z\"}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("'action' field is required"));
    }

    @Test
    public void shouldRejectClockAdvanceWithNonPositiveDuration() throws Exception {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"action\":\"advance\",\"durationMillis\":0}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("'durationMillis' must be a positive number"));
    }

    @Test
    public void shouldRejectClockFreezeWithInvalidInstant() throws Exception {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("{\"action\":\"freeze\",\"instant\":\"not-a-date\"}");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("invalid 'instant' value"));
    }

    @Test
    public void shouldHandleClockGetWhenUnfrozen() throws Exception {
        // given
        TimeService.reset();
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("GET");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("\"frozen\" : false"));
        assertThat(body, containsString("\"currentInstant\""));
        assertThat(body, containsString("\"currentEpochMillis\""));
    }

    @Test
    public void shouldHandleClockGetWhenFrozen() throws Exception {
        // given
        TimeService.freeze(java.time.Instant.parse("2024-06-15T12:00:00Z"));
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("GET");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(200));
        String body = responseWriter.response.getBodyAsString();
        assertThat(body, containsString("\"frozen\" : true"));
        assertThat(body, containsString("\"currentInstant\" : \"2024-06-15T12:00:00Z\""));
    }

    // --- Service chaos endpoint tests ---

    @Test
    public void shouldRegisterAndGetServiceChaos() throws Exception {
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();
        // register
        FakeResponseWriter putWriter = new FakeResponseWriter();
        HttpRequest putRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"host\":\"upstream.svc\",\"chaos\":{\"errorStatus\":503,\"errorProbability\":1.0}}");
        assertThat(httpState.handle(putRequest, putWriter, false), is(true));
        assertThat(putWriter.response.getStatusCode(), is(200));
        assertThat(putWriter.response.getBodyAsString(), containsString("\"status\" : \"registered\""));
        assertThat(org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().get("upstream.svc"), is(notNullValue()));

        // read back
        FakeResponseWriter getWriter = new FakeResponseWriter();
        assertThat(httpState.handle(request("/mockserver/serviceChaos").withMethod("GET"), getWriter, false), is(true));
        assertThat(getWriter.response.getStatusCode(), is(200));
        String getBody = getWriter.response.getBodyAsString();
        assertThat(getBody, containsString("upstream.svc"));
        assertThat(getBody, containsString("\"errorStatus\""));
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();
    }

    @Test
    public void shouldRemoveServiceChaosForHost() throws Exception {
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().put("upstream.svc",
            org.mockserver.model.HttpChaosProfile.httpChaosProfile().withErrorStatus(503));
        FakeResponseWriter writer = new FakeResponseWriter();
        HttpRequest removeRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"host\":\"upstream.svc\",\"remove\":true}");
        assertThat(httpState.handle(removeRequest, writer, false), is(true));
        assertThat(writer.response.getStatusCode(), is(200));
        assertThat(writer.response.getBodyAsString(), containsString("\"status\" : \"removed\""));
        assertThat(org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().get("upstream.svc"), is(nullValue()));
    }

    @Test
    public void shouldClearAllServiceChaos() throws Exception {
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().put("a",
            org.mockserver.model.HttpChaosProfile.httpChaosProfile().withErrorStatus(503));
        FakeResponseWriter writer = new FakeResponseWriter();
        HttpRequest clearRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"clear\":true}");
        assertThat(httpState.handle(clearRequest, writer, false), is(true));
        assertThat(writer.response.getStatusCode(), is(200));
        assertThat(writer.response.getBodyAsString(), containsString("\"status\" : \"cleared\""));
        assertThat(org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().entries().isEmpty(), is(true));
    }

    @Test
    public void shouldRejectInvalidServiceChaosProfile() throws Exception {
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();
        FakeResponseWriter writer = new FakeResponseWriter();
        HttpRequest putRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"host\":\"upstream.svc\",\"chaos\":{\"errorStatus\":999}}");
        assertThat(httpState.handle(putRequest, writer, false), is(true));
        assertThat(writer.response.getStatusCode(), is(400));
        assertThat(writer.response.getBodyAsString(), containsString("invalid chaos profile"));
        assertThat(org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().get("upstream.svc"), is(nullValue()));
    }

    @Test
    public void shouldRejectServiceChaosWithBothClearAndHost() throws Exception {
        FakeResponseWriter writer = new FakeResponseWriter();
        HttpRequest putRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"clear\":true,\"host\":\"upstream.svc\"}");
        assertThat(httpState.handle(putRequest, writer, false), is(true));
        assertThat(writer.response.getStatusCode(), is(400));
        assertThat(writer.response.getBodyAsString(), containsString("cannot specify both 'clear' and 'host'"));
    }

    @Test
    public void shouldRejectServiceChaosWithoutHost() throws Exception {
        FakeResponseWriter writer = new FakeResponseWriter();
        HttpRequest putRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"chaos\":{\"errorStatus\":503}}");
        assertThat(httpState.handle(putRequest, writer, false), is(true));
        assertThat(writer.response.getStatusCode(), is(400));
        assertThat(writer.response.getBodyAsString(), containsString("'host' field is required"));
    }

    @Test
    public void shouldRegisterServiceChaosWithTtl() throws Exception {
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();
        FakeResponseWriter writer = new FakeResponseWriter();
        HttpRequest putRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"host\":\"upstream.svc\",\"chaos\":{\"errorStatus\":503},\"ttlMillis\":300000}");
        assertThat(httpState.handle(putRequest, writer, false), is(true));
        assertThat(writer.response.getStatusCode(), is(200));
        String body = writer.response.getBodyAsString();
        assertThat(body, containsString("\"status\" : \"registered\""));
        assertThat(body, containsString("\"ttlMillis\" : 300000"));
        assertThat(org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().get("upstream.svc"), is(notNullValue()));

        // GET surfaces a remaining-TTL countdown for the registration
        FakeResponseWriter getWriter = new FakeResponseWriter();
        assertThat(httpState.handle(request("/mockserver/serviceChaos").withMethod("GET"), getWriter, false), is(true));
        assertThat(getWriter.response.getBodyAsString(), containsString("ttlRemainingMillis"));

        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();
    }

    @Test
    public void shouldAddCorsHeadersToServiceChaosResponsesWithoutEnableCorsForApi() {
        // the dashboard may be served from another origin (e.g. the UI dev server) — the
        // service-chaos control-plane responses must carry CORS headers even when
        // enableCORSForAPI is off (the default here), mirroring the metrics / MCP endpoints
        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();

        FakeResponseWriter getWriter = new FakeResponseWriter();
        HttpRequest getRequest = request("/mockserver/serviceChaos")
            .withMethod("GET")
            .withHeader("Origin", "http://localhost:3000");
        assertThat(httpState.handle(getRequest, getWriter, false), is(true));
        assertThat(getWriter.response.getStatusCode(), is(200));
        assertThat(getWriter.response.getFirstHeader("access-control-allow-origin"), is("http://localhost:3000"));

        FakeResponseWriter putWriter = new FakeResponseWriter();
        HttpRequest putRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withHeader("Origin", "http://localhost:3000")
            .withBody("{\"host\":\"upstream.svc\",\"chaos\":{\"errorStatus\":503}}");
        assertThat(httpState.handle(putRequest, putWriter, false), is(true));
        assertThat(putWriter.response.getStatusCode(), is(200));
        assertThat(putWriter.response.getFirstHeader("access-control-allow-origin"), is("http://localhost:3000"));

        org.mockserver.mock.action.http.ServiceChaosRegistry.getInstance().reset();
    }

    @Test
    public void shouldAddCorsHeadersToDriftResponsesWithoutEnableCorsForApi() {
        // the Drift dashboard tab GETs /mockserver/drift (and clears via /drift/clear)
        // cross-origin from the UI dev server, so these responses must carry CORS headers
        // even when enableCORSForAPI is off (the default here)
        FakeResponseWriter getWriter = new FakeResponseWriter();
        HttpRequest getRequest = request("/mockserver/drift")
            .withMethod("GET")
            .withHeader("Origin", "http://localhost:3000");
        assertThat(httpState.handle(getRequest, getWriter, false), is(true));
        assertThat(getWriter.response.getStatusCode(), is(200));
        assertThat(getWriter.response.getFirstHeader("access-control-allow-origin"), is("http://localhost:3000"));

        FakeResponseWriter clearWriter = new FakeResponseWriter();
        HttpRequest clearRequest = request("/mockserver/drift/clear")
            .withMethod("PUT")
            .withHeader("Origin", "http://localhost:3000");
        assertThat(httpState.handle(clearRequest, clearWriter, false), is(true));
        assertThat(clearWriter.response.getStatusCode(), is(200));
        assertThat(clearWriter.response.getFirstHeader("access-control-allow-origin"), is("http://localhost:3000"));
    }

    @Test
    public void shouldRejectServiceChaosWithTtlBelowOne() throws Exception {
        FakeResponseWriter writer = new FakeResponseWriter();
        HttpRequest putRequest = request("/mockserver/serviceChaos")
            .withMethod("PUT")
            .withBody("{\"host\":\"upstream.svc\",\"chaos\":{\"errorStatus\":503},\"ttlMillis\":0}");
        assertThat(httpState.handle(putRequest, writer, false), is(true));
        assertThat(writer.response.getStatusCode(), is(400));
        assertThat(writer.response.getBodyAsString(), containsString("'ttlMillis' must be >= 1"));
    }

    @Test
    public void shouldRejectClockWithEmptyBody() throws Exception {
        // given
        FakeResponseWriter responseWriter = new FakeResponseWriter();
        HttpRequest clockRequest = request("/mockserver/clock")
            .withMethod("PUT")
            .withBody("");

        // when
        boolean handle = httpState.handle(clockRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("request body is required"));
    }

    @Test
    public void shouldHandleOidcRequestWithDefaults() {
        // given
        HttpRequest oidcRequest = request("/mockserver/oidc")
            .withMethod("PUT");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(oidcRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        Expectation[] returnedExpectations = expectationSerializer.deserializeArray(
            responseWriter.response.getBodyAsString(), true
        );
        assertThat(returnedExpectations.length, is(6));

        // Verify the discovery endpoint is now matchable
        HttpResponse discoveryResponse = httpState.firstMatchingExpectation(
            request("/.well-known/openid-configuration").withMethod("GET")
        ).getHttpResponse();
        assertThat(discoveryResponse, is(notNullValue()));
        assertThat(discoveryResponse.getStatusCode(), is(200));
        assertThat(discoveryResponse.getBodyAsString(), containsString("\"issuer\""));
    }

    @Test
    public void shouldHandleOidcRequestWithCustomConfig() {
        // given
        HttpRequest oidcRequest = request("/mockserver/oidc")
            .withMethod("PUT")
            .withBody("{\"issuer\":\"https://custom.idp\",\"subject\":\"custom-sub\",\"tokenPath\":\"/custom/token\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(oidcRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));

        // Verify the token endpoint matches the custom path
        Expectation tokenMatch = httpState.firstMatchingExpectation(
            request("/custom/token").withMethod("POST")
        );
        assertThat(tokenMatch, is(notNullValue()));
        assertThat(tokenMatch.getHttpResponse().getBodyAsString(), containsString("access_token"));
    }

    @Test
    public void shouldHandleOidcRequestWithEmptyBody() {
        // given
        HttpRequest oidcRequest = request("/mockserver/oidc")
            .withMethod("PUT")
            .withBody("");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(oidcRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        Expectation[] returnedExpectations = expectationSerializer.deserializeArray(
            responseWriter.response.getBodyAsString(), true
        );
        assertThat(returnedExpectations.length, is(6));
    }

    @Test
    public void shouldHandleInvalidOidcRequest() {
        // given
        HttpRequest oidcRequest = request("/mockserver/oidc")
            .withMethod("PUT")
            .withBody("{invalid json");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(oidcRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

    // --- PUT /import tests ---

    @Test
    public void shouldHandleImportHarAutoDetected() {
        // given
        String har = "{\"log\":{\"entries\":[" +
            "{\"request\":{\"method\":\"GET\",\"url\":\"http://example.com/api/test\"}," +
            "\"response\":{\"status\":200,\"content\":{\"text\":\"{\\\"ok\\\":true}\"}}}" +
            "]}}";
        HttpRequest importRequest = request("/mockserver/import")
            .withMethod("PUT")
            .withBody(har);
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(importRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        Expectation[] returnedExpectations = expectationSerializer.deserializeArray(
            responseWriter.response.getBodyAsString(), true
        );
        assertThat(returnedExpectations.length, is(1));

        // Verify the expectation is now matchable
        Expectation match = httpState.firstMatchingExpectation(
            request("/api/test").withMethod("GET")
        );
        assertThat(match, is(notNullValue()));
        assertThat(match.getHttpResponse().getStatusCode(), is(200));
    }

    @Test
    public void shouldHandleImportPostmanAutoDetected() {
        // given
        String postman = "{\"info\":{\"name\":\"Test\"},\"item\":[" +
            "{\"name\":\"Get Health\",\"request\":{\"method\":\"GET\",\"url\":\"http://example.com/health\"}," +
            "\"response\":[{\"code\":200,\"body\":\"{\\\"status\\\":\\\"up\\\"}\"}]}" +
            "]}";
        HttpRequest importRequest = request("/mockserver/import")
            .withMethod("PUT")
            .withBody(postman);
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(importRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
        Expectation[] returnedExpectations = expectationSerializer.deserializeArray(
            responseWriter.response.getBodyAsString(), true
        );
        assertThat(returnedExpectations.length, is(1));
    }

    @Test
    public void shouldHandleImportWithFormatQueryParam() {
        // given — use ?format=har
        String har = "{\"log\":{\"entries\":[" +
            "{\"request\":{\"method\":\"GET\",\"url\":\"http://example.com/data\"}," +
            "\"response\":{\"status\":200}}" +
            "]}}";
        HttpRequest importRequest = request("/mockserver/import")
            .withMethod("PUT")
            .withQueryStringParameter("format", "har")
            .withBody(har);
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(importRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(201));
    }

    @Test
    public void shouldHandleImportMalformedJson() {
        // given
        HttpRequest importRequest = request("/mockserver/import")
            .withMethod("PUT")
            .withBody("{not valid json");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(importRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

    @Test
    public void shouldHandleImportEmptyBody() {
        // given
        HttpRequest importRequest = request("/mockserver/import")
            .withMethod("PUT")
            .withBody("");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(importRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

    @Test
    public void shouldHandleImportUnsupportedFormat() {
        // given
        HttpRequest importRequest = request("/mockserver/import")
            .withMethod("PUT")
            .withQueryStringParameter("format", "insomnia")
            .withBody("{\"some\":\"json\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(importRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("unsupported import format"));
    }

    @Test
    public void shouldHandleImportUnrecognisedAutoDetect() {
        // given — JSON that is neither HAR nor Postman
        HttpRequest importRequest = request("/mockserver/import")
            .withMethod("PUT")
            .withBody("{\"something\":\"else\"}");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(importRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("unable to auto-detect"));
    }

    // ---- Pact Verify Endpoint Tests ----

    @Test
    public void shouldHandlePactVerifyReturning202WhenAllInteractionsPass() {
        // given — register an expectation
        httpState.add(new Expectation(
            request().withMethod("GET").withPath("/health")
        ).thenRespond(
            response().withStatusCode(200)
        ));

        String pactContract = "{\n"
            + "  \"consumer\": {\"name\": \"test\"},\n"
            + "  \"provider\": {\"name\": \"provider\"},\n"
            + "  \"interactions\": [{\n"
            + "    \"description\": \"health check\",\n"
            + "    \"request\": {\"method\": \"GET\", \"path\": \"/health\"},\n"
            + "    \"response\": {\"status\": 200}\n"
            + "  }],\n"
            + "  \"metadata\": {\"pactSpecification\": {\"version\": \"3.0.0\"}}\n"
            + "}";

        HttpRequest verifyRequest = request("/mockserver/pact/verify")
            .withMethod("PUT")
            .withBody(pactContract);
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(verifyRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(202));
        assertThat(responseWriter.response.getBodyAsString(), containsString("\"verified\" : true"));
    }

    @Test
    public void shouldHandlePactVerifyReturning406WhenInteractionFails() {
        // given — register an expectation with status 200
        httpState.add(new Expectation(
            request().withMethod("GET").withPath("/health")
        ).thenRespond(
            response().withStatusCode(200)
        ));

        // Pact expects status 204 — mismatch
        String pactContract = "{\n"
            + "  \"consumer\": {\"name\": \"test\"},\n"
            + "  \"provider\": {\"name\": \"provider\"},\n"
            + "  \"interactions\": [{\n"
            + "    \"description\": \"health check\",\n"
            + "    \"request\": {\"method\": \"GET\", \"path\": \"/health\"},\n"
            + "    \"response\": {\"status\": 204}\n"
            + "  }],\n"
            + "  \"metadata\": {\"pactSpecification\": {\"version\": \"3.0.0\"}}\n"
            + "}";

        HttpRequest verifyRequest = request("/mockserver/pact/verify")
            .withMethod("PUT")
            .withBody(pactContract);
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(verifyRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(406));
        assertThat(responseWriter.response.getBodyAsString(), containsString("\"verified\" : false"));
        assertThat(responseWriter.response.getBodyAsString(), containsString("status code mismatch"));
    }

    @Test
    public void shouldHandlePactVerifyReturning400OnEmptyBody() {
        HttpRequest verifyRequest = request("/mockserver/pact/verify")
            .withMethod("PUT")
            .withBody("");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(verifyRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
        assertThat(responseWriter.response.getBodyAsString(), containsString("must not be empty"));
    }

    @Test
    public void shouldHandlePactVerifyReturning400OnMalformedJson() {
        HttpRequest verifyRequest = request("/mockserver/pact/verify")
            .withMethod("PUT")
            .withBody("not json at all {{{");
        FakeResponseWriter responseWriter = new FakeResponseWriter();

        // when
        boolean handle = httpState.handle(verifyRequest, responseWriter, false);

        // then
        assertThat(handle, is(true));
        assertThat(responseWriter.response.getStatusCode(), is(400));
    }

}
