//
// 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 org.apache.couchdb.nouveau.lucene;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.couchdb.nouveau.api.DocumentDeleteRequest;
import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
import org.apache.couchdb.nouveau.api.DoubleField;
import org.apache.couchdb.nouveau.api.DoubleRange;
import org.apache.couchdb.nouveau.api.Field;
import org.apache.couchdb.nouveau.api.IndexDefinition;
import org.apache.couchdb.nouveau.api.IndexInfoResponse;
import org.apache.couchdb.nouveau.api.SearchRequest;
import org.apache.couchdb.nouveau.api.SearchResults;
import org.apache.couchdb.nouveau.api.StringField;
import org.apache.couchdb.nouveau.core.Index;
import org.apache.couchdb.nouveau.core.StaleIndexException;
import org.apache.couchdb.nouveau.core.UpdatesOutOfOrderException;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.misc.store.DirectIODirectory;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class LuceneIndexTest {

    protected final Index setup(final Path path) throws IOException {
        final IndexDefinition indexDefinition = new IndexDefinition();
        final Analyzer analyzer = LuceneAnalyzerFactory.fromDefinition(indexDefinition);
        final Directory dir = new DirectIODirectory(FSDirectory.open(path));
        final IndexWriterConfig config = new IndexWriterConfig(analyzer);
        config.setUseCompoundFile(false);
        final IndexWriter writer = new IndexWriter(dir, config);
        final SearcherManager searcherManager = new SearcherManager(writer, null);
        return new LuceneIndex(analyzer, writer, 0L, 0L, searcherManager);
    }

    protected final void cleanup(final Index index) throws IOException {
        index.setDeleteOnClose(true);
        index.close();
    }

    @Test
    public void testOpenClose(@TempDir Path path) throws IOException {
        final Index index = setup(path);
        cleanup(index);
    }

    @Test
    public void testSearching(@TempDir Path path) throws IOException {
        final Index index = setup(path);
        try {
            final int count = 100;
            for (int i = 1; i <= count; i++) {
                final Collection<Field> fields = List.of(new StringField("foo", "bar", false));
                final DocumentUpdateRequest request = new DocumentUpdateRequest(i - 1, i, null, fields);
                index.update("doc" + i, request);
            }
            final SearchRequest request = new SearchRequest.Builder()
                    .setMinUpdateSeq(count)
                    .setQuery("*:*")
                    .build();
            final SearchResults results = index.search(request);
            assertThat(results.totalHits()).isEqualTo(count);
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testSort(@TempDir Path path) throws IOException {
        final Index index = setup(path);
        try {
            final int count = 100;
            for (int i = 1; i <= count; i++) {
                final Collection<Field> fields = List.of(new StringField("foo", "bar", false));
                final DocumentUpdateRequest request = new DocumentUpdateRequest(i - 1, i, null, fields);
                index.update("doc" + i, request);
            }
            final SearchRequest request = new SearchRequest.Builder()
                    .setMinUpdateSeq(count)
                    .setQuery("*:*")
                    .setSort(List.of("foo"))
                    .build();
            final SearchResults results = index.search(request);
            assertThat(results.totalHits()).isEqualTo(count);
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testCounts(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            final int count = 100;
            for (int i = 1; i <= count; i++) {
                final Collection<Field> fields = List.of(new StringField("bar", "baz", false));
                final DocumentUpdateRequest request = new DocumentUpdateRequest(i - 1, i, null, fields);
                index.update("doc" + i, request);
            }
            final SearchRequest request = new SearchRequest.Builder()
                    .setMinUpdateSeq(count)
                    .setQuery("*:*")
                    .setCounts(List.of("bar"))
                    .build();
            final SearchResults results = index.search(request);
            assertThat(results.counts()).isEqualTo(Map.of("bar", Map.of("baz", count)));
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testRanges(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            final int count = 100;
            for (int i = 1; i <= count; i++) {
                final Collection<Field> fields = List.of(new DoubleField("bar", (double) i, false));
                final DocumentUpdateRequest request = new DocumentUpdateRequest(i - 1, i, null, fields);
                index.update("doc" + i, request);
            }
            final SearchRequest request = new SearchRequest.Builder()
                    .setMinUpdateSeq(count)
                    .setQuery("*:*")
                    .setRanges(Map.of(
                            "bar",
                            List.of(
                                    new DoubleRange("low", 0.0, true, (double) count / 2, true),
                                    new DoubleRange("high", (double) count / 2, true, (double) count, true))))
                    .build();
            final SearchResults results = index.search(request);
            assertThat(results.ranges()).isEqualTo(Map.of("bar", Map.of("low", count / 2, "high", count / 2 + 1)));
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testTopN(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            final int count = 50;
            for (int i = 1; i <= count; i++) {
                final Collection<Field> fields = List.of(new StringField("bar", "bar", false));
                final DocumentUpdateRequest request = new DocumentUpdateRequest(i - 1, i, null, fields);
                index.update("doc" + i, request);
            }

            for (int i = count + 1; i <= (count * 2) + 5; i++) {
                final Collection<Field> fields = List.of(new StringField("bar", "baz", false));
                final DocumentUpdateRequest request = new DocumentUpdateRequest(i - 1, i, null, fields);
                index.update("doc" + i, request);
            }

            final SearchRequest request = new SearchRequest.Builder()
                    .setMinUpdateSeq(count)
                    .setQuery("*:*")
                    .setCounts(List.of("bar"))
                    .setTopN(1)
                    .build();
            final SearchResults results = index.search(request);
            assertThat(results.counts()).isEqualTo(Map.of("bar", Map.of("baz", count + 5)));
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testOutOfOrder(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            final Collection<Field> fields = Collections.emptyList();

            // get match seq wrong
            assertThrows(
                    UpdatesOutOfOrderException.class,
                    () -> index.update("foo", new DocumentUpdateRequest(1, 2, null, fields)));

            // Go to 2.
            index.update("foo", new DocumentUpdateRequest(0, 2, null, fields));

            // Should be prevented from going down to 1.
            assertThrows(
                    UpdatesOutOfOrderException.class,
                    () -> index.update("foo", new DocumentUpdateRequest(2, 1, null, fields)));
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testMinSeq(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            // Require min seq 1 on new, empty index should fail.
            assertThrows(StaleIndexException.class, () -> {
                final SearchRequest request = new SearchRequest.Builder()
                        .setMinUpdateSeq(1)
                        .setQuery("*:*")
                        .build();
                index.search(request);
            });
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testInfo(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            IndexInfoResponse info = index.info();
            assertThat(info.diskSize()).isEqualTo(0);
            assertThat(info.numDocs()).isEqualTo(0);
            assertThat(info.updateSeq()).isEqualTo(0);

            final Collection<Field> fields = List.of(new DoubleField("bar", 12.0, false));
            index.update("foo", new DocumentUpdateRequest(0, 2, null, fields));
            index.commit();

            info = index.info();
            assertThat(info.diskSize()).isGreaterThan(0);
            assertThat(info.numDocs()).isEqualTo(1);
            assertThat(info.updateSeq()).isEqualTo(2);
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testDelete(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            final Collection<Field> fields = List.of(new DoubleField("bar", 12.0, false));
            index.update("foo", new DocumentUpdateRequest(0, 2, null, fields));
            index.commit();

            IndexInfoResponse info = index.info();
            assertThat(info.numDocs()).isEqualTo(1);

            index.delete("foo", new DocumentDeleteRequest(2, 3, false));
            index.commit();

            info = index.info();
            assertThat(info.numDocs()).isEqualTo(0);
            assertThat(info.updateSeq()).isEqualTo(3);
        } finally {
            cleanup(index);
        }
    }

    @Test
    public void testPurge(@TempDir Path path) throws IOException {
        Index index = setup(path);
        try {
            final Collection<Field> fields = List.of(new DoubleField("bar", 12.0, false));
            index.update("foo", new DocumentUpdateRequest(0, 2, null, fields));
            index.commit();

            IndexInfoResponse info = index.info();
            assertThat(info.numDocs()).isEqualTo(1);

            index.delete("foo", new DocumentDeleteRequest(0, 3, true));
            index.commit();

            info = index.info();
            assertThat(info.numDocs()).isEqualTo(0);
            assertThat(info.purgeSeq()).isEqualTo(3);
        } finally {
            cleanup(index);
        }
    }
}
