/**
 * Copyright 2016-2017 Sixt GmbH & Co. Autovermietung KG
 * 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.sixt.service.framework.servicetest.mockservice;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.protobuf.Message;
import com.sixt.service.framework.MethodHandlerDictionary;
import com.sixt.service.framework.ServiceProperties;
import com.sixt.service.framework.health.HealthCheckManager;
import com.sixt.service.framework.injection.ServiceRegistryModule;
import com.sixt.service.framework.kafka.KafkaPublisher;
import com.sixt.service.framework.kafka.KafkaPublisherFactory;
import com.sixt.service.framework.protobuf.ProtobufUtil;
import com.sixt.service.framework.registry.ServiceDiscoveryProvider;
import com.sixt.service.framework.registry.consul.RegistrationManager;
import com.sixt.service.framework.rpc.LoadBalancerFactory;
import com.sixt.service.framework.rpc.RpcClientFactory;
import com.sixt.service.framework.servicetest.injection.TestInjectionModule;
import com.sixt.service.framework.util.Sleeper;
import com.squareup.wire.schema.internal.parser.RpcMethodDefinition;
import com.squareup.wire.schema.internal.parser.RpcMethodScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

public class ServiceImpersonator {

    private static final Logger logger = LoggerFactory.getLogger(ServiceImpersonator.class);

    public static final int SLEEP_AFTER_PUBLISH = 500;

    protected String serviceName;
    protected RegistrationManager registrationManager;
    protected MessageHandler messageHandler;
    protected MethodHandlerDictionary methodHandlers;
    protected ServiceProperties serviceProperties;
    protected HealthCheckManager healthCheckManager;
    protected Injector injector;
    protected RpcMethodScanner rpcMethodScanner;
    protected KafkaPublisherFactory factory;
    private Map<String, KafkaPublisher> topicToPublisher = new HashMap<>();
    private int sleepAfterPublish = SLEEP_AFTER_PUBLISH;

    public ServiceImpersonator(String serviceName) throws Exception {
        //ServiceImpersonator needs its own injection stack so that each mock service
        //and service under servicetest get their own ecosystem
        this.serviceName = serviceName;
        TestInjectionModule testInjectionModule = new TestInjectionModule(serviceName);
        serviceProperties = testInjectionModule.getServiceProperties();
        serviceProperties.setServiceName(serviceName); //has to be before getting regMgr
        serviceProperties.setServiceInstanceId(UUID.randomUUID().toString());
        serviceProperties.addProperty("registry", "consul");
        injector = Guice.createInjector(testInjectionModule, new ServiceRegistryModule(serviceProperties));
        ServiceDiscoveryProvider provider = injector.getInstance(ServiceDiscoveryProvider.class);
        LoadBalancerFactory lbFactory = injector.getInstance(LoadBalancerFactory.class);
        lbFactory.initialize(provider);
        registrationManager = injector.getInstance(RegistrationManager.class);
        healthCheckManager = injector.getInstance(HealthCheckManager.class);
        rpcMethodScanner = new RpcMethodScanner(injector.getInstance(RpcClientFactory.class));
        methodHandlers = injector.getInstance(MethodHandlerDictionary.class);
        messageHandler = injector.getInstance(MessageHandler.class);
        factory = injector.getInstance(KafkaPublisherFactory.class);
        initialize();
    }

    public void setSleepAfterPublish(int sleepAfterPublish) {
        this.sleepAfterPublish = sleepAfterPublish;
    }

    private void initialize() throws Exception {
        buildMethodHandlers(serviceName.replaceAll("-", "_"));
        registrationManager.setRegisteredHandlers(methodHandlers.getMethodHandlers());
        registrationManager.register();
        messageHandler.setServiceName(serviceName);
        messageHandler.start();

        while(! registrationManager.isRegistered()) {
            logger.info("Waiting for service registration of {}", serviceName);
            new Sleeper().sleepNoException(100);
        }
        healthCheckManager.initialize();

        LoadBalancerFactory lbFactory = injector.getInstance(LoadBalancerFactory.class);
        lbFactory.getLoadBalancer(serviceName).waitForServiceInstance();
    }

    public void shutdown() {
        registrationManager.shutdown();
        messageHandler.shutdown();
    }

    public ServiceImpersonator addMapping(CommandResponseMapping mapping) {

        ServiceMethodProxy proxy = ((ServiceMethodProxy) this.methodHandlers.getMethodHandler(mapping.getCommand()));

        if (proxy == null) {
            throw new RuntimeException("The method " + mapping.getCommand() + " for " + this.serviceName + " could not be found");
        }
        logger.info("Adding mock response mapping for {}", mapping.getCommand());

        proxy.setResponse(mapping.getResponse());

        return this;
    }

    /**
     * Publish an event to kafka. We create one publisher per topic.  Uses a null key.
     *
     * @param topic The topic to publish under
     * @param event The event to publish
     */
    public void publishEvent(String topic, Message event) {
        publishEventWithKey(topic, null, event);
    }

    /**
     * Publish an event to kafka. We create one publisher per topic.
     *
     * @param topic The topic to publish under
     * @param key   The key for the event
     * @param event The event to publish
     */
    public void publishEventWithKey(String topic, String key, Message event) {

        KafkaPublisher publisher = topicToPublisher.get(topic);

        if (publisher == null) {
            publisher = factory.newBuilder(topic).build();
            topicToPublisher.put(topic, publisher);
        }

        String jsonEvent = ProtobufUtil.protobufToJson(event).toString();
        boolean isPublished = publisher.publishSyncWithKey(key, jsonEvent);

        if (isPublished){
            logger.info("Published event: {}", jsonEvent);
            new Sleeper().sleepNoException(sleepAfterPublish);
        } else{
            logger.warn("Publishing event message {} to Kafka topic {} failed", jsonEvent, topic);
        }

    }

    /**
     * Get counter how often a service method was called
     *
     * @param serviceMethodName The service method name e.g. Telematics.LockVehicle
     * @return Number of calls of a service method
     */
    public int getServiceMethodCallCount(String serviceMethodName) throws Exception {
        ServiceMethodProxy serviceMethodProxy = (ServiceMethodProxy) methodHandlers.getMethodHandler(serviceMethodName);
        if(serviceMethodProxy == null){
            throw new Exception("No method handler found for service method '" + serviceMethodName + "'");
        }

        return serviceMethodProxy.getMethodCallCounter();
    }

    /**
     * Reset counter how often a service method was called to 0
     *
     * @param serviceMethodName The service method name e.g. Telematics.LockVehicle
     */
    public void resetServiceMethodCallCount(String serviceMethodName) throws Exception {
        ServiceMethodProxy serviceMethodProxy = (ServiceMethodProxy) methodHandlers.getMethodHandler(serviceMethodName);
        if(serviceMethodProxy == null){
            throw new Exception("No method handler found for service method '" + serviceMethodName + "'");
        }

        serviceMethodProxy.resetMethodCallCounter();
    }

    private void buildMethodHandlers(String serviceName) throws Exception {
        // get the generated proto classes
        List<String> protoClasses = rpcMethodScanner.getGeneratedProtoClasses(serviceName);
        List<RpcMethodDefinition> defs = rpcMethodScanner.getRpcMethodDefinitions(serviceName);

        for (RpcMethodDefinition def : defs) {
            ServiceMethodProxy handler = buildMethodHandler(def, protoClasses);
            methodHandlers.put(def.getMethodName(), handler);
            logger.info("Adding mock method handler for {}", def.getMethodName());
        }
    }

    private ServiceMethodProxy buildMethodHandler(RpcMethodDefinition def, List<String> protoClasses)
            throws ClassNotFoundException {
        Class<?> requestClass = rpcMethodScanner.findProtobufClass(protoClasses, def.getRequestType());
        Class<?> responseClass = rpcMethodScanner.findProtobufClass(protoClasses, def.getResponseType());
        return new ServiceMethodProxy(requestClass, responseClass);
    }
}

@@ -1003,32 +1003,91 @@
 RegistryModule;%0A
+import com.sixt.service.framework.injection.TracingModule;%0A
 import com.sixt.
@@ -3406,16 +3406,54 @@
 perties)
+, new TracingModule(serviceProperties)
 );%0A     

Add tracing module to service impersonator