package org.mockserver.log;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.mockserver.configuration.Configuration;
import org.mockserver.log.model.LogEntry;
import org.mockserver.logging.MockServerLogger;
import org.mockserver.model.HttpRequest;
import org.mockserver.scheduler.Scheduler;
import org.mockserver.verify.Verification;
import org.mockserver.verify.VerificationSequence;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;

import static java.util.concurrent.TimeUnit.SECONDS;
import static junit.framework.TestCase.fail;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockserver.character.Character.NEW_LINE;
import static org.mockserver.configuration.Configuration.configuration;
import static org.mockserver.log.model.LogEntry.LogMessageType.RECEIVED_REQUEST;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.verify.Verification.verification;
import static org.mockserver.verify.VerificationTimes.atLeast;
import static org.mockserver.verify.VerificationTimes.exactly;

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

    private static final Scheduler scheduler = new Scheduler(configuration(), new MockServerLogger());
    private final Configuration configuration = configuration();
    private MockServerEventLog mockServerEventLog;

    @Before
    public void setupTestFixture() {
        configuration.detailedVerificationFailures(false);
        mockServerEventLog = new MockServerEventLog(configuration, new MockServerLogger(configuration, MockServerLogger.class), scheduler, true);
    }

    @AfterClass
    public static void stopScheduler() {
        scheduler.shutdown();
    }

    public String verify(Verification verification) {
        CompletableFuture<String> result = new CompletableFuture<>();
        mockServerEventLog.verify(verification, result::complete);
        try {
            return result.get(10, SECONDS);
        } catch (Exception e) {
            fail(e.getMessage());
            return null;
        }
    }

    public String verify(VerificationSequence verificationSequence) {
        CompletableFuture<String> result = new CompletableFuture<>();
        mockServerEventLog.verify(verificationSequence, result::complete);
        try {
            return result.get(10, SECONDS);
        } catch (Exception e) {
            fail(e.getMessage());
            return null;
        }
    }

    @Test
    public void shouldPassVerificationWithNullRequest() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify((Verification) null), is(""));
    }

    @Test
    public void shouldPassVerificationWithDefaultTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest()
                            .withPath("some_path")
                    )
            ),
            is(""));
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest()
                            .withPath("some_other_path")
                    )
            ),
            is(""));
    }

    @Test
    public void shouldPassVerificationWithAtLeastTwoTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest().withPath("some_path")
                    )
                    .withTimes(atLeast(2))
            ),
            is(""));
    }

    @Test
    public void shouldPassVerificationWithAtLeastZeroTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest().withPath("some_non_matching_path")
                    )
                    .withTimes(atLeast(0))
            ),
            is(""));
    }

    @Test
    public void shouldPassVerificationWithExactlyTwoTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest()
                            .withPath("some_path")
                    )
                    .withTimes(exactly(2))
            ),
            is(""));
    }

    @Test
    public void shouldPassVerificationWithExactlyZeroTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest()
                            .withPath("some_non_matching_path")
                    )
                    .withTimes(exactly(0))
            ),
            is(""));
    }

    @Test
    public void shouldFailVerificationWithNullRequest() {
        // given

        // then
        assertThat(verify((Verification) null), is(""));
    }

    @Test
    public void shouldFailVerificationWithDefaultTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest().withPath("some_non_matching_path")
                    )
            ),
            is("Request not found at least once, expected:<{" + NEW_LINE +
                "  \"path\" : \"some_non_matching_path\"" + NEW_LINE +
                "}> but was:<[ {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "} ]>"));
    }

    @Test
    public void shouldFailVerificationWithAtLeastTwoTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest().withPath("some_other_path")
                    )
                    .withTimes(atLeast(2))
            ),
            is("Request not found at least 2 times, expected:<{" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}> but was:<[ {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "} ]>"));
    }

    @Test
    public void shouldFailVerificationWithLimitedReturnedRequestsViaConfiguration() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");
        configuration.maximumNumberOfRequestToReturnInVerificationFailure(1);

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest().withPath("some_other_path")
                    )
                    .withTimes(atLeast(2))
            ),
            is("Request not found at least 2 times, expected:<{" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}> but was found 1 time among 3 total requests"));
    }

    @Test
    public void shouldFailVerificationWithLimitedReturnedRequestsViaVerification() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest().withPath("some_other_path")
                    )
                    .withTimes(atLeast(2))
                    .withMaximumNumberOfRequestToReturnInVerificationFailure(1)
            ),
            is("Request not found at least 2 times, expected:<{" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}> but was found 1 time among 3 total requests"));
    }

    @Test
    public void shouldFailVerificationWithExactTwoTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest()
                            .withPath("some_other_path")
                    )
                    .withTimes(exactly(2))
            ),
            is("Request not found exactly 2 times, expected:<{" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}> but was:<[ {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "} ]>"));
    }

    @Test
    public void shouldFailVerificationWithExactOneTime() {
        // given

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest()
                            .withPath("some_other_path")
                    )
                    .withTimes(exactly(1))
            ),
            is("Request not found exactly once, expected:<{" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}> but was:<[]>"));
    }

    @Test
    public void shouldFailVerificationWithExactZeroTimes() {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        HttpRequest otherHttpRequest = new HttpRequest().withPath("some_other_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(otherHttpRequest)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(
                        new HttpRequest()
                            .withPath("some_other_path")
                    )
                    .withTimes(exactly(0))
            ),
            is("Request not found exactly 0 times, expected:<{" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}> but was:<[ {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_other_path\"" + NEW_LINE +
                "}, {" + NEW_LINE +
                "  \"path\" : \"some_path\"" + NEW_LINE +
                "} ]>"));
    }

    @Test
    public void shouldFailVerificationWithNoInteractions() {
        // given
        HttpRequest httpRequest = new HttpRequest();

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        assertThat(verify(
                verification()
                    .withRequest(request())
                    .withTimes(exactly(0))
            ),
            is("Request not found exactly 0 times, expected:<{ }> but was:<{ }>"));
    }

    @Test
    public void shouldPassVerificationWhenRequestLoggedConcurrently() throws Exception {
        // given
        HttpRequest httpRequest = new HttpRequest().withPath("some_path");
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // when
        CountDownLatch concurrentAddComplete = new CountDownLatch(1);
        new Thread(() -> {
            mockServerEventLog.add(
                new LogEntry()
                    .setHttpRequest(httpRequest)
                    .setType(RECEIVED_REQUEST)
            );
            concurrentAddComplete.countDown();
        }).start();

        if (!concurrentAddComplete.await(10, SECONDS)) {
            fail("Background add() did not complete within 10 seconds");
        }

        // then
        assertThat(verify(verification().withRequest(httpRequest).withTimes(exactly(2))), is(""));
    }

    @Test
    public void shouldIncludeDiffInVerificationFailureWhenEnabled() {
        // given
        configuration.detailedVerificationFailures(true);
        HttpRequest httpRequest = new HttpRequest().withMethod("POST").withPath("some_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        String result = verify(
            verification()
                .withRequest(
                    request().withMethod("GET").withPath("some_other_path")
                )
        );
        assertThat(result, org.hamcrest.CoreMatchers.containsString("Request not found"));
        assertThat(result, org.hamcrest.CoreMatchers.containsString("closest match diff:"));
        assertThat(result, org.hamcrest.CoreMatchers.containsString("method:"));
    }

    @Test
    public void shouldNotIncludeDiffInVerificationFailureWhenDisabled() {
        // given
        configuration.detailedVerificationFailures(false);
        HttpRequest httpRequest = new HttpRequest().withMethod("POST").withPath("some_path");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(httpRequest)
                .setType(RECEIVED_REQUEST)
        );

        // then
        String result = verify(
            verification()
                .withRequest(
                    request().withMethod("GET").withPath("some_other_path")
                )
        );
        assertThat(result, org.hamcrest.CoreMatchers.containsString("Request not found"));
        assertThat(result, org.hamcrest.CoreMatchers.not(org.hamcrest.CoreMatchers.containsString("closest match diff:")));
    }

    @Test
    public void shouldPickClosestMatchByFewestFailedFields() {
        // given
        configuration.detailedVerificationFailures(true);
        // closeMatch shares method GET with verification request, so method passes and only path fails
        HttpRequest closeMatch = new HttpRequest().withMethod("GET").withPath("almost_right");
        // farMatch has different method POST so fails at method (first check) with fail-fast
        HttpRequest farMatch = new HttpRequest().withMethod("POST").withPath("completely_wrong");

        // when
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(farMatch)
                .setType(RECEIVED_REQUEST)
        );
        mockServerEventLog.add(
            new LogEntry()
                .setHttpRequest(closeMatch)
                .setType(RECEIVED_REQUEST)
        );

        // then - verification matcher for GET /some_path is used against each received request
        // closeMatch (GET /almost_right): method matches, path fails -> 1 diff field (path)
        // farMatch (POST /completely_wrong): method fails with fail-fast -> 1 diff field (method)
        // both have 1 diff field but closeMatch is actually closer; since both have same count,
        // the first processed one (farMatch) will be kept
        String result = verify(
            verification()
                .withRequest(
                    request().withMethod("GET").withPath("some_path")
                )
        );
        assertThat(result, org.hamcrest.CoreMatchers.containsString("closest match diff:"));
    }

    @Test
    public void shouldNotIncludeDiffWhenNoRequestsLogged() {
        // given
        configuration.detailedVerificationFailures(true);

        // then
        String result = verify(
            verification()
                .withRequest(
                    request().withMethod("GET").withPath("some_path")
                )
        );
        assertThat(result, org.hamcrest.CoreMatchers.containsString("Request not found"));
        assertThat(result, org.hamcrest.CoreMatchers.not(org.hamcrest.CoreMatchers.containsString("closest match diff:")));
    }
}
