/*
 *    Copyright (C) 2016 Mesosphere, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mesosphere.mesos.rx.java.example.framework.sleepy;

import com.google.protobuf.ByteString;
import com.mesosphere.mesos.rx.java.protobuf.ProtobufMessageCodecs;
import com.mesosphere.mesos.rx.java.protobuf.SchedulerCalls;
import com.mesosphere.mesos.rx.java.test.Async;
import com.mesosphere.mesos.rx.java.test.simulation.MesosServerSimulation;
import org.apache.mesos.v1.scheduler.Protos;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import rx.subjects.BehaviorSubject;

import java.net.URI;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import static com.mesosphere.mesos.rx.java.protobuf.SchedulerEvents.*;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.mesos.v1.Protos.TaskState.TASK_RUNNING;
import static org.assertj.core.api.Assertions.assertThat;

public final class SleepySimulationTest {

    @NotNull
    private static final Protos.Event HEARTBEAT = Protos.Event.newBuilder()
        .setType(Protos.Event.Type.HEARTBEAT)
        .build();

    @Rule
    public Timeout timeoutRule = new Timeout(15_000, MILLISECONDS);

    @Rule
    public Async async = new Async();

    private BehaviorSubject<Protos.Event> subject;
    private MesosServerSimulation<Protos.Event, Protos.Call> sim;

    private URI uri;

    @Before
    public void setUp() throws Exception {
        subject = BehaviorSubject.create();
        sim = new MesosServerSimulation<>(
            subject,
            ProtobufMessageCodecs.SCHEDULER_EVENT,
            ProtobufMessageCodecs.SCHEDULER_CALL,
            (e) -> e.getType() == Protos.Call.Type.SUBSCRIBE
        );
        final int serverPort = sim.start();
        uri = URI.create(String.format("http://localhost:%d/api/v1/scheduler", serverPort));
    }

    @After
    public void tearDown() throws Exception {
        sim.shutdown();
    }

    @Test
    public void offerSimulation() throws Throwable {
        final String fwId = "sleepy-" + UUID.randomUUID();
        final org.apache.mesos.v1.Protos.FrameworkID frameworkID = org.apache.mesos.v1.Protos.FrameworkID.newBuilder()
            .setValue(fwId)
            .build();
        final Protos.Call subscribe = SchedulerCalls.subscribe(
            frameworkID,
            org.apache.mesos.v1.Protos.FrameworkInfo.newBuilder()
                .setId(frameworkID)
                .setUser("root")
                .setName("sleepy")
                .setFailoverTimeout(0)
                .setRole("*")
                .build()
        );

        async.run("sleepy-framework", () -> Sleepy._main(fwId, uri.toString(), "1", "*"));

        subject.onNext(subscribed(fwId, 15));
        sim.awaitSubscribeCall();
        final List<Protos.Call> callsReceived1 = sim.getCallsReceived();
        assertThat(callsReceived1).hasSize(1);
        assertThat(callsReceived1.get(0)).isEqualTo(subscribe);

        // send a heartbeat
        subject.onNext(HEARTBEAT);
        // send an offer
        subject.onNext(resourceOffer("host-1", "offer-1", "agent-1", fwId, 4, 16 * 1024, 100 * 1024));

        sim.awaitCall();  // wait for accept to reach the server
        final List<Protos.Call> callsReceived2 = sim.getCallsReceived();
        assertThat(callsReceived2).hasSize(2);
        final Protos.Call.Accept accept = callsReceived2.get(1).getAccept();
        assertThat(accept).isNotNull();
        assertThat(
            accept.getOfferIdsList().stream()
                .map(org.apache.mesos.v1.Protos.OfferID::getValue)
                .collect(Collectors.toList())
        ).isEqualTo(newArrayList("offer-1"));
        final List<org.apache.mesos.v1.Protos.TaskInfo> taskInfos = accept.getOperationsList().stream()
            .map(org.apache.mesos.v1.Protos.Offer.Operation::getLaunch)
            .flatMap(l -> l.getTaskInfosList().stream())
            .collect(Collectors.toList());
        assertThat(taskInfos).hasSize(4);
        assertThat(
            taskInfos.stream()
                .map(t -> t.getTaskId().getValue())
                .collect(Collectors.toSet())
        ).isEqualTo(newHashSet("task-1-1", "task-1-2", "task-1-3", "task-1-4"));

        // send task status updates
        final ByteString uuid1 = ByteString.copyFromUtf8(UUID.randomUUID().toString());
        final ByteString uuid2 = ByteString.copyFromUtf8(UUID.randomUUID().toString());
        final ByteString uuid3 = ByteString.copyFromUtf8(UUID.randomUUID().toString());
        final ByteString uuid4 = ByteString.copyFromUtf8(UUID.randomUUID().toString());
        subject.onNext(update("agent-1", "task-1-1", "task-1-1", TASK_RUNNING, uuid1));
        subject.onNext(update("agent-1", "task-1-2", "task-1-2", TASK_RUNNING, uuid2));
        subject.onNext(update("agent-1", "task-1-3", "task-1-3", TASK_RUNNING, uuid3));
        subject.onNext(update("agent-1", "task-1-4", "task-1-4", TASK_RUNNING, uuid4));

        // wait for the task status updates to be ack'd
        sim.awaitCall(4);
        final List<Protos.Call> callsReceived3 = sim.getCallsReceived();
        assertThat(callsReceived3).hasSize(6);
        final List<Protos.Call> ackCalls = callsReceived3.subList(2, 6);
        final Set<ByteString> ackdUuids = ackCalls.stream()
            .map(c -> c.getAcknowledge().getUuid())
            .collect(Collectors.toSet());

        assertThat(ackdUuids).isEqualTo(newHashSet(uuid1, uuid2, uuid3, uuid4));

        // send another offer with too little cpu for a task to run
        subject.onNext(resourceOffer("host-1", "offer-2", "agent-1", fwId, 0.9, 15 * 1024, 100 * 1024));

        // wait for the decline of the offer
        sim.awaitCall();
        final List<Protos.Call> callsReceived4 = sim.getCallsReceived();
        assertThat(callsReceived4).hasSize(7);
        final Protos.Call.Decline decline = callsReceived4.get(6).getDecline();
        assertThat(
            decline.getOfferIdsList().stream()
                .map(org.apache.mesos.v1.Protos.OfferID::getValue)
                .collect(Collectors.toList())
        ).isEqualTo(newArrayList("offer-2"));

        subject.onCompleted();
        sim.awaitSendingEvents();
    }

}