package org.codelibs.elasticsearch.reindex;

import junit.framework.TestCase;
import org.codelibs.elasticsearch.runner.ElasticsearchClusterRunner;
import org.codelibs.elasticsearch.runner.ElasticsearchClusterRunner.BuilderCallback;
import org.codelibs.elasticsearch.runner.net.Curl;
import org.codelibs.elasticsearch.runner.net.CurlResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.Settings.Builder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.node.Node;

import java.io.IOException;
import java.util.Map;

import static org.codelibs.elasticsearch.runner.ElasticsearchClusterRunner.newConfigs;

public class ReindexingPluginTest extends TestCase {

    private ElasticsearchClusterRunner runner;

    private String clusterName;

    private final int docNumber = 25;
    private final int parentNumber = 10;
    private final int childNumber = 5;

    @Override
    protected void setUp() throws Exception {
        clusterName = "es-reindexing-" + System.currentTimeMillis();
        // create runner instance
        runner = new ElasticsearchClusterRunner();
        // create ES nodes
        runner.onBuild(new ElasticsearchClusterRunner.Builder() {
            @Override
            public void build(final int number, final Builder settingsBuilder) {
                settingsBuilder.put("http.cors.enabled", true);
                settingsBuilder.put("http.cors.allow-origin", "*");
                settingsBuilder.put("index.number_of_shards", 3);
                settingsBuilder.put("index.number_of_replicas", 0);
                settingsBuilder.putArray("discovery.zen.ping.unicast.hosts",
                        "localhost:9301-9310");
                settingsBuilder.put("plugin.types",
                        "org.codelibs.elasticsearch.reindex.ReindexingPlugin");
                settingsBuilder
                        .put("index.unassigned.node_left.delayed_timeout", "0");
            }
        }).build(newConfigs().numOfNode(1).clusterName(clusterName));

        // wait for yellow status
        runner.ensureYellow();
    }

    @Override
    protected void tearDown() throws Exception {
        // close runner
        runner.close();
        // delete all files
        runner.clean();
    }

    public void test_reindexing() throws Exception {

        final String index = "dataset";
        final String type = "item";

        create_index(runner, index, type, docNumber);

        Node node = runner.node();

        runner.ensureGreen();
        test_wait_for_completion(node, index);

        runner.ensureGreen();
        test_index_to_newIndex(node, index);

        runner.ensureGreen();
        test_index_to_newIndex_withSource(node, index);

        runner.ensureGreen();
        test_index_type_to_newIndex(node, index, type);

        runner.ensureGreen();
        test_index_type_to_newIndex_newType(node, index, type);

        runner.ensureGreen();
        test_index_to_remote_newIndex(node, index);

        runner.ensureGreen();
        test_index_to_remote_newIndex_withSource(node, index);

        runner.ensureGreen();
        test_index_type_to_remote_newIndex(node, index, type);

        runner.ensureGreen();
        test_index_type_to_remote_newIndex_newType(node, index, type);

        runner.ensureGreen();
        test_reindex_with_deletion(node, index);

        create_index(runner, index, type, docNumber);

        runner.ensureGreen();
        test_reindex_with_deletion(node, index, type);
    }

    private void create_index(ElasticsearchClusterRunner runner, String index, String type, int number) {

        if (runner.indexExists(index))
            runner.deleteIndex(index);

        runner.createIndex(index, (Settings) null);

        if (!runner.indexExists(index))
            fail();

        for (int i = 0; i < number; i++) {
            final IndexResponse response = runner.insert(index, type, String.valueOf(i),
                    "{\"msg\":\"test " + i + "\", \"id\":\"" + i + "\"}");
            assertTrue(response.isCreated());
        }

        // make it searchable immediately
        runner.refresh();

        // search documents
        final SearchResponse searchResponse = runner.search(index, type, null, null, 0, 10);
        assertEquals(number, searchResponse.getHits().getTotalHits());

        assertTrue(runner.indexExists(index));
    }

    private void test_wait_for_completion(Node node, String index) {
        String newIndex0 = "dataset0", newIndex1 = "dataset1";

        CurlResponse response0 = Curl.post(node, "/" + index + "/_reindex/" + newIndex0)
                .param("wait_for_completion", "true").execute();
        CurlResponse response1 = Curl.post(node, "/" + index + "/_reindex/" + newIndex1)
                .execute();
        Map<String, Object> map0 = response0.getContentAsMap();
        Map<String, Object> map1 = response1.getContentAsMap();
        assertEquals(map0.size(), 1);
        assertTrue(map0.containsKey("acknowledged"));
        assertEquals(map1.size(), 2);
        assertTrue(map1.containsKey("acknowledged"));
        assertTrue(map1.containsKey("name"));

        runner.flush();
        runner.deleteIndex(newIndex0);
        runner.deleteIndex(newIndex1);
    }

    private void test_index_to_newIndex(Node node, String index) throws IOException {
        String newIndex = "dataset2";

        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/_reindex/" + newIndex)
                .param("wait_for_completion", "true")
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        // causes a lucene commit, more expensive than refresh
        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    // BuilderCallback对象用于加工SearchRequestBuilder对象
                    new BuilderCallback<SearchRequestBuilder>() {
                        @Override
                        public SearchRequestBuilder apply(SearchRequestBuilder builder) {
                            return builder;
                        }
                    });
            assertEquals(docNumber, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_index_to_newIndex_withSource(Node node, String index) throws IOException {
        String newIndex = "dataset2";

        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/_reindex/" + newIndex)
                .param("wait_for_completion", "true")
                .body("{\"query\":{\"term\":{\"msg\":{\"value\":\"1\"}}}}")
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search newly created index
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    new BuilderCallback<SearchRequestBuilder>() {
                        @Override
                        public SearchRequestBuilder apply(SearchRequestBuilder builder) {
                            return builder;
                        }
                    });
            assertEquals(1, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_index_type_to_newIndex(Node node, String index, String type) throws IOException {
        String newIndex = "dataset2";
        String newType = type;

        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/" + type + "/_reindex/" + newIndex)
                .param("wait_for_completion", "true")
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newType, null, null, 0, 10);
            assertEquals(docNumber, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_index_type_to_newIndex_newType(Node node, String index, String type) throws IOException {
        String newIndex = "dataset2";
        String newType = "item2";

        try (CurlResponse curlResponse = Curl
                .post(node,
                        "/" + index + "/" + type + "/_reindex/" + newIndex
                                + "/" + newType)
                .param("wait_for_completion", "true").execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newType, null, null, 0, 10);
            assertEquals(docNumber, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_index_to_remote_newIndex(Node node, String index) throws IOException {
        String newIndex = "dataset2";

        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/_reindex/" + newIndex)
                .param("wait_for_completion", "true")
                .param("url", "http://localhost:" + node.settings().get("http.port"))
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    new BuilderCallback<SearchRequestBuilder>() {
                        @Override
                        public SearchRequestBuilder apply(SearchRequestBuilder builder) {
                            return builder;
                        }
                    });
            assertEquals(docNumber, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_index_to_remote_newIndex_withSource(Node node, String index) throws IOException {
        String newIndex = "dataset2";

        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/_reindex/" + newIndex)
                .param("wait_for_completion", "true")
                .param("url",
                        "http://localhost:" + node.settings().get("http.port"))
                .body("{\"query\":{\"term\":{\"msg\":{\"value\":\"1\"}}}}")
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    new BuilderCallback<SearchRequestBuilder>() {
                        @Override
                        public SearchRequestBuilder apply(SearchRequestBuilder builder) {
                            return builder;
                        }
                    });
            assertEquals(1, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_index_type_to_remote_newIndex(Node node, String index, String type) throws IOException {
        String newIndex = "dataset2";
        String newType = type;

        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/" + type + "/_reindex/" + newIndex)
                .param("wait_for_completion", "true")
                .param("url",
                        "http://localhost:" + node.settings().get("http.port"))
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newType, null, null, 0, 10);
            assertEquals(docNumber, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_index_type_to_remote_newIndex_newType(Node node, String index, String type) throws IOException {
        String newIndex = "dataset2";
        String newType = "item2";

        try (CurlResponse curlResponse = Curl
                .post(node,
                        "/" + index + "/" + type + "/_reindex/" + newIndex
                                + "/" + newType)
                .param("wait_for_completion", "true")
                .param("url",
                        "http://localhost:" + node.settings().get("http.port"))
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newType, null, null, 0, 10);
            assertEquals(docNumber, searchResponse.getHits().getTotalHits());
        }

        runner.deleteIndex(newIndex);
    }

    private void test_reindex_with_deletion(Node node, final String... document_identifier) {

        if (document_identifier.length == 0 || document_identifier.length > 2)
            throw new IllegalArgumentException("args should be index and type[optinal].");

        final String index = document_identifier[0];
        String type = new String();
        if (document_identifier.length == 2) {
            type = "/" + document_identifier[1];
        }

        final String newIndex = "dataset2";

        CurlResponse curlResponse = Curl
                .post(node, "/" + index + type + "/_reindex/" + newIndex)
                .param("wait_for_completion", "true")
                .param("deletion", "true")
                .execute();
        Map<String, Object> map = curlResponse.getContentAsMap();
        assertTrue(map.containsKey("acknowledged"));
        assertNull(map.get("name"));

        runner.flush();

        if (type.isEmpty()) {
            assertFalse(runner.indexExists(index));
        } else {
            assertTrue(runner.indexExists(index));
            final SearchResponse searchResponse = runner.search(index,
                    new BuilderCallback<SearchRequestBuilder>() {
                        @Override
                        public SearchRequestBuilder apply(SearchRequestBuilder builder) {
                            return builder.setTypes(document_identifier[1]);
                        }
                    });
            assertEquals(0, searchResponse.getHits().getTotalHits());
        }
        assertTrue(runner.indexExists(newIndex));

        final SearchResponse searchResponse = runner.search(newIndex,
                new BuilderCallback<SearchRequestBuilder>() {
                    @Override
                    public SearchRequestBuilder apply(SearchRequestBuilder builder) {
                        return builder;
                    }
                });
        assertEquals(docNumber, searchResponse.getHits().getTotalHits());

        runner.deleteIndex(newIndex);
    }

    public void test_parentChild() throws Exception {

        final String index = "company";
        final String parentType = "branch";
        final String childType = "employee";

        final int age = (int) (Math.random() % childNumber) + 1;
        final String ageStr = String.valueOf(age);

        // create an index with parent mapping
        runner.createIndex(index, (Settings) null);
        runner.createMapping(index, childType, "{\"_parent\":{\"type\":\""
                + parentType + "\"}}");

        if (!runner.indexExists(index)) {
            fail();
        }

        // create documents
        for (int i = 1; i <= parentNumber; i++) {
            final IndexResponse indexResponse1 = runner.insert(index, parentType, String.valueOf(i), "{\"name\":\"Branch" + i + "\"}");
            assertTrue(indexResponse1.isCreated());
            for (int j = 1; j <= childNumber; j++) {
                final IndexResponse indexResponse2 = runner
                        .client()
                        .prepareIndex(index, childType, i + "_" + j)
                        .setSource("{\"name\":\"Taro " + i + "_" + j + "\", \"age\":\"" + (j) + "\"}")
                        .setParent(String.valueOf(i))
                        .setRefresh(true).execute().actionGet();
                assertTrue(indexResponse2.isCreated());
            }
        }
        runner.refresh();

        // search parent type documents
        {
            final SearchResponse searchResponse = runner.search(index,
                    parentType, null, null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        // search all documents
        {
            final SearchResponse searchResponse = runner.search(index,
                    childType, null, null, 0, 10);
            assertEquals(parentNumber * childNumber, searchResponse.getHits().getTotalHits());
        }
        // search a certain parent documents
        {
            final SearchResponse searchResponse = runner
                    .search(index, parentType,
                            QueryBuilders.hasChildQuery(childType, QueryBuilders.matchQuery("age", ageStr)),
                            null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }

        Node node = runner.node();

        runner.ensureGreen();
        test_index_to_newIndex_pc(node, index, parentType, childType, ageStr);

        runner.ensureGreen();
        test_index_type_to_newIndex_pc(node, index, parentType, childType, ageStr);

        runner.ensureGreen();
        test_index_to_remote_newIndex_pc(node, index, parentType, childType, ageStr);

        runner.ensureGreen();
        test_index_type_to_remote_newIndex_pc(node, index, parentType,
                childType, ageStr);
    }

    private void test_index_to_newIndex_pc(Node node, String index, String parentType, String childType, String age) throws IOException {
        String newIndex = "company2";
        String newParentType = parentType;
        String newChildType = childType;

        // create an index
        runner.createIndex(newIndex, (Settings) null);
        runner.createMapping(newIndex, newChildType,
                "{\"_parent\":{\"type\":\"" + parentType + "\"}}");

        // reindex
        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/_reindex/" + newIndex + "/")
                .param("wait_for_completion", "true").execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search parent documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newParentType, null, null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        // search child documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newChildType, null, null, 0, 10);
            assertEquals(parentNumber * childNumber, searchResponse.getHits().getTotalHits());
        }
        // search a certain parent documents
        {
            final SearchResponse searchResponse = runner
                    .search(newIndex, newParentType, QueryBuilders
                                    .hasChildQuery(newChildType,
                                            QueryBuilders.matchQuery("age", age)),
                            null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        runner.deleteIndex(newIndex);
    }

    private void test_index_type_to_newIndex_pc(Node node, String index, String parentType, String childType, String age) throws IOException {
        String newIndex = "company2";
        String newParentType = parentType;
        String newChildType = childType;

        // create an index
        runner.createIndex(newIndex, (Settings) null);
        runner.createMapping(newIndex, newChildType,
                "{\"_parent\":{\"type\":\"" + parentType + "\"}}");

        // reindex
        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/" + parentType + "," + childType + "/_reindex/" + newIndex + "/")
                .param("wait_for_completion", "true").execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search parent documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newParentType, null, null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        // search 1000 child documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newChildType, null, null, 0, 10);
            assertEquals(parentNumber * childNumber, searchResponse.getHits().getTotalHits());
        }
        // search a certain parent documents
        {
            final SearchResponse searchResponse = runner
                    .search(newIndex, newParentType, QueryBuilders
                                    .hasChildQuery(newChildType,
                                            QueryBuilders.matchQuery("age", age)),
                            null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        runner.deleteIndex(newIndex);
    }

    private void test_index_to_remote_newIndex_pc(Node node, String index, String parentType, String childType, String age) throws IOException {
        String newIndex = "company2";
        String newParentType = parentType;
        String newChildType = childType;

        // create an index
        runner.createIndex(newIndex, (Settings) null);
        runner.createMapping(newIndex, newChildType,
                "{\"_parent\":{\"type\":\"" + parentType + "\"}}");

        // reindex
        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/_reindex/" + newIndex + "/")
                .param("wait_for_completion", "true")
                .param("url", "http://localhost:" + node.settings().get("http.port"))
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search parent documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newParentType, null, null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        // search child documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newChildType, null, null, 0, 10);
            assertEquals(parentNumber * childNumber, searchResponse.getHits().getTotalHits());
        }
        // search a certain parent documents
        {
            final SearchResponse searchResponse = runner
                    .search(newIndex, newParentType,
                            QueryBuilders.hasChildQuery(newChildType, QueryBuilders.matchQuery("age", age)),
                            null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        runner.deleteIndex(newIndex);
    }

    private void test_index_type_to_remote_newIndex_pc(Node node, String index, String parentType, String childType, String age) throws IOException {
        String newIndex = "company2";
        String newParentType = parentType;
        String newChildType = childType;

        // create an index
        runner.createIndex(newIndex, (Settings) null);
        runner.createMapping(newIndex, newChildType,
                "{\"_parent\":{\"type\":\"" + parentType + "\"}}");

        // reindex
        try (CurlResponse curlResponse = Curl
                .post(node, "/" + index + "/" + parentType + "," + childType + "/_reindex/" + newIndex + "/")
                .param("wait_for_completion", "true")
                .param("url", "http://localhost:" + node.settings().get("http.port"))
                .execute()) {
            Map<String, Object> map = curlResponse.getContentAsMap();
            assertTrue(map.containsKey("acknowledged"));
            assertNull(map.get("name"));
        }

        runner.flush();

        assertTrue(runner.indexExists(index));
        assertTrue(runner.indexExists(newIndex));

        // search parent documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newParentType, null, null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        // search child documents
        {
            final SearchResponse searchResponse = runner.search(newIndex,
                    newChildType, null, null, 0, 10);
            assertEquals(parentNumber * childNumber, searchResponse.getHits().getTotalHits());
        }
        // search a certain parent documents
        {
            final SearchResponse searchResponse = runner
                    .search(newIndex, newParentType,
                            QueryBuilders.hasChildQuery(newChildType, QueryBuilders.matchQuery("age", age)),
                            null, 0, 10);
            assertEquals(parentNumber, searchResponse.getHits().getTotalHits());
        }
        runner.deleteIndex(newIndex);
    }
}