/*
 * 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.ignite.internal.client.thin;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.ignite.Ignite;
import org.apache.ignite.client.ClientClusterGroup;
import org.apache.ignite.client.ClientException;
import org.apache.ignite.client.IgniteClient;
import org.apache.ignite.client.Person;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.resources.IgniteInstanceResource;
import org.apache.ignite.services.Service;
import org.apache.ignite.services.ServiceContext;
import org.apache.ignite.testframework.GridTestUtils;
import org.junit.Test;

/**
 * Checks service invocation for thin client.
 */
public class ServicesTest extends AbstractThinClientTest {
    /** Node-id service name. */
    private static final String NODE_ID_SERVICE_NAME = "node_id_svc";

    /** Node-singleton service name. */
    private static final String NODE_SINGLTON_SERVICE_NAME = "node_svc";

    /** Cluster-singleton service name. */
    private static final String CLUSTER_SINGLTON_SERVICE_NAME = "cluster_svc";

    /** {@inheritDoc} */
    @Override protected void beforeTestsStarted() throws Exception {
        super.beforeTestsStarted();

        startGrids(3);

        startClientGrid(3);

        grid(0).createCache(DEFAULT_CACHE_NAME);

        awaitPartitionMapExchange();

        grid(0).services().deployNodeSingleton(NODE_ID_SERVICE_NAME, new TestNodeIdService());

        grid(0).services().deployNodeSingleton(NODE_SINGLTON_SERVICE_NAME, new TestService());

        // Deploy CLUSTER_SINGLTON_SERVICE_NAME to grid(1).
        int keyGrid1 = primaryKey(grid(1).cache(DEFAULT_CACHE_NAME));

        grid(0).services().deployKeyAffinitySingleton(CLUSTER_SINGLTON_SERVICE_NAME, new TestService(),
            DEFAULT_CACHE_NAME, keyGrid1);
    }

    /**
     * Test that overloaded methods resolved correctly.
     */
    @Test
    public void testOverloadedMethods() throws Exception {
        try (IgniteClient client = startClient(0)) {
            // Test local service calls (service deployed to each node).
            TestServiceInterface svc = client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
                TestServiceInterface.class);

            checkOverloadedMethods(svc);

            // Test remote service calls (client connected to grid(0) but service deployed to grid(1)).
            svc = client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME, TestServiceInterface.class);

            checkOverloadedMethods(svc);
        }
    }

    /**
     * @param svc Service.
     */
    private void checkOverloadedMethods(TestServiceInterface svc) {
        assertEquals("testMethod()", svc.testMethod());

        assertEquals("testMethod(String val): test", svc.testMethod("test"));

        assertEquals(123, svc.testMethod(123));

        assertEquals("testMethod(Object val): test", svc.testMethod(new StringBuilder("test")));

        assertEquals("testMethod(String val): null", svc.testMethod((String)null));

        assertEquals("testMethod(Object val): null", svc.testMethod((Object)null));

        Person person1 = new Person(1, "Person 1");
        Person person2 = new Person(2, "Person 2");

        assertEquals("testMethod(Person person, Object obj): " + person1 + ", " + person2,
            svc.testMethod(person1, (Object)person2));

        assertEquals("testMethod(Object obj, Person person): " + person1 + ", " + person2,
            svc.testMethod((Object)person1, person2));
    }

    /**
     * Test that methods which get and return collections work correctly.
     */
    @Test
    public void testCollectionMethods() throws Exception {
        try (IgniteClient client = startClient(0)) {
            // Test local service calls (service deployed to each node).
            TestServiceInterface svc = client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
                TestServiceInterface.class);

            checkCollectionMethods(svc);

            // Test remote service calls (client connected to grid(0) but service deployed to grid(1)).
            svc = client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME, TestServiceInterface.class);

            checkCollectionMethods(svc);
        }
    }

    /**
     * @param svc Service.
     */
    private void checkCollectionMethods(TestServiceInterface svc) {
        Person person1 = new Person(1, "Person 1");
        Person person2 = new Person(2, "Person 2");

        Person[] arr = new Person[] {person1, person2};
        assertTrue(Arrays.equals(arr, svc.testArray(arr)));

        Collection<Person> col = new HashSet<>(F.asList(person1, person2));
        assertEquals(col, svc.testCollection(col));

        Map<Integer, Person> map = F.asMap(1, person1, 2, person2);
        assertEquals(map, svc.testMap(map));
    }

    /**
     * Test that exception is thrown when invoking service with wrong interface.
     */
    @Test
    public void testWrongMethodInvocation() throws Exception {
        try (IgniteClient client = startClient(0)) {
            TestServiceInterface svc = client.services().serviceProxy(NODE_ID_SERVICE_NAME,
                TestServiceInterface.class);

            GridTestUtils.assertThrowsAnyCause(log, () -> svc.testMethod(0), ClientException.class,
                "Method not found");
        }
    }

    /**
     * Test that exception is thrown when trying to invoke non-existing service.
     */
    @Test
    public void testWrongServiceName() throws Exception {
        try (IgniteClient client = startClient(0)) {
            TestServiceInterface svc = client.services().serviceProxy("no_such_service",
                TestServiceInterface.class);

            GridTestUtils.assertThrowsAnyCause(log, () -> svc.testMethod(0), ClientException.class,
                "Service not found");
        }
    }

    /**
     * Test that service exception message is propagated to client.
     */
    @Test
    public void testServiceException() throws Exception {
        try (IgniteClient client = startClient(0)) {
            // Test local service calls (service deployed to each node).
            TestServiceInterface svc = client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
                TestServiceInterface.class);

            GridTestUtils.assertThrowsAnyCause(log, svc::testException, ClientException.class,
                "testException()");

            // Test remote service calls (client connected to grid(0) but service deployed to grid(1)).
            client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME, TestServiceInterface.class);

            GridTestUtils.assertThrowsAnyCause(log, svc::testException, ClientException.class,
                "testException()");
        }
    }

    /**
     * Test that services executed on cluster group.
     */
    @Test
    public void testServicesOnClusterGroup() throws Exception {
        try (IgniteClient client = startClient(0)) {
            // Local node.
            ClientClusterGroup grp = client.cluster().forNodeId(nodeId(0), nodeId(3));

            TestNodeIdServiceInterface nodeSvc0 = client.services(grp).serviceProxy(NODE_ID_SERVICE_NAME,
                TestNodeIdServiceInterface.class);

            assertEquals(nodeId(0), nodeSvc0.nodeId());

            // Remote node.
            grp = client.cluster().forNodeId(nodeId(1), nodeId(3));

            nodeSvc0 = client.services(grp).serviceProxy(NODE_ID_SERVICE_NAME,
                TestNodeIdServiceInterface.class);

            assertEquals(nodeId(1), nodeSvc0.nodeId());

            // Client node.
            grp = client.cluster().forNodeId(nodeId(3));

            TestNodeIdServiceInterface nodeSvc1 = client.services(grp).serviceProxy(NODE_ID_SERVICE_NAME,
                TestNodeIdServiceInterface.class);

            GridTestUtils.assertThrowsAnyCause(log, nodeSvc1::nodeId, ClientException.class,
                "Failed to find deployed service");

            // All nodes, except service node.
            grp = client.cluster().forNodeId(nodeId(0), nodeId(2), nodeId(3));

            TestServiceInterface nodeSvc2 = client.services(grp).serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME,
                TestServiceInterface.class);

            GridTestUtils.assertThrowsAnyCause(log, nodeSvc2::testMethod, ClientException.class,
                "Failed to find deployed service");
        }
    }

    /**
     * Test services timeout.
     */
    @Test
    public void testServiceTimeout() throws Exception {
        long timeout = 100L;

        try (IgniteClient client = startClient(0)) {
            TestServiceInterface svc = client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME,
                TestServiceInterface.class, timeout);

            TestService.latch = new CountDownLatch(1);

            GridTestUtils.assertThrowsAnyCause(log, svc::waitLatch, ClientException.class, "timed out");
        }
        finally {
            if (TestService.latch != null) {
                TestService.latch.countDown();
                TestService.latch = null;
            }
        }
    }

    /** */
    public static interface TestServiceInterface {
        /** */
        public String testMethod();

        /** */
        public String testMethod(String val);

        /** */
        public String testMethod(Object val);

        /** */
        public int testMethod(int val);

        /** */
        public String testMethod(Person person, Object obj);

        /** */
        public String testMethod(Object obj, Person person);

        /** */
        public Person[] testArray(Person[] persons);

        /** */
        public Collection<Person> testCollection(Collection<Person> persons);

        /** */
        public Map<Integer, Person> testMap(Map<Integer, Person> persons);

        /** */
        public Object testException();

        /** */
        public boolean waitLatch() throws Exception;
    }

    /**
     * Implementation of TestServiceInterface.
     */
    public static class TestService implements Service, TestServiceInterface {
        /** Latch. */
        public static CountDownLatch latch;

        /** {@inheritDoc} */
        @Override public void cancel(ServiceContext ctx) {
            // No-op.
        }

        /** {@inheritDoc} */
        @Override public void init(ServiceContext ctx) throws Exception {
            // No-op.
        }

        /** {@inheritDoc} */
        @Override public void execute(ServiceContext ctx) throws Exception {
            // No-op.
        }

        /** {@inheritDoc} */
        @Override public String testMethod() {
            return "testMethod()";
        }

        /** {@inheritDoc} */
        @Override public String testMethod(String val) {
            return "testMethod(String val): " + val;
        }

        /** {@inheritDoc} */
        @Override public String testMethod(Object val) {
            return "testMethod(Object val): " + val;
        }

        /** {@inheritDoc} */
        @Override public int testMethod(int val) {
            return val;
        }

        /** {@inheritDoc} */
        @Override public String testMethod(Person person, Object obj) {
            return "testMethod(Person person, Object obj): " + person + ", " + obj;
        }

        /** {@inheritDoc} */
        @Override public String testMethod(Object obj, Person person) {
            return "testMethod(Object obj, Person person): " + obj + ", " + person;
        }

        /** {@inheritDoc} */
        @Override public Person[] testArray(Person[] persons) {
            return persons;
        }

        /** {@inheritDoc} */
        @Override public Collection<Person> testCollection(Collection<Person> persons) {
            return persons;
        }

        /** {@inheritDoc} */
        @Override public Map<Integer, Person> testMap(Map<Integer, Person> persons) {
            return persons;
        }

        /** {@inheritDoc} */
        @Override public Object testException() {
            throw new IllegalStateException("testException()");
        }

        /** {@inheritDoc} */
        @Override public boolean waitLatch() throws Exception {
            latch.await(10L, TimeUnit.SECONDS);

            return true;
        }
    }

    /**
     * Service to return ID of node where method was executed.
     */
    public static interface TestNodeIdServiceInterface {
        /** Gets ID of node where method was executed */
        public UUID nodeId();
    }

    /**
     * Implementation of TestNodeIdServiceInterface
     */
    public static class TestNodeIdService implements Service, TestNodeIdServiceInterface {
        /** Ignite. */
        @IgniteInstanceResource
        Ignite ignite;

        /** {@inheritDoc} */
        @Override public void cancel(ServiceContext ctx) {
            // No-op.
        }

        /** {@inheritDoc} */
        @Override public void init(ServiceContext ctx) throws Exception {
            // No-op.
        }

        /** {@inheritDoc} */
        @Override public void execute(ServiceContext ctx) throws Exception {
            // No-op.
        }

        /** {@inheritDoc} */
        @Override public UUID nodeId() {
            return ignite.cluster().localNode().id();
        }
    }
}
