/*
 * Copyright 2017 HugeGraph Authors
 *
 * 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 com.baidu.hugegraph.api.traverser;

import java.util.ArrayList;
import java.util.List;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import com.baidu.hugegraph.api.BaseApiTest;
import com.baidu.hugegraph.driver.GraphManager;
import com.baidu.hugegraph.driver.SchemaManager;
import com.baidu.hugegraph.exception.ServerException;
import com.baidu.hugegraph.structure.constant.Direction;
import com.baidu.hugegraph.structure.constant.T;
import com.baidu.hugegraph.structure.graph.Vertex;
import com.baidu.hugegraph.structure.traverser.Ranks;
import com.baidu.hugegraph.testutil.Assert;
import com.google.common.collect.ImmutableMap;

public class NeighborRankApiTest extends TraverserApiTest {

    @BeforeClass
    public static void initNeighborRankGraph() {
        GraphManager graph = graph();
        SchemaManager schema = schema();

        schema.propertyKey("name").asText().ifNotExist().create();

        schema.vertexLabel("person")
              .properties("name")
              .useCustomizeStringId()
              .ifNotExist()
              .create();

        schema.vertexLabel("movie")
              .properties("name")
              .useCustomizeStringId()
              .ifNotExist()
              .create();

        schema.edgeLabel("follow")
              .sourceLabel("person")
              .targetLabel("person")
              .ifNotExist()
              .create();

        schema.edgeLabel("like")
              .sourceLabel("person")
              .targetLabel("movie")
              .ifNotExist()
              .create();

        schema.edgeLabel("directedBy")
              .sourceLabel("movie")
              .targetLabel("person")
              .ifNotExist()
              .create();

        Vertex O = graph.addVertex(T.label, "person", T.id, "O", "name", "O");

        Vertex A = graph.addVertex(T.label, "person", T.id, "A", "name", "A");
        Vertex B = graph.addVertex(T.label, "person", T.id, "B", "name", "B");
        Vertex C = graph.addVertex(T.label, "person", T.id, "C", "name", "C");
        Vertex D = graph.addVertex(T.label, "person", T.id, "D", "name", "D");

        Vertex E = graph.addVertex(T.label, "movie", T.id, "E", "name", "E");
        Vertex F = graph.addVertex(T.label, "movie", T.id, "F", "name", "F");
        Vertex G = graph.addVertex(T.label, "movie", T.id, "G", "name", "G");
        Vertex H = graph.addVertex(T.label, "movie", T.id, "H", "name", "H");
        Vertex I = graph.addVertex(T.label, "movie", T.id, "I", "name", "I");
        Vertex J = graph.addVertex(T.label, "movie", T.id, "J", "name", "J");

        Vertex K = graph.addVertex(T.label, "person", T.id, "K", "name", "K");
        Vertex L = graph.addVertex(T.label, "person", T.id, "L", "name", "L");
        Vertex M = graph.addVertex(T.label, "person", T.id, "M", "name", "M");

        O.addEdge("follow", A);
        O.addEdge("follow", B);
        O.addEdge("follow", C);
        D.addEdge("follow", O);

        A.addEdge("follow", B);
        A.addEdge("like", E);
        A.addEdge("like", F);

        B.addEdge("like", G);
        B.addEdge("like", H);

        C.addEdge("like", I);
        C.addEdge("like", J);

        E.addEdge("directedBy", K);
        F.addEdge("directedBy", B);
        F.addEdge("directedBy", L);

        G.addEdge("directedBy", M);
    }

    @AfterClass
    public static void clearNeighborRankGraph() {
        List<Long> taskIds = new ArrayList<>();
        taskIds.add(edgeLabelAPI.delete("directedBy"));
        taskIds.add(edgeLabelAPI.delete("like"));
        taskIds.add(edgeLabelAPI.delete("follow"));
        taskIds.forEach(BaseApiTest::waitUntilTaskCompleted);
        taskIds.clear();
        taskIds.add(vertexLabelAPI.delete("movie"));
        taskIds.add(vertexLabelAPI.delete("person"));
        taskIds.forEach(BaseApiTest::waitUntilTaskCompleted);
    }

    @Test
    public void testNeighborRank() {
        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().direction(Direction.OUT).degree(-1).top(10);
        builder.steps().direction(Direction.OUT).degree(-1).top(10);
        builder.steps().direction(Direction.OUT).degree(-1).top(10);
        builder.alpha(0.9).capacity(-1);
        NeighborRankAPI.Request request = builder.build();

        List<Ranks> ranks = neighborRankAPI.post(request);
        Assert.assertEquals(4, ranks.size());
        Assert.assertEquals(ImmutableMap.of("O", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of("B", 0.4305D, "A", 0.3D, "C", 0.3D),
                            ranks.get(1));
        Assert.assertEquals(ImmutableMap.builder()
                                        .put("G", 0.17550000000000002D)
                                        .put("H", 0.17550000000000002D)
                                        .put("I", 0.135D)
                                        .put("J", 0.135D)
                                        .put("E", 0.09000000000000001D)
                                        .put("F", 0.09000000000000001D)
                                        .build(),
                            ranks.get(2));
        Assert.assertEquals(ImmutableMap.of("M", 0.15795D,
                                            "K", 0.08100000000000002D,
                                            "L", 0.04050000000000001D),
                            ranks.get(3));
    }

    @Test
    public void testNeighborRankWithOtherAlpha() {
        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().direction(Direction.OUT).degree(-1).top(10);
        builder.steps().direction(Direction.OUT).degree(-1).top(10);
        builder.steps().direction(Direction.OUT).degree(-1).top(10);
        builder.alpha(1.0).capacity(-1);
        NeighborRankAPI.Request request = builder.build();

        List<Ranks> ranks = neighborRankAPI.post(request);
        Assert.assertEquals(4, ranks.size());
        Assert.assertEquals(ImmutableMap.of("O", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of("B", 0.5D,
                                            "A", 0.3333333333333333D,
                                            "C", 0.3333333333333333D),
                            ranks.get(1));
        Assert.assertEquals(ImmutableMap.builder()
                                        .put("G", 0.2222222222222222D)
                                        .put("H", 0.2222222222222222D)
                                        .put("I", 0.16666666666666666D)
                                        .put("J", 0.16666666666666666D)
                                        .put("E", 0.1111111111111111D)
                                        .put("F", 0.1111111111111111D)
                                        .build(),
                            ranks.get(2));
        Assert.assertEquals(ImmutableMap.of("M", 0.2222222222222222D,
                                            "K", 0.1111111111111111D,
                                            "L", 0.05555555555555555D),
                            ranks.get(3));
    }

    @Test
    public void testNeighborRankWithDirection() {
        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().direction(Direction.BOTH);
        builder.steps().direction(Direction.IN);
        builder.steps().direction(Direction.OUT);
        builder.alpha(0.9).capacity(-1);
        NeighborRankAPI.Request request = builder.build();

        List<Ranks> ranks = neighborRankAPI.post(request);
        Assert.assertEquals(4, ranks.size());
        Assert.assertEquals(ImmutableMap.of("O", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of("A", 0.32625000000000004D,
                                            "B", 0.27056250000000004D,
                                            "C", 0.225D,
                                            "D", 0.225D),
                            ranks.get(1));
        Assert.assertEquals(ImmutableMap.of("F", 0.10125D),
                            ranks.get(2));
        Assert.assertEquals(ImmutableMap.of("L", 0.045562500000000006D),
                            ranks.get(3));
    }

    @Test
    public void testNeighborRankWithLabels() {
        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().labels("follow").direction(Direction.OUT);
        builder.steps().labels("like").direction(Direction.OUT);
        builder.steps().labels("directedBy").direction(Direction.OUT);
        builder.alpha(0.9).capacity(-1);
        NeighborRankAPI.Request request = builder.build();

        List<Ranks> ranks = neighborRankAPI.post(request);
        Assert.assertEquals(4, ranks.size());
        Assert.assertEquals(ImmutableMap.of("O", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of("B", 0.36075D,
                                            "A", 0.3D,
                                            "C", 0.3D),
                            ranks.get(1));
        Assert.assertEquals(ImmutableMap.builder()
                                        .put("E", 0.135)
                                        .put("F", 0.135)
                                        .put("G", 0.135)
                                        .put("H", 0.135)
                                        .put("I", 0.135)
                                        .put("J", 0.135)
                                        .build(),
                            ranks.get(2));
        Assert.assertEquals(ImmutableMap.of("K", 0.12150000000000001D,
                                            "M", 0.12150000000000001D,
                                            "L", 0.060750000000000005D),
                            ranks.get(3));
    }

    @Test
    public void testNeighborRankWithTop() {
        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().direction(Direction.OUT).degree(-1).top(2);
        builder.steps().direction(Direction.OUT).degree(-1).top(3);
        builder.steps().direction(Direction.OUT).degree(-1).top(2);
        builder.alpha(0.9).capacity(-1);
        NeighborRankAPI.Request request = builder.build();

        List<Ranks> ranks = neighborRankAPI.post(request);
        Assert.assertEquals(4, ranks.size());
        Assert.assertEquals(ImmutableMap.of("O", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of("B", 0.4305D, "A", 0.3D),
                            ranks.get(1));
        Assert.assertEquals(ImmutableMap.of("G", 0.17550000000000002D,
                                            "H", 0.17550000000000002D,
                                            "I", 0.135D),
                            ranks.get(2));
        Assert.assertEquals(ImmutableMap.of("M", 0.15795D,
                                            "K", 0.08100000000000002D),
                            ranks.get(3));
    }

    @Test
    public void testNeighborRankWithDegree() {
        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().direction(Direction.OUT).degree(2);
        builder.steps().direction(Direction.OUT).degree(1);
        builder.steps().direction(Direction.OUT).degree(1);
        builder.alpha(0.9).capacity(-1);
        NeighborRankAPI.Request request = builder.build();

        List<Ranks> ranks = neighborRankAPI.post(request);
        Assert.assertEquals(4, ranks.size());
        Assert.assertEquals(ImmutableMap.of("O", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of("B", 0.855D, "A", 0.45D),
                            ranks.get(1));
        Assert.assertEquals(ImmutableMap.of("G", 0.7695D), ranks.get(2));
        Assert.assertEquals(ImmutableMap.of("M", 0.69255D), ranks.get(3));

        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().direction(Direction.OUT).degree(2);
        builder.steps().direction(Direction.OUT).degree(2);
        builder.steps().direction(Direction.OUT).degree(1);
        builder.alpha(0.9).capacity(-1);
        request = builder.build();

        ranks = neighborRankAPI.post(request);
        Assert.assertEquals(4, ranks.size());
        Assert.assertEquals(ImmutableMap.of("O", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of("B", 0.6525000000000001D,
                                            "A", 0.45D),
                            ranks.get(1));
        Assert.assertEquals(ImmutableMap.of("G", 0.293625D,
                                            "H", 0.293625D,
                                            "E", 0.2025D),
                            ranks.get(2));
        Assert.assertEquals(ImmutableMap.of("M", 0.2642625D,
                                            "K", 0.18225000000000002D),
                            ranks.get(3));
    }

    @Test
    public void testNeighborRankWithCapacity() {
        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("O");
        builder.steps().direction(Direction.OUT);
        builder.steps().direction(Direction.OUT);
        builder.steps().direction(Direction.OUT);
        builder.alpha(0.9).capacity(1);
        NeighborRankAPI.Request request = builder.build();

        Assert.assertThrows(ServerException.class, () -> {
            neighborRankAPI.post(request);
        }, e -> {
            String expect = "Exceed capacity '1' while finding neighbor rank";
            Assert.assertTrue(e.toString(), e.getMessage().contains(expect));
        });
    }

    @Test
    public void testNeighborRankWithIsolatedVertex() {
        Vertex isolate = graph().addVertex(T.label, "person", T.id, "isolate",
                                           "name", "isolate-vertex");

        NeighborRankAPI.Request.Builder builder;
        builder = NeighborRankAPI.Request.builder();
        builder.source("isolate").alpha(0.9);
        builder.steps().direction(Direction.BOTH);
        NeighborRankAPI.Request request = builder.build();

        List<Ranks> ranks = neighborRankAPI.post(request);
        Assert.assertEquals(2, ranks.size());
        Assert.assertEquals(ImmutableMap.of("isolate", 1.0D), ranks.get(0));
        Assert.assertEquals(ImmutableMap.of(), ranks.get(1));

        graph().removeVertex(isolate.id());
    }

    @Test
    public void testNeighborRankWithInvalidParams() {
        // Invalid source
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.source(null);
        });

        // Invalid degree
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.steps().degree(-2);
        });

        // Invalid top
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.steps().top(0);
        });
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.steps().top(1001);
        });

        // Invalid alpha
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.alpha(0.0);
        });
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.alpha(1.1);
        });

        // Invalid capacity
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.capacity(-2);
        });

        // Without steps
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            NeighborRankAPI.Request.Builder builder;
            builder = NeighborRankAPI.Request.builder();
            builder.source("A");
            builder.build();
        });
    }
}