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

import com.codahale.metrics.Metric;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.join.ScoreMode;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.metrics.MetricsMap;
import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SolrCache;
import org.junit.BeforeClass;
import org.junit.Ignore;

public class TestScoreJoinQPScore extends SolrTestCaseJ4 {

  private static final String idField = "id";
  private static final String toField = "movieId_s";

  @BeforeClass
  public static void beforeTests() throws Exception {
    System.setProperty("enable.update.log", "false"); // schema12 doesn't support _version_
    System.setProperty("solr.filterCache.async", "true");
    initCore("solrconfig.xml", "schema12.xml");
  }

  public void testSimple() throws Exception {
    final String idField = "id";
    final String toField = "productId_s";

    clearIndex();

    // 0
    assertU(add(doc("t_description", "random text", "name", "name1", idField, "1")));

    // 1

    assertU(add(doc("price_s", "10.0", idField, "2", toField, "1")));
    // 2
    assertU(add(doc("price_s", "20.0", idField, "3", toField, "1")));
    // 3
    assertU(add(doc("t_description", "more random text", "name", "name2", idField, "4")));
    // 4
    assertU(add(doc("price_s", "10.0", idField, "5", toField, "4")));
    // 5
    assertU(add(doc("price_s", "20.0", idField, "6", toField, "4")));

    assertU(commit());

    // Search for product
    assertJQ(
        req(
            "q",
            "{!join from=" + idField + " to=" + toField + " score=None}name:name2",
            "fl",
            "id"),
        "/response=={'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'5'},{'id':'6'}]}");

    /*Query joinQuery =
        JoinUtil.createJoinQuery(idField, false, toField, new TermQuery(new Term("name", "name2")), indexSearcher, ScoreMode.None);

    TopDocs result = indexSearcher.search(joinQuery, 10);
    assertEquals(2, result.totalHits);
    assertEquals(4, result.scoreDocs[0].doc);
    assertEquals(5, result.scoreDocs[1].doc);
    */
    assertJQ(
        req(
            "q",
            "{!join from=" + idField + " to=" + toField + " score=None}name:name1",
            "fl",
            "id"),
        "/response=={'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'2'},{'id':'3'}]}");

    /*joinQuery = JoinUtil.createJoinQuery(idField, false, toField, new TermQuery(new Term("name", "name1")), indexSearcher, ScoreMode.None);
    result = indexSearcher.search(joinQuery, 10);
    assertEquals(2, result.totalHits);
    assertEquals(1, result.scoreDocs[0].doc);
    assertEquals(2, result.scoreDocs[1].doc);*/

    // Search for offer
    assertJQ(
        req("q", "{!join from=" + toField + " to=" + idField + " score=None}id:5", "fl", "id"),
        "/response=={'numFound':1,'start':0,'numFoundExact':true,'docs':[{'id':'4'}]}");
    /*joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("id", "5")), indexSearcher, ScoreMode.None);
    result = indexSearcher.search(joinQuery, 10);
    assertEquals(1, result.totalHits);
    assertEquals(3, result.scoreDocs[0].doc);

    indexSearcher.getIndexReader().close();
    dir.close();*/
  }

  public void testDeleteByScoreJoinQuery() throws Exception {
    indexDataForScoring();
    String joinQuery = "{!join from=" + toField + " to=" + idField + " score=Max}title:random";
    assertJQ(
        req("q", joinQuery, "fl", "id"),
        "/response=={'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'1'},{'id':'4'}]}");
    assertU(delQ(joinQuery));
    assertU(commit());
    assertJQ(
        req("q", joinQuery, "fl", "id"),
        "/response=={'numFound':0,'start':0,'numFoundExact':true,'docs':[]}");
  }

  public void testSimpleWithScoring() throws Exception {
    indexDataForScoring();

    // Search for movie via subtitle
    assertJQ(
        req(
            "q",
            "{!join from=" + toField + " to=" + idField + " score=Max}title:random",
            "fl",
            "id"),
        "/response=={'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'1'},{'id':'4'}]}");
    // dump(req("q","{!scorejoin from="+toField+" to="+idField+" score=Max}title:random",
    // "fl","id,score", "debug", "true"));
    /*
    Query joinQuery =
        JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("title", "random")), indexSearcher, ScoreMode.Max);
    TopDocs result = indexSearcher.search(joinQuery, 10);
    assertEquals(2, result.totalHits);
    assertEquals(0, result.scoreDocs[0].doc);
    assertEquals(3, result.scoreDocs[1].doc);*/

    // Score mode max.
    // dump(req("q","{!scorejoin from="+toField+" to="+idField+" score=Max}title:movie",
    // "fl","id,score", "debug", "true"));

    // dump(req("q","title:movie", "fl","id,score", "debug", "true"));
    assertJQ(
        req(
            "q",
            "{!join from=" + toField + " to=" + idField + " score=Max}title:movie",
            "fl",
            "id"),
        "/response=={'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'4'},{'id':'1'}]}");

    /*joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("title", "movie")), indexSearcher, ScoreMode.Max);
    result = indexSearcher.search(joinQuery, 10);
    assertEquals(2, result.totalHits);
    assertEquals(3, result.scoreDocs[0].doc);
    assertEquals(0, result.scoreDocs[1].doc);*/

    // Score mode total
    assertJQ(
        req(
            "q",
            "{!join from=" + toField + " to=" + idField + " score=Total}title:movie",
            "fl",
            "id"),
        "/response=={'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'1'},{'id':'4'}]}");
    /*  joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("title", "movie")), indexSearcher, ScoreMode.Total);
        result = indexSearcher.search(joinQuery, 10);
        assertEquals(2, result.totalHits);
        assertEquals(0, result.scoreDocs[0].doc);
        assertEquals(3, result.scoreDocs[1].doc);
    */
    // Score mode avg
    assertJQ(
        req(
            "q",
            "{!join from=" + toField + " to=" + idField + " score=Avg}title:movie",
            "fl",
            "id"),
        "/response=={'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'4'},{'id':'1'}]}");

    /*  joinQuery = JoinUtil.createJoinQuery(toField, false, idField, new TermQuery(new Term("title", "movie")), indexSearcher, ScoreMode.Avg);
    result = indexSearcher.search(joinQuery, 10);
    assertEquals(2, result.totalHits);
    assertEquals(3, result.scoreDocs[0].doc);
    assertEquals(0, result.scoreDocs[1].doc);*/

  }

  @Ignore("SOLR-7814, also don't forget cover boost at testCacheHit()")
  public void testBoost() throws Exception {
    indexDataForScoring();
    ScoreMode score = ScoreMode.values()[random().nextInt(ScoreMode.values().length)];

    final SolrQueryRequest req =
        req(
            "q",
            "{!join from=movieId_s to=id score=" + score + " b=200}title:movie",
            "fl",
            "id,score",
            "omitHeader",
            "true");
    SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new SolrQueryResponse()));
    final Query luceneQ =
        QParser.getParser(req.getParams().get("q"), req)
            .getQuery()
            .rewrite(req.getSearcher().getSlowAtomicReader());
    assertTrue(luceneQ instanceof BoostQuery);
    float boost = ((BoostQuery) luceneQ).getBoost();
    assertEquals("" + luceneQ, Float.floatToIntBits(200), Float.floatToIntBits(boost));
    SolrRequestInfo.clearRequestInfo();
    req.close();
  }

  public void testCacheHit() throws Exception {
    indexDataForScoring();

    Map<String, Metric> metrics =
        h.getCoreContainer()
            .getMetricManager()
            .registry(h.getCore().getCoreMetricManager().getRegistryName())
            .getMetrics();

    @SuppressWarnings("rawtypes")
    MetricsMap mm =
        (MetricsMap)
            ((SolrMetricManager.GaugeWrapper) metrics.get("CACHE.searcher.queryResultCache"))
                .getGauge();
    {
      Map<String, Object> statPre = mm.getValue();
      h.query(
          req(
              "q",
              "{!join from=movieId_s to=id score=Avg}title:first",
              "fl",
              "id",
              "omitHeader",
              "true"));
      assertHitOrInsert(mm.getValue(), statPre);
    }

    {
      Map<String, Object> statPre = mm.getValue();
      h.query(
          req(
              "q",
              "{!join from=movieId_s to=id score=Avg}title:first",
              "fl",
              "id",
              "omitHeader",
              "true"));
      assertHit(mm.getValue(), statPre);
    }

    {
      Map<String, Object> statPre = mm.getValue();

      Random r = random();
      boolean changed = false;
      boolean x = false;
      String from = (x = r.nextBoolean()) ? "id" : "movieId_s";
      changed |= x;
      String to = (x = r.nextBoolean()) ? "movieId_s" : "id";
      changed |= x;
      String score = (x = r.nextBoolean()) ? not(ScoreMode.Avg).name() : "Avg";
      changed |= x;
      /* till SOLR-7814
       * String boost = (x = r.nextBoolean()) ? "23" : "1";
      changed |= x; */
      String q = (!changed) ? (r.nextBoolean() ? "title:first^67" : "title:night") : "title:first";

      final String resp =
          h.query(
              req(
                  "q",
                  "{!join from="
                      + from
                      + " to="
                      + to
                      + " score="
                      + score
                      +
                      // " b=" + boost +
                      "}"
                      + q,
                  "fl",
                  "id",
                  "omitHeader",
                  "true"));
      assertInsert(mm.getValue(), statPre);

      statPre = mm.getValue();
      final String repeat =
          h.query(
              req(
                  "q",
                  "{!join from="
                      + from
                      + " to="
                      + to
                      + " score="
                      + score.toLowerCase(Locale.ROOT)
                      +
                      // " b=" + boost
                      "}"
                      + q,
                  "fl",
                  "id",
                  "omitHeader",
                  "true"));
      assertHit(mm.getValue(), statPre);

      assertEquals("lowercase shouldn't change anything", resp, repeat);

      final String aMod = score.substring(0, score.length() - 1);
      assertQEx(
          "exception on " + aMod,
          "ScoreMode",
          req(
              "q",
              "{!join from=" + from + " to=" + to + " score=" + aMod + "}" + q,
              "fl",
              "id",
              "omitHeader",
              "true"),
          SolrException.ErrorCode.BAD_REQUEST);
    }
    // these queries are not overlapping with others in this test case.
    // however, it might be better to extract this method into a separate suite
    // for a while let's nuke a cache content, in case of repetitions
    @SuppressWarnings("rawtypes")
    SolrCache cache = (SolrCache) h.getCore().getInfoRegistry().get("queryResultCache");
    cache.clear();
  }

  private ScoreMode not(ScoreMode s) {
    Random r = random();
    final List<ScoreMode> l = new ArrayList<>(Arrays.asList(ScoreMode.values()));
    l.remove(s);
    return l.get(r.nextInt(l.size()));
  }

  private void assertInsert(Map<String, Object> current, final Map<String, Object> statPre) {
    assertEquals("it lookups", 1, delta("lookups", current, statPre));
    assertEquals("it doesn't hit", 0, delta("hits", current, statPre));
    assertEquals("it inserts", 1, delta("inserts", current, statPre));
  }

  private void assertHit(Map<String, Object> current, final Map<String, Object> statPre) {
    assertEquals("it lookups", 1, delta("lookups", current, statPre));
    assertEquals("it hits", 1, delta("hits", current, statPre));
    assertEquals("it doesn't insert", 0, delta("inserts", current, statPre));
  }

  private void assertHitOrInsert(Map<String, Object> current, final Map<String, Object> statPre) {
    assertEquals("it lookups", 1, delta("lookups", current, statPre));
    final long mayHit = delta("hits", current, statPre);
    assertTrue("it may hit", 0 == mayHit || 1 == mayHit);
    assertEquals("or insert on cold", 1, delta("inserts", current, statPre) + mayHit);
  }

  private long delta(String key, Map<String, Object> a, Map<String, Object> b) {
    return (Long) a.get(key) - (Long) b.get(key);
  }

  private void indexDataForScoring() {
    clearIndex();
    // 0
    assertU(add(doc("t_description", "A random movie", "name", "Movie 1", idField, "1")));
    // 1

    assertU(add(doc("title", "The first subtitle of this movie", idField, "2", toField, "1")));

    // 2

    assertU(add(doc("title", "random subtitle; random event movie", idField, "3", toField, "1")));

    // 3

    assertU(add(doc("t_description", "A second random movie", "name", "Movie 2", idField, "4")));
    // 4

    assertU(
        add(
            doc(
                "title",
                "a very random event happened during christmas night",
                idField,
                "5",
                toField,
                "4")));

    // 5

    assertU(
        add(doc("title", "movie end movie test 123 test 123 random", idField, "6", toField, "4")));

    assertU(commit());
  }
}
