/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.dubbo.rpc.cluster.loadbalance;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.utils.JsonUtils;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcInvocation;
import org.apache.dubbo.rpc.RpcStatus;
import org.apache.dubbo.rpc.cluster.LoadBalance;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import static org.apache.dubbo.rpc.cluster.Constants.DEFAULT_WARMUP;
import static org.apache.dubbo.rpc.cluster.Constants.DEFAULT_WEIGHT;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;

/**
 * RoundRobinLoadBalanceTest
 */
@SuppressWarnings({"unchecked", "rawtypes"})
class LoadBalanceBaseTest {
    Invocation invocation;
    Invocation genericInvocation;
    List<Invoker<LoadBalanceBaseTest>> invokers = new ArrayList<Invoker<LoadBalanceBaseTest>>();
    Invoker<LoadBalanceBaseTest> invoker1;
    Invoker<LoadBalanceBaseTest> invoker2;
    Invoker<LoadBalanceBaseTest> invoker3;
    Invoker<LoadBalanceBaseTest> invoker4;
    Invoker<LoadBalanceBaseTest> invoker5;

    RpcStatus weightTestRpcStatus1;
    RpcStatus weightTestRpcStatus2;
    RpcStatus weightTestRpcStatus3;
    RpcStatus weightTestRpcStatus5;

    RpcInvocation weightTestInvocation;

    /**
     * @throws java.lang.Exception
     */
    @BeforeAll
    public static void setUpBeforeClass() throws Exception {
    }

    /**
     * @throws java.lang.Exception
     */
    @BeforeEach
    public void setUp() throws Exception {

        invocation = mock(Invocation.class);
        given(invocation.getMethodName()).willReturn("method1");
        given(invocation.getArguments()).willReturn(new Object[] {"arg1","arg2","arg3"});

        genericInvocation = mock(Invocation.class);
        String methodName = "method1";
        given(genericInvocation.getMethodName()).willReturn("$invoke");
        String[] paraTypes = new String[] {String.class.getName(),String.class.getName(),String.class.getName()};
        Object[] argsObject = new Object[] {"arg1","arg2","arg3"};
        Object[] args = new Object[] {methodName,paraTypes,argsObject};
        given(genericInvocation.getArguments()).willReturn(args);

        invoker1 = mock(Invoker.class);
        invoker2 = mock(Invoker.class);
        invoker3 = mock(Invoker.class);
        invoker4 = mock(Invoker.class);
        invoker5 = mock(Invoker.class);

        URL url1 = URL.valueOf("test://127.0.0.1:1/DemoService");
        URL url2 = URL.valueOf("test://127.0.0.1:2/DemoService");
        URL url3 = URL.valueOf("test://127.0.0.1:3/DemoService");
        URL url4 = URL.valueOf("test://127.0.0.1:4/DemoService");
        URL url5 = URL.valueOf("test://127.0.0.1:5/DemoService");

        given(invoker1.isAvailable()).willReturn(true);
        given(invoker1.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(invoker1.getUrl()).willReturn(url1);

        given(invoker2.isAvailable()).willReturn(true);
        given(invoker2.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(invoker2.getUrl()).willReturn(url2);

        given(invoker3.isAvailable()).willReturn(true);
        given(invoker3.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(invoker3.getUrl()).willReturn(url3);

        given(invoker4.isAvailable()).willReturn(true);
        given(invoker4.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(invoker4.getUrl()).willReturn(url4);

        given(invoker5.isAvailable()).willReturn(true);
        given(invoker5.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(invoker5.getUrl()).willReturn(url5);

        invokers.add(invoker1);
        invokers.add(invoker2);
        invokers.add(invoker3);
        invokers.add(invoker4);
        invokers.add(invoker5);
    }

    public Map<Invoker, AtomicLong> getInvokeCounter(int runs, String loadbalanceName) {
        Map<Invoker, AtomicLong> counter = new ConcurrentHashMap<Invoker, AtomicLong>();
        LoadBalance lb = getLoadBalance(loadbalanceName);
        for (Invoker invoker : invokers) {
            counter.put(invoker, new AtomicLong(0));
        }
        URL url = invokers.get(0).getUrl();
        for (int i = 0; i < runs; i++) {
            Invoker sinvoker = lb.select(invokers, url, invocation);
            counter.get(sinvoker).incrementAndGet();
        }
        return counter;
    }

    public Map<Invoker, AtomicLong> getGenericInvokeCounter(int runs, String loadbalanceName) {
        Map<Invoker, AtomicLong> counter = new ConcurrentHashMap<Invoker, AtomicLong>();
        LoadBalance lb = getLoadBalance(loadbalanceName);
        for (Invoker invoker : invokers) {
            counter.put(invoker, new AtomicLong(0));
        }
        URL url = invokers.get(0).getUrl();
        for (int i = 0; i < runs; i++) {
            Invoker sinvoker = lb.select(invokers, url, genericInvocation);
            counter.get(sinvoker).incrementAndGet();
        }
        return counter;
    }

    protected AbstractLoadBalance getLoadBalance(String loadbalanceName) {
        return (AbstractLoadBalance) ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);
    }

    @Test
    void testLoadBalanceWarmup() {
        Assertions.assertEquals(1, calculateDefaultWarmupWeight(0));
        Assertions.assertEquals(1, calculateDefaultWarmupWeight(13));
        Assertions.assertEquals(1, calculateDefaultWarmupWeight(6 * 1000));
        Assertions.assertEquals(2, calculateDefaultWarmupWeight(12 * 1000));
        Assertions.assertEquals(10, calculateDefaultWarmupWeight(60 * 1000));
        Assertions.assertEquals(50, calculateDefaultWarmupWeight(5 * 60 * 1000));
        Assertions.assertEquals(50, calculateDefaultWarmupWeight(5 * 60 * 1000 + 23));
        Assertions.assertEquals(50, calculateDefaultWarmupWeight(5 * 60 * 1000 + 5999));
        Assertions.assertEquals(51, calculateDefaultWarmupWeight(5 * 60 * 1000 + 6000));
        Assertions.assertEquals(90, calculateDefaultWarmupWeight(9 * 60 * 1000));
        Assertions.assertEquals(98, calculateDefaultWarmupWeight(10 * 60 * 1000 - 12 * 1000));
        Assertions.assertEquals(99, calculateDefaultWarmupWeight(10 * 60 * 1000 - 6 * 1000));
        Assertions.assertEquals(100, calculateDefaultWarmupWeight(10 * 60 * 1000));
        Assertions.assertEquals(100, calculateDefaultWarmupWeight(20 * 60 * 1000));
    }

    /**
     * handle default data
     *
     * @return
     */
    private static int calculateDefaultWarmupWeight(int uptime) {
        return AbstractLoadBalance.calculateWarmupWeight(uptime, DEFAULT_WARMUP, DEFAULT_WEIGHT);
    }

    /*------------------------------------test invokers for weight---------------------------------------*/

    protected static class InvokeResult {
        private AtomicLong count = new AtomicLong();
        private int weight = 0;
        private int totalWeight = 0;

        public InvokeResult(int weight) {
            this.weight = weight;
        }

        public AtomicLong getCount() {
            return count;
        }

        public int getWeight() {
            return weight;
        }

        public int getTotalWeight() {
            return totalWeight;
        }

        public void setTotalWeight(int totalWeight) {
            this.totalWeight = totalWeight;
        }

        public int getExpected(int runCount) {
            return getWeight() * runCount / getTotalWeight();
        }

        public float getDeltaPercentage(int runCount) {
            int expected = getExpected(runCount);
            return Math.abs((expected - getCount().get()) * 100.0f / expected);
        }

        @Override
        public String toString() {
            return JsonUtils.toJson(this);
        }
    }

    protected List<Invoker<LoadBalanceBaseTest>> weightInvokers = new ArrayList<Invoker<LoadBalanceBaseTest>>();
    protected List<Invoker<LoadBalanceBaseTest>> weightInvokersSR= new ArrayList<Invoker<LoadBalanceBaseTest>>();

    protected Invoker<LoadBalanceBaseTest> weightInvoker1;
    protected Invoker<LoadBalanceBaseTest> weightInvoker2;
    protected Invoker<LoadBalanceBaseTest> weightInvoker3;
    protected Invoker<LoadBalanceBaseTest> weightInvokerTmp;
    protected Invoker<LoadBalanceBaseTest> weightInvoker5;

    @BeforeEach
    public void before() throws Exception {
        weightInvoker1 = mock(Invoker.class, Mockito.withSettings().stubOnly());
        weightInvoker2 = mock(Invoker.class, Mockito.withSettings().stubOnly());
        weightInvoker3 = mock(Invoker.class, Mockito.withSettings().stubOnly());
        weightInvokerTmp = mock(Invoker.class, Mockito.withSettings().stubOnly());
        weightInvoker5 = mock(Invoker.class, Mockito.withSettings().stubOnly());

        weightTestInvocation = new RpcInvocation();
        weightTestInvocation.setMethodName("test");

        URL url1 = URL.valueOf("test1://127.0.0.1:11/DemoService?weight=1&active=0");
        URL url2 = URL.valueOf("test2://127.0.0.1:12/DemoService?weight=9&active=0");
        URL url3 = URL.valueOf("test3://127.0.0.1:13/DemoService?weight=6&active=1");
        URL urlTmp = URL.valueOf("test4://127.0.0.1:9999/DemoService?weight=11&active=0");
        URL url5 = URL.valueOf("test5://127.0.0.1:15/DemoService?weight=15&active=0");

        given(weightInvoker1.isAvailable()).willReturn(true);
        given(weightInvoker1.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(weightInvoker1.getUrl()).willReturn(url1);

        given(weightInvoker2.isAvailable()).willReturn(true);
        given(weightInvoker2.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(weightInvoker2.getUrl()).willReturn(url2);

        given(weightInvoker3.isAvailable()).willReturn(true);
        given(weightInvoker3.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(weightInvoker3.getUrl()).willReturn(url3);

        given(weightInvokerTmp.isAvailable()).willReturn(true);
        given(weightInvokerTmp.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(weightInvokerTmp.getUrl()).willReturn(urlTmp);

        given(weightInvoker5.isAvailable()).willReturn(true);
        given(weightInvoker5.getInterface()).willReturn(LoadBalanceBaseTest.class);
        given(weightInvoker5.getUrl()).willReturn(url5);

        weightInvokers.add(weightInvoker1);
        weightInvokers.add(weightInvoker2);
        weightInvokers.add(weightInvoker3);

        weightInvokersSR.add(weightInvoker1);
        weightInvokersSR.add(weightInvoker2);
        weightInvokersSR.add(weightInvoker5);

        weightTestRpcStatus1 = RpcStatus.getStatus(weightInvoker1.getUrl(), weightTestInvocation.getMethodName());
        weightTestRpcStatus2 = RpcStatus.getStatus(weightInvoker2.getUrl(), weightTestInvocation.getMethodName());
        weightTestRpcStatus3 = RpcStatus.getStatus(weightInvoker3.getUrl(), weightTestInvocation.getMethodName());
        weightTestRpcStatus5 = RpcStatus.getStatus(weightInvoker5.getUrl(), weightTestInvocation.getMethodName());


        // weightTestRpcStatus3 active is 1
        RpcStatus.beginCount(weightInvoker3.getUrl(), weightTestInvocation.getMethodName());

        // weightTestRpcStatus5 shortest response time of success calls is bigger than 0
        // weightTestRpcStatus5 active is 1
        RpcStatus.beginCount(weightInvoker5.getUrl(), weightTestInvocation.getMethodName());
        RpcStatus.endCount(weightInvoker5.getUrl(), weightTestInvocation.getMethodName(), 5000L, true);
        RpcStatus.beginCount(weightInvoker5.getUrl(), weightTestInvocation.getMethodName());
    }

    protected Map<Invoker, InvokeResult> getWeightedInvokeResult(int runs, String loadbalanceName) {
        Map<Invoker, InvokeResult> counter = new ConcurrentHashMap<Invoker, InvokeResult>();
        AbstractLoadBalance lb = getLoadBalance(loadbalanceName);
        int totalWeight = 0;
        for (int i = 0; i < weightInvokers.size(); i ++) {
            InvokeResult invokeResult = new InvokeResult(lb.getWeight(weightInvokers.get(i), weightTestInvocation));
            counter.put(weightInvokers.get(i), invokeResult);
            totalWeight += invokeResult.getWeight();
        }
        for (InvokeResult invokeResult : counter.values()) {
            invokeResult.setTotalWeight(totalWeight);
        }
        URL url = weightInvokers.get(0).getUrl();
        for (int i = 0; i < runs; i++) {
            Invoker sinvoker = lb.select(weightInvokers, url, weightTestInvocation);
            counter.get(sinvoker).getCount().incrementAndGet();
        }
        return counter;
    }

}
