package org.zalando.nakadi.webservice.hila;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.apache.http.HttpStatus;
import org.hamcrest.Matchers;
import org.hamcrest.core.StringContains;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.zalando.nakadi.config.JsonConfig;
import org.zalando.nakadi.domain.EventType;
import org.zalando.nakadi.domain.ItemsWrapper;
import org.zalando.nakadi.domain.Subscription;
import org.zalando.nakadi.domain.SubscriptionBase;
import org.zalando.nakadi.domain.SubscriptionEventTypeStats;
import org.zalando.nakadi.service.BlacklistService;
import org.zalando.nakadi.util.ThreadUtils;
import org.zalando.nakadi.utils.JsonTestHelper;
import org.zalando.nakadi.utils.RandomSubscriptionBuilder;
import org.zalando.nakadi.view.Cursor;
import org.zalando.nakadi.view.EventTypePartitionView;
import org.zalando.nakadi.view.SubscriptionCursor;
import org.zalando.nakadi.view.SubscriptionCursorWithoutToken;
import org.zalando.nakadi.webservice.BaseAT;
import org.zalando.nakadi.webservice.SettingsControllerAT;
import org.zalando.nakadi.webservice.utils.NakadiTestUtils;
import org.zalando.nakadi.webservice.utils.TestStreamingClient;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.jayway.restassured.RestAssured.given;
import static com.jayway.restassured.RestAssured.when;
import static com.jayway.restassured.http.ContentType.JSON;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.zalando.nakadi.domain.SubscriptionBase.InitialPosition.BEGIN;
import static org.zalando.nakadi.domain.SubscriptionBase.InitialPosition.END;
import static org.zalando.nakadi.domain.SubscriptionEventTypeStats.Partition.AssignmentType.AUTO;
import static org.zalando.nakadi.domain.SubscriptionEventTypeStats.Partition.AssignmentType.DIRECT;
import static org.zalando.nakadi.utils.TestUtils.waitFor;
import static org.zalando.nakadi.webservice.hila.StreamBatch.MatcherIgnoringToken.equalToBatchIgnoringToken;
import static org.zalando.nakadi.webservice.hila.StreamBatch.singleEventBatch;
import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.commitCursors;
import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.createEventType;
import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.createSubscription;
import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.getNumberOfAssignedStreams;
import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.publishEvent;
import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.publishEvents;
import static org.zalando.nakadi.webservice.utils.TestStreamingClient.SESSION_ID_UNKNOWN;

public class HilaAT extends BaseAT {

    private static final ObjectMapper MAPPER = (new JsonConfig()).jacksonObjectMapper();
    private static final JsonTestHelper JSON_TEST_HELPER = new JsonTestHelper(MAPPER);
    private EventType eventType;
    private Subscription subscription;

    @Before
    public void before() throws IOException {
        // create event-type and subscribe to it
        eventType = createEventType();
        final SubscriptionBase subscription = RandomSubscriptionBuilder.builder()
                .withEventType(eventType.getName())
                .withStartFrom(BEGIN)
                .buildSubscriptionBase();
        this.subscription = createSubscription(subscription);
    }

    @Test(timeout = 10000)
    public void whenStreamTimeoutReachedPossibleToCommit() throws Exception {
        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "batch_limit=1&stream_limit=2&stream_timeout=1")
                .start();
        waitFor(() -> assertThat(client.getSessionId(), Matchers.not(equalTo(SESSION_ID_UNKNOWN))));

        publishEvent(eventType.getName(), "{\"foo\":\"bar\"}");
        waitFor(() -> Assert.assertFalse(client.getBatches().isEmpty()), TimeUnit.SECONDS.toMillis(2), 100);
        final SubscriptionCursor toCommit = client.getBatches().get(0).getCursor();
        client.close(); // connection is closed, and stream as well
        ThreadUtils.sleep(TimeUnit.SECONDS.toMillis(1));
        final int statusCode = commitCursors(
                subscription.getId(),
                Collections.singletonList(toCommit),
                client.getSessionId());
        Assert.assertEquals(HttpStatus.SC_NO_CONTENT, statusCode);
    }

    @Test(timeout = 30000)
    public void whenEventTypeRepartitionedTheNewSubscriptionShouldHaveUpdatedPartition() throws Exception {
        final EventType eventType = NakadiTestUtils.createBusinessEventTypeWithPartitions(1);
        NakadiTestUtils.publishBusinessEventWithUserDefinedPartition(
                eventType.getName(), 1, x -> "{\"foo\":\"bar\"}", p -> "0");
        NakadiTestUtils.repartitionEventType(eventType, 2);
        final Subscription subscription = createSubscription(
                RandomSubscriptionBuilder.builder()
                        .withEventType(eventType.getName())
                        .withStartFrom(BEGIN)
                        .buildSubscriptionBase());
        final TestStreamingClient clientAfterRepartitioning = TestStreamingClient
                .create(URL, subscription.getId(), "")
                .start();
        NakadiTestUtils.publishBusinessEventWithUserDefinedPartition(
                eventType.getName(), 1, x -> "{\"foo\":\"bar" + x + "\"}", p -> "1");
        waitFor(() -> assertThat(clientAfterRepartitioning.getBatches(), Matchers.hasSize(2)));
        Assert.assertTrue(clientAfterRepartitioning.getBatches().stream()
                .anyMatch(event -> event.getCursor().getPartition().equals("1")));
    }

    @Test(timeout = 10000)
    public void whenStreamTimeoutReachedThenEventsFlushed() {
        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(),
                        "batch_flush_timeout=600&batch_limit=1000&stream_timeout=2&max_uncommitted_events=1000")
                .start();
        waitFor(() -> assertThat(client.getSessionId(), Matchers.not(equalTo(SESSION_ID_UNKNOWN))));

        publishEvents(eventType.getName(), 4, x -> "{\"foo\":\"bar\"}");

        // when stream_timeout is reached we should get 2 batches:
        // first one containing 4 events, second one with debug message
        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(2)));
        assertThat(client.getBatches().get(0).getEvents(), Matchers.hasSize(4));
        assertThat(client.getBatches().get(1).getEvents(), Matchers.hasSize(0));
        System.out.println(client.getBatches());
    }

    @Test(timeout = 30000)
    public void whenOffsetIsCommittedNextSessionStartsFromNextEventAfterCommitted() throws Exception {
        // write 4 events to event-type
        publishEvents(eventType.getName(), 4, x -> "{\"foo\":\"bar" + x + "\"}");

        // create session, read from subscription and wait for events to be sent
        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "stream_limit=2")
                .start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(2)));
        assertThat(client.getBatches().get(0), equalToBatchIgnoringToken(singleEventBatch("0",
                "001-0001-000000000000000000", eventType.getName(), ImmutableMap.of("foo", "bar0"),
                "Stream started")));
        assertThat(client.getBatches().get(1), equalToBatchIgnoringToken(singleEventBatch("0",
                "001-0001-000000000000000001", eventType.getName(), ImmutableMap.of("foo", "bar1"))));

        // commit offset that will also trigger session closing as we reached stream_limit and committed
        commitCursors(subscription.getId(), ImmutableList.of(client.getBatches().get(1).getCursor()),
                client.getSessionId());
        waitFor(() -> assertThat(client.isRunning(), is(false)));

        // create new session and read from subscription again
        client.start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(2)));

        // check that we have read the next two events with correct offsets
        assertThat(client.getBatches().get(0), equalToBatchIgnoringToken(singleEventBatch("0",
                "001-0001-000000000000000002", eventType.getName(),
                ImmutableMap.of("foo", "bar2"), "Stream started")));
        assertThat(client.getBatches().get(1), equalToBatchIgnoringToken(singleEventBatch("0",
                "001-0001-000000000000000003", eventType.getName(), ImmutableMap.of("foo", "bar3"))));
    }


    @Test(timeout = 5000)
    public void whenNoEventsThenFirstOffsetIsBEGIN() {
        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "batch_flush_timeout=1")
                .start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.not(Matchers.empty())));
        assertThat(client.getBatches().get(0).getCursor().getOffset(), equalTo("001-0001--1"));
    }

    @Test(timeout = 5000)
    public void whenNoEventsThenBeginOffsetIsUsed() throws Exception {
        final TestStreamingClient client = TestStreamingClient
                .create(subscription.getId())
                .start();
        waitFor(() -> assertThat(client.getSessionId(), Matchers.not(equalTo(SESSION_ID_UNKNOWN))));

        when().get("/subscriptions/{sid}/cursors", subscription.getId())
                .then()
                .body("items[0].offset", equalTo("001-0001--1"));

        final int commitResult = commitCursors(subscription.getId(),
                ImmutableList.of(new SubscriptionCursor("0", Cursor.BEFORE_OLDEST_OFFSET, eventType.getName(), "abc")),
                client.getSessionId());
        assertThat(commitResult, equalTo(HttpStatus.SC_OK));
    }

    @Test(timeout = 5000)
    public void whenCommitVeryFirstEventThenOk() throws Exception {
        publishEvent(eventType.getName(), "{\"foo\":\"bar\"}");

        // create session, read from subscription and wait for events to be sent
        final TestStreamingClient client = TestStreamingClient
                .create(subscription.getId())
                .start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.not(Matchers.empty())));

        // commit and check that status is 204
        final int commitResult = commitCursors(subscription.getId(),
                ImmutableList.of(new SubscriptionCursor("0", "0", eventType.getName(), "token")),
                client.getSessionId());
        assertThat(commitResult, equalTo(HttpStatus.SC_NO_CONTENT));
    }

    @Test(timeout = 15000)
    public void whenWindowSizeIsSetItIsConsidered() throws Exception {

        publishEvents(eventType.getName(), 15, i -> "{\"foo\":\"bar\"}");

        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "max_uncommitted_events=5")
                .start();

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(5)));

        SubscriptionCursor cursorToCommit = client.getBatches().get(4).getCursor();
        commitCursors(subscription.getId(), ImmutableList.of(cursorToCommit), client.getSessionId());

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(10)));

        cursorToCommit = client.getBatches().get(6).getCursor();
        commitCursors(subscription.getId(), ImmutableList.of(cursorToCommit), client.getSessionId());

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(12)));
    }

    @Test(timeout = 15000)
    public void whenCommitTimeoutReachedSessionIsClosed() {

        publishEvent(eventType.getName(), "{\"foo\":\"bar\"}");

        final TestStreamingClient client = TestStreamingClient
                .create(subscription.getId()) // commit_timeout is 5 seconds for test
                .start();

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(2)), 10000);
        waitFor(() -> assertThat(client.isRunning(), is(false)), 10000);
        assertThat(client.getBatches().get(1), equalToBatchIgnoringToken(singleEventBatch("0",
                "001-0001-000000000000000000", eventType.getName(), ImmutableMap.of(), "Commit timeout reached")));
    }

    @Test(timeout = 15000)
    public void whenStreamTimeoutReachedSessionIsClosed() throws Exception {

        publishEvent(eventType.getName(), "{\"foo\":\"bar\"}");

        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "stream_timeout=3")
                .start();

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(1)));

        // to check that stream_timeout works we need to commit everything we consumed, in other case
        // Nakadi will first wait till commit_timeout exceeds
        final SubscriptionCursor lastBatchCursor = client.getBatches().get(client.getBatches().size() - 1).getCursor();
        commitCursors(subscription.getId(), ImmutableList.of(lastBatchCursor), client.getSessionId());

        waitFor(() -> assertThat(client.isRunning(), is(false)), 5000);
    }

    @Test(timeout = 10000)
    public void whenBatchLimitAndTimeoutAreSetTheyAreConsidered() {

        publishEvents(eventType.getName(), 12, i -> "{\"foo\":\"bar\"}");

        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "batch_limit=5&batch_flush_timeout=1&max_uncommitted_events=20")
                .start();

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(3)));

        assertThat(client.getBatches().get(0).getEvents(), Matchers.hasSize(5));
        assertThat(client.getBatches().get(0).getCursor().getOffset(), is("001-0001-000000000000000004"));

        assertThat(client.getBatches().get(1).getEvents(), Matchers.hasSize(5));
        assertThat(client.getBatches().get(1).getCursor().getOffset(), is("001-0001-000000000000000009"));

        assertThat(client.getBatches().get(2).getEvents(), Matchers.hasSize(2));
        assertThat(client.getBatches().get(2).getCursor().getOffset(), is("001-0001-000000000000000011"));
    }

    @Test(timeout = 10000)
    public void whenThereAreNoEmptySlotsThenConflict() {

        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "batch_flush_timeout=1");
        client.start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(1)));

        given()
                .get("/subscriptions/{id}/events", subscription.getId())
                .then()
                .statusCode(HttpStatus.SC_CONFLICT);
    }

    @Test(timeout = 10000)
    public void whenConnectionIsClosedByClientNakadiRecognizesIt() throws Exception {

        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "batch_flush_timeout=1");
        client.start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(1)));

        client.close();
        ThreadUtils.sleep(2500);

        final TestStreamingClient anotherClient = TestStreamingClient
                .create(URL, subscription.getId(), "batch_flush_timeout=1");
        anotherClient.start();
        // if we start to get data for another client it means that Nakadi recognized that first client closed
        // connection (in other case it would not allow second client to connect because of lack of slots)
        waitFor(() -> assertThat(anotherClient.getBatches(), Matchers.hasSize(1)));
    }

    @Test(timeout = 10000)
    public void testGetSubscriptionStat() throws Exception {
        publishEvents(eventType.getName(), 15, i -> "{\"foo\":\"bar\"}");

        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "max_uncommitted_events=20")
                .start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(15)));

        List<SubscriptionEventTypeStats> subscriptionStats =
                Collections.singletonList(new SubscriptionEventTypeStats(
                        eventType.getName(),
                        Collections.singletonList(new SubscriptionEventTypeStats.Partition(
                                "0",
                                "assigned",
                                15L,
                                null,
                                client.getSessionId(),
                                AUTO)))
                );
        NakadiTestUtils.getSubscriptionStat(subscription)
                .then()
                .content(new StringContains(JSON_TEST_HELPER.asJsonString(new ItemsWrapper<>(subscriptionStats))));

        final String partition = client.getBatches().get(0).getCursor().getPartition();
        final SubscriptionCursor cursor = new SubscriptionCursor(partition, "9", eventType.getName(), "token");
        commitCursors(subscription.getId(), ImmutableList.of(cursor), client.getSessionId());

        subscriptionStats =
                Collections.singletonList(new SubscriptionEventTypeStats(
                        eventType.getName(),
                        Collections.singletonList(new SubscriptionEventTypeStats.Partition(
                                "0",
                                "assigned",
                                5L,
                                null,
                                client.getSessionId(),
                                AUTO)))
                );
        NakadiTestUtils.getSubscriptionStat(subscription)
                .then()
                .content(new StringContains(JSON_TEST_HELPER.asJsonString(new ItemsWrapper<>(subscriptionStats))));
    }

    @Test(timeout = 10000)
    public void testGetSubscriptionStatWhenDirectAssignment() throws Exception {
        // connect with 1 stream directly requesting the partition
        final TestStreamingClient client = new TestStreamingClient(URL, subscription.getId(), "",
                Optional.empty(),
                Optional.of("{\"partitions\":[" +
                        "{\"event_type\":\"" + eventType.getName() + "\",\"partition\":\"0\"}]}"));
        client.start();
        // wait for rebalance to finish
        waitFor(() -> assertThat(getNumberOfAssignedStreams(subscription.getId()), Matchers.is(1)));

        NakadiTestUtils.getSubscriptionStat(subscription)
                .then()
                .content(new StringContains(JSON_TEST_HELPER.asJsonString(new SubscriptionEventTypeStats(
                        eventType.getName(),
                        Collections.singletonList(new SubscriptionEventTypeStats.Partition(
                                "0",
                                "assigned",
                                0L,
                                null,
                                client.getSessionId(),
                                DIRECT
                        ))))));
    }

    @Test
    public void testSubscriptionStatsMultiET() throws IOException {
        final List<EventType> eventTypes = Lists.newArrayList(createEventType(), createEventType());
        publishEvents(eventTypes.get(0).getName(), 10, i -> "{\"foo\":\"bar\"}");
        publishEvents(eventTypes.get(1).getName(), 20, i -> "{\"foo\":\"bar\"}");

        final Subscription subscription = createSubscription(RandomSubscriptionBuilder.builder()
                .withEventTypes(eventTypes.stream().map(EventType::getName).collect(Collectors.toSet()))
                .withStartFrom(END)
                .build());
        // client is needed only to initialize stats
        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "batch_flush_timeout=1")
                .start();

        waitFor(() -> assertThat(client.getBatches().isEmpty(), is(false)));

        publishEvents(eventTypes.get(0).getName(), 1, i -> "{\"foo\":\"bar\"}");
        publishEvents(eventTypes.get(1).getName(), 2, i -> "{\"foo\":\"bar\"}");

        NakadiTestUtils.getSubscriptionStat(subscription)
                .then()
                .content(new StringContains(JSON_TEST_HELPER.asJsonString(new SubscriptionEventTypeStats(
                        eventTypes.get(0).getName(),
                        Collections.singletonList(new SubscriptionEventTypeStats.Partition(
                                "0",
                                "assigned",
                                1L,
                                null,
                                client.getSessionId(),
                                AUTO
                        ))))))
                .content(new StringContains(JSON_TEST_HELPER.asJsonString(new SubscriptionEventTypeStats(
                        eventTypes.get(1).getName(),
                        Collections.singletonList(new SubscriptionEventTypeStats.Partition(
                                "0",
                                "assigned",
                                2L,
                                null,
                                client.getSessionId(),
                                AUTO
                        ))))));
        client.close();
    }

    @Test(timeout = 10000)
    public void whenConsumerIsBlocked403() throws Exception {
        SettingsControllerAT.blacklist(eventType.getName(), BlacklistService.Type.CONSUMER_ET);

        final TestStreamingClient client1 = TestStreamingClient
                .create(subscription.getId())
                .start();
        waitFor(() -> Assert.assertEquals(403, client1.getResponseCode()));

        SettingsControllerAT.whitelist(eventType.getName(), BlacklistService.Type.CONSUMER_ET);

        final TestStreamingClient client2 = TestStreamingClient
                .create(subscription.getId())
                .start();
        waitFor(() -> Assert.assertEquals(HttpStatus.SC_OK, client2.getResponseCode()));
    }

    @Test(timeout = 10000)
    public void whenConsumerIsBlockedDuringConsumption() throws Exception {
        publishEvents(eventType.getName(), 5, i -> "{\"foo\":\"bar\"}");
        final TestStreamingClient client = TestStreamingClient
                .create(subscription.getId())
                .start();
        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(5)));
        SettingsControllerAT.blacklist(eventType.getName(), BlacklistService.Type.CONSUMER_ET);

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(6)));

        Assert.assertEquals("Consumption is blocked",
                client.getBatches().get(client.getBatches().size() - 1).getMetadata().getDebug());
        SettingsControllerAT.whitelist(eventType.getName(), BlacklistService.Type.CONSUMER_ET);
    }

    @Test(timeout = 15000)
    public void whenStreamTimeout0ThenInfiniteStreaming() {
        publishEvents(eventType.getName(), 5, i -> "{\"foo\":\"bar\"}");
        final TestStreamingClient client = TestStreamingClient
                .create(URL, subscription.getId(), "stream_timeout=0")
                .start();

        waitFor(() -> assertThat(client.getBatches(), Matchers.hasSize(5)));
        Assert.assertFalse(client.getBatches().stream()
                .anyMatch(streamBatch -> streamBatch.getMetadata() != null
                        && streamBatch.getMetadata().getDebug().equals("Stream timeout reached")));
    }

    @Test(timeout = 15000)
    public void whenResetCursorsThenStreamFromResetCursorOffset() throws Exception {
        publishEvents(eventType.getName(), 20, i -> "{\"foo\":\"bar\"}");
        final TestStreamingClient client1 = TestStreamingClient
                .create(subscription.getId())
                .start();
        waitFor(() -> assertThat(client1.getBatches(), Matchers.hasSize(10)));

        int statusCode = commitCursors(
                subscription.getId(),
                Collections.singletonList(client1.getBatches().get(9).getCursor()),
                client1.getSessionId());
        Assert.assertEquals(HttpStatus.SC_NO_CONTENT, statusCode);

        final List<SubscriptionCursor> resetCursors =
                Collections.singletonList(client1.getBatches().get(4).getCursor());
        statusCode = given()
                .body(MAPPER.writeValueAsString(new ItemsWrapper<>(resetCursors)))
                .contentType(JSON)
                .patch("/subscriptions/{id}/cursors", subscription.getId())
                .getStatusCode();
        Assert.assertEquals(HttpStatus.SC_NO_CONTENT, statusCode);
        Assert.assertFalse(client1.isRunning());
        Assert.assertTrue(client1.getBatches().stream()
                .anyMatch(streamBatch -> streamBatch.getMetadata() != null
                        && streamBatch.getMetadata().getDebug().equals("Resetting subscription cursors")));


        final TestStreamingClient client2 = TestStreamingClient
                .create(subscription.getId())
                .start();
        waitFor(() -> assertThat(client2.getBatches(), Matchers.hasSize(10)));

        Assert.assertEquals("001-0001-000000000000000005", client2.getBatches().get(0).getCursor().getOffset());
    }

    @Test(timeout = 15000)
    public void whenPatchThenCursorsAreInitializedToDefault() throws Exception {
        final EventType et = createEventType();
        publishEvents(et.getName(), 10, i -> "{\"foo\": \"bar\"}");
        ThreadUtils.sleep(1000L);
        final Subscription s = createSubscription(RandomSubscriptionBuilder.builder()
                .withEventType(et.getName())
                .withStartFrom(END)
                .buildSubscriptionBase());
        given()
                .body(MAPPER.writeValueAsString(new ItemsWrapper<>(Collections.emptyList())))
                .contentType(JSON)
                .patch("/subscriptions/{id}/cursors", s.getId())
                .then()
                .statusCode(HttpStatus.SC_NO_CONTENT);

        final ItemsWrapper<SubscriptionCursor> subscriptionCursors = MAPPER.readValue(
                given().get("/subscriptions/{id}/cursors", s.getId()).getBody().asString(),
                new TypeReference<ItemsWrapper<SubscriptionCursor>>() {
                }
        );
        final List<EventTypePartitionView> etStats = MAPPER.readValue(
                given().get("/event-types/{et}/partitions", et.getName()).getBody().asString(),
                new TypeReference<List<EventTypePartitionView>>() {
                }
        );
        Assert.assertEquals(subscriptionCursors.getItems().size(), etStats.size());
        subscriptionCursors.getItems().forEach(sCursor -> {
            final boolean offsetSame = etStats.stream()
                    .anyMatch(ss -> ss.getPartitionId().equals(sCursor.getPartition()) &&
                            ss.getNewestAvailableOffset().equals(sCursor.getOffset()));
            // Check that after patch cursors are the same as END
            Assert.assertTrue(offsetSame);
        });
    }

    @Test(timeout = 15000)
    public void whenPatchThenCursorsAreInitializedAndPatched() throws Exception {
        final EventType et = createEventType();
        publishEvents(et.getName(), 10, i -> "{\"foo\": \"bar\"}");
        final List<EventTypePartitionView> etStats = MAPPER.readValue(
                given().get("/event-types/{et}/partitions", et.getName()).getBody().asString(),
                new TypeReference<List<EventTypePartitionView>>() {
                }
        );
        final EventTypePartitionView begin = etStats.get(0);
        final Subscription s = createSubscription(RandomSubscriptionBuilder.builder()
                .withEventType(et.getName())
                .withStartFrom(END)
                .buildSubscriptionBase());
        given()
                .body(MAPPER.writeValueAsString(new ItemsWrapper<>(Collections.singletonList(
                        new SubscriptionCursorWithoutToken(
                                et.getName(), begin.getPartitionId(), begin.getOldestAvailableOffset())
                ))))
                .contentType(JSON)
                .patch("/subscriptions/{id}/cursors", s.getId())
                .then()
                .statusCode(HttpStatus.SC_NO_CONTENT);

        final ItemsWrapper<SubscriptionCursor> subscriptionCursors = MAPPER.readValue(
                given().get("/subscriptions/{id}/cursors", s.getId()).getBody().asString(),
                new TypeReference<ItemsWrapper<SubscriptionCursor>>() {
                }
        );

        Assert.assertEquals(subscriptionCursors.getItems().size(), etStats.size());

        subscriptionCursors.getItems().forEach(item -> {
            if (item.getPartition().equals(begin.getPartitionId())) {
                Assert.assertEquals(begin.getOldestAvailableOffset(), item.getOffset());
            } else {
                Assert.assertEquals(begin.getNewestAvailableOffset(), item.getOffset());
            }
        });

    }
}