/*
 * 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.lucene.search;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

public class TestLiveFieldValues extends LuceneTestCase {
  public void test() throws Exception {

    Directory dir = newFSDirectory(createTempDir("livefieldupdates"));
    IndexWriterConfig iwc = newIndexWriterConfig(new MockAnalyzer(random()));

    final IndexWriter w = new IndexWriter(dir, iwc);

    final SearcherManager mgr =
        new SearcherManager(
            w,
            new SearcherFactory() {
              @Override
              public IndexSearcher newSearcher(IndexReader r, IndexReader previous) {
                return new IndexSearcher(r);
              }
            });

    final Integer missing = -1;

    final LiveFieldValues<IndexSearcher, Integer> rt =
        new LiveFieldValues<IndexSearcher, Integer>(mgr, missing) {
          @Override
          protected Integer lookupFromSearcher(IndexSearcher s, String id) throws IOException {
            TermQuery tq = new TermQuery(new Term("id", id));
            TopDocs hits = s.search(tq, 1);
            assertTrue(hits.totalHits.value <= 1);
            if (hits.totalHits.value == 0) {
              return null;
            } else {
              Document doc = s.storedFields().document(hits.scoreDocs[0].doc);
              return (Integer) doc.getField("field").numericValue();
            }
          }
        };

    int numThreads = TestUtil.nextInt(random(), 2, 5);
    if (VERBOSE) {
      System.out.println(numThreads + " threads");
    }

    final CountDownLatch startingGun = new CountDownLatch(1);
    List<Thread> threads = new ArrayList<>();

    final int iters = atLeast(1000);
    final int idCount = TestUtil.nextInt(random(), 100, 10000);

    final double reopenChance = random().nextDouble() * 0.01;
    final double deleteChance = random().nextDouble() * 0.25;
    final double addChance = random().nextDouble() * 0.5;

    for (int t = 0; t < numThreads; t++) {
      final int threadID = t;
      final Random threadRandom = new Random(random().nextLong());
      Thread thread =
          new Thread() {

            @Override
            public void run() {
              try {
                Map<String, Integer> values = new HashMap<>();
                List<String> allIDs = Collections.synchronizedList(new ArrayList<String>());

                startingGun.await();
                for (int iter = 0; iter < iters; iter++) {
                  // Add/update a document
                  Document doc = new Document();
                  // Threads must not update the same id at the
                  // same time:
                  if (threadRandom.nextDouble() <= addChance) {
                    String id =
                        String.format(
                            Locale.ROOT, "%d_%04x", threadID, threadRandom.nextInt(idCount));
                    Integer field = threadRandom.nextInt(Integer.MAX_VALUE);
                    doc.add(newStringField("id", newBytesRef(id), Field.Store.YES));
                    doc.add(new StoredField("field", field.intValue()));
                    w.updateDocument(new Term("id", id), doc);
                    rt.add(id, field);
                    if (values.put(id, field) == null) {
                      allIDs.add(id);
                    }
                  }

                  if (allIDs.size() > 0 && threadRandom.nextDouble() <= deleteChance) {
                    String randomID = allIDs.get(threadRandom.nextInt(allIDs.size()));
                    w.deleteDocuments(new Term("id", newBytesRef(randomID)));
                    rt.delete(randomID);
                    values.put(randomID, missing);
                  }

                  if (threadRandom.nextDouble() <= reopenChance || rt.size() > 10000) {
                    // System.out.println("refresh @ " + rt.size());
                    mgr.maybeRefresh();
                    if (VERBOSE) {
                      IndexSearcher s = mgr.acquire();
                      try {
                        System.out.println("TEST: reopen " + s);
                      } finally {
                        mgr.release(s);
                      }
                      System.out.println("TEST: " + values.size() + " values");
                    }
                  }

                  if (threadRandom.nextInt(10) == 7) {
                    assertEquals(null, rt.get("foo"));
                  }

                  if (allIDs.size() > 0) {
                    String randomID = allIDs.get(threadRandom.nextInt(allIDs.size()));
                    Integer expected = values.get(randomID);
                    if (Objects.equals(expected, missing)) {
                      expected = null;
                    }
                    assertEquals("id=" + randomID, expected, rt.get(randomID));
                  }
                }
              } catch (Throwable t) {
                throw new RuntimeException(t);
              }
            }
          };
      threads.add(thread);
      thread.start();
    }

    startingGun.countDown();

    for (Thread thread : threads) {
      thread.join();
    }
    mgr.maybeRefresh();
    assertEquals(0, rt.size());

    rt.close();
    mgr.close();
    w.close();
    dir.close();
  }
}
