/*
 * 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.queryparser.xml;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.analysis.MockTokenFilter;
import org.apache.lucene.analysis.MockTokenizer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.DisjunctionMaxQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.spans.SpanQuery;
import org.apache.lucene.util.LuceneTestCase;
import org.junit.AfterClass;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.InputStream;

public class TestCoreParser extends LuceneTestCase {

  final private static String defaultField = "contents";

  private static Analyzer analyzer;
  private static CoreParser coreParser;

  private static CoreParserTestIndexData indexData;

  protected Analyzer newAnalyzer() {
    // TODO: rewrite test (this needs to set QueryParser.enablePositionIncrements, too, for work with CURRENT):
    return new MockAnalyzer(random(), MockTokenizer.WHITESPACE, true, MockTokenFilter.ENGLISH_STOPSET);
  }

  protected CoreParser newCoreParser(String defaultField, Analyzer analyzer) {
    return new CoreParser(defaultField, analyzer);
  }

  @AfterClass
  public static void afterClass() throws Exception {
    if (indexData != null) {
      indexData.close();
      indexData = null;
    }
    coreParser = null;
    analyzer = null;
  }

  public void testTermQueryXML() throws ParserException, IOException {
    Query q = parse("TermQuery.xml");
    dumpResults("TermQuery", q, 5);
  }

  public void test_DOCTYPE_TermQueryXML() throws ParserException, IOException {
    SAXException saxe = LuceneTestCase.expectThrows(ParserException.class, SAXException.class,
        () -> parse("DOCTYPE_TermQuery.xml"));
    assertTrue(saxe.getMessage().startsWith("External Entity resolving unsupported:"));
  }

  public void test_ENTITY_TermQueryXML() throws ParserException, IOException {
    SAXException saxe = LuceneTestCase.expectThrows(ParserException.class, SAXException.class,
        () -> parse("ENTITY_TermQuery.xml"));
    assertTrue(saxe.getMessage().startsWith("External Entity resolving unsupported:"));
  }

  public void testTermQueryEmptyXML() throws ParserException, IOException {
    parseShouldFail("TermQueryEmpty.xml",
        "TermQuery has no text");
  }

  public void testTermsQueryXML() throws ParserException, IOException {
    Query q = parse("TermsQuery.xml");
    dumpResults("TermsQuery", q, 5);
  }

  public void testBooleanQueryXML() throws ParserException, IOException {
    Query q = parse("BooleanQuery.xml");
    dumpResults("BooleanQuery", q, 5);
  }
  
  public void testDisjunctionMaxQueryXML() throws ParserException, IOException {
    Query q = parse("DisjunctionMaxQuery.xml");
    assertTrue(q instanceof DisjunctionMaxQuery);
    DisjunctionMaxQuery d = (DisjunctionMaxQuery)q;
    assertEquals(0.0f, d.getTieBreakerMultiplier(), 0.0001f);
    assertEquals(2, d.getDisjuncts().size());
    DisjunctionMaxQuery ndq = (DisjunctionMaxQuery) d.getDisjuncts().get(1);
    assertEquals(0.3f, ndq.getTieBreakerMultiplier(), 0.0001f);
    assertEquals(1, ndq.getDisjuncts().size());
  }

  public void testRangeQueryXML() throws ParserException, IOException {
    Query q = parse("RangeQuery.xml");
    dumpResults("RangeQuery", q, 5);
  }

  public void testUserQueryXML() throws ParserException, IOException {
    Query q = parse("UserInputQuery.xml");
    dumpResults("UserInput with Filter", q, 5);
  }

  public void testCustomFieldUserQueryXML() throws ParserException, IOException {
    Query q = parse("UserInputQueryCustomField.xml");
    long h = searcher().search(q, 1000).totalHits.value;
    assertEquals("UserInputQueryCustomField should produce 0 result ", 0, h);
  }

  public void testBoostingTermQueryXML() throws Exception {
    Query q = parse("BoostingTermQuery.xml");
    dumpResults("BoostingTermQuery", q, 5);
  }

  public void testSpanTermXML() throws Exception {
    Query q = parse("SpanQuery.xml");
    dumpResults("Span Query", q, 5);
    SpanQuery sq = parseAsSpan("SpanQuery.xml");
    dumpResults("Span Query", sq, 5);
    assertEquals(q, sq);
  }

  public void testSpanPositionRangeQueryXML() throws Exception {
    Query q = parse("SpanPositionRangeQuery.xml");
    long h = searcher().search(q, 10).totalHits.value;
    assertEquals("SpanPositionRangeQuery should produce 2 result ", 2, h);
    SpanQuery sq = parseAsSpan("SpanPositionRangeQuery.xml");
    dumpResults("SpanPositionRangeQuery", sq, 5);
    assertEquals(q, sq);
  }

  public void testSpanNearQueryWithoutSlopXML() throws Exception {
    Exception expectedException = new NumberFormatException("For input string: \"\"");
    try {
      Query q = parse("SpanNearQueryWithoutSlop.xml");
      fail("got query "+q+" instead of expected exception "+expectedException);
    } catch (Exception e) {
      assertEquals(expectedException.toString(), e.toString());
    }
    try {
      SpanQuery sq = parseAsSpan("SpanNearQueryWithoutSlop.xml");
      fail("got span query "+sq+" instead of expected exception "+expectedException);
    } catch (Exception e) {
      assertEquals(expectedException.toString(), e.toString());
    }
  }

  public void testConstantScoreQueryXML() throws Exception {
    Query q = parse("ConstantScoreQuery.xml");
    dumpResults("ConstantScoreQuery", q, 5);
  }

  public void testMatchAllDocsPlusFilterXML() throws ParserException, IOException {
    Query q = parse("MatchAllDocsQuery.xml");
    dumpResults("MatchAllDocsQuery with range filter", q, 5);
  }

  public void testNestedBooleanQuery() throws ParserException, IOException {
    Query q = parse("NestedBooleanQuery.xml");
    dumpResults("Nested Boolean query", q, 5);
  }
  
  public void testPointRangeQuery() throws ParserException, IOException {
    Query q = parse("PointRangeQuery.xml");
    dumpResults("PointRangeQuery", q, 5);
  }

  public void testPointRangeQueryWithoutLowerTerm() throws ParserException, IOException {
    Query q = parse("PointRangeQueryWithoutLowerTerm.xml");
    dumpResults("PointRangeQueryWithoutLowerTerm", q, 5);
  }

  public void testPointRangeQueryWithoutUpperTerm() throws ParserException, IOException {
    Query q = parse("PointRangeQueryWithoutUpperTerm.xml");
    dumpResults("PointRangeQueryWithoutUpperTerm", q, 5);
  }

  public void testPointRangeQueryWithoutRange() throws ParserException, IOException {
    Query q = parse("PointRangeQueryWithoutRange.xml");
    dumpResults("PointRangeQueryWithoutRange", q, 5);
  }

  //================= Helper methods ===================================

  protected String defaultField() {
    return defaultField;
  }

  protected Analyzer analyzer() {
    if (analyzer == null) {
      analyzer = newAnalyzer();
    }
    return analyzer;
  }

  protected CoreParser coreParser() {
    if (coreParser == null) {
      coreParser = newCoreParser(defaultField, analyzer());
    }
    return coreParser;
  }

  private CoreParserTestIndexData indexData() {
    if (indexData == null) {
      try {
        indexData = new CoreParserTestIndexData(analyzer());
      } catch (Exception e) {
        fail("caught Exception "+e);
      }
    }
    return indexData;
  }

  protected IndexReader reader() {
    return indexData().reader;
  }

  protected IndexSearcher searcher() {
    return indexData().searcher;
  }

  protected void parseShouldFail(String xmlFileName, String expectedParserExceptionMessage) throws IOException {
    Query q = null;
    ParserException pe = null;
    try {
      q = parse(xmlFileName);
    } catch (ParserException e) {
      pe = e;
    }
    assertNull("for "+xmlFileName+" unexpectedly got "+q, q);
    assertNotNull("expected a ParserException for "+xmlFileName, pe);
    assertEquals("expected different ParserException for "+xmlFileName,
        expectedParserExceptionMessage, pe.getMessage());
  }

  protected Query parse(String xmlFileName) throws ParserException, IOException {
    return implParse(xmlFileName, false);
  }

  protected SpanQuery parseAsSpan(String xmlFileName) throws ParserException, IOException {
    return (SpanQuery)implParse(xmlFileName, true);
  }

  private Query implParse(String xmlFileName, boolean span) throws ParserException, IOException {
    try (InputStream xmlStream = TestCoreParser.class.getResourceAsStream(xmlFileName)) {
      assertNotNull("Test XML file " + xmlFileName + " cannot be found", xmlStream);
      if (span) {
        return coreParser().parseAsSpanQuery(xmlStream);
      } else {
        return coreParser().parse(xmlStream);
      }
    }
  }

  protected Query rewrite(Query q) throws IOException {
    return q.rewrite(reader());
  }

  protected void dumpResults(String qType, Query q, int numDocs) throws IOException {
    if (VERBOSE) {
      System.out.println("TEST: qType=" + qType + " numDocs=" + numDocs + " " + q.getClass().getCanonicalName() + " query=" + q);
    }
    final IndexSearcher searcher = searcher();
    TopDocs hits = searcher.search(q, numDocs);
    final boolean producedResults = (hits.totalHits.value > 0);
    if (!producedResults) {
      System.out.println("TEST: qType=" + qType + " numDocs=" + numDocs + " " + q.getClass().getCanonicalName() + " query=" + q);
    }
    if (VERBOSE) {
      ScoreDoc[] scoreDocs = hits.scoreDocs;
      for (int i = 0; i < Math.min(numDocs, hits.totalHits.value); i++) {
        Document ldoc = searcher.doc(scoreDocs[i].doc);
        System.out.println("[" + ldoc.get("date") + "]" + ldoc.get("contents"));
      }
      System.out.println();
    }
    assertTrue(qType + " produced no results", producedResults);
  }
}
