/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.modeshape.jcr.query.parse;

import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.modeshape.common.text.ParsingException;
import org.modeshape.jcr.query.model.Column;
import org.modeshape.jcr.query.model.FullTextSearch.CompoundTerm;
import org.modeshape.jcr.query.model.FullTextSearch.Conjunction;
import org.modeshape.jcr.query.model.FullTextSearch.Disjunction;
import org.modeshape.jcr.query.model.FullTextSearch.NegationTerm;
import org.modeshape.jcr.query.model.FullTextSearch.SimpleTerm;
import org.modeshape.jcr.query.model.FullTextSearch.Term;

public class FullTextSearchParserTest {

    private FullTextSearchParser parser;

    @Before
    public void beforeEach() {
        parser = new FullTextSearchParser();
    }

    @Test
    public void shouldHaveLocationAndScoresButNoColumns() {
        List<? extends Column> columns = FullTextSearchParser.FULL_TEXT_COLUMNS;
        assertThat(columns.size(), is(1));
        Column column1 = columns.get(0);
        assertThat(column1.getColumnName(), is("jcr:score"));
    }

    @Test( expected = IllegalArgumentException.class )
    public void shouldFailToParseNullString() {
        parser.parse((String)null);
    }

    @Test( expected = ParsingException.class )
    public void shouldFailToParseEmptyString() {
        parser.parse("");
    }

    @Test( expected = ParsingException.class )
    public void shouldFailToParseBlankString() {
        parser.parse("   ");
    }

    @Test
    public void shouldParseStringWithOneUnquotedTerm() {
        Term result = parser.parse("term1");
        assertSimpleTerm(result, "term1", false, false);
    }

    @Test
    public void shouldParseStringWithOneSingleQuotedTermWithOneWord() {
        Term result = parser.parse("'term1'");
        assertSimpleTerm(result, "term1", false, false);
    }

    @Test
    public void shouldParseStringWithOneSingleQuotedTermWithMultipleWords() {
        Term result = parser.parse("'term1 has two words'");
        assertSimpleTerm(result, "term1 has two words", false, true);
    }

    @Test
    public void shouldParseStringWithOneDoubleQuotedTermWithOneWord() {
        Term result = parser.parse("\"term1\"");
        assertSimpleTerm(result, "term1", false, false);
    }

    @Test
    public void shouldParseStringWithOneDoubleQuotedTermWithMultipleWords() {
        Term result = parser.parse("\"term1 has two words\"");
        assertSimpleTerm(result, "term1 has two words", false, true);
    }

    @Test
    public void shouldParseStringWithMultipleUnquotedTerms() {
        Term result = parser.parse("term1 term2 term3");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Conjunction.class)));
        Conjunction conjunction = (Conjunction)result;
        assertHasSimpleTerms(conjunction, "term1", "term2", "term3");
    }

    @Test
    public void shouldParseStringWithMultipleUnquotedTermsWithNegatedTerms() {
        Term result = parser.parse("term1 term2 -term3");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Conjunction.class)));
        Conjunction conjunction = (Conjunction)result;
        assertHasSimpleTerms(conjunction, "term1", "term2", "-term3");
    }

    @Test
    public void shouldParseStringWithMultipleQuotedAndUnquotedTermsWithNegatedTerms() {
        Term result = parser.parse("term1 \"term2 and 2a\" -term3 -'term 4'");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Conjunction.class)));
        Conjunction conjunction = (Conjunction)result;
        assertHasSimpleTerms(conjunction, "term1", "term2 and 2a", "-term3", "-term 4");
    }

    @Test
    public void shouldParseStringWithMultipleUnquotedORedTerms() {
        Term result = parser.parse("term1 OR term2 OR term3");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Disjunction.class)));
        Disjunction disjunction = (Disjunction)result;
        assertHasSimpleTerms(disjunction, "term1", "term2", "term3");
    }

    @Test
    public void shouldParseStringWithMultipleUnquotedORedTermsWithNegatedTerms() {
        Term result = parser.parse("term1 OR term2 OR -term3");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Disjunction.class)));
        Disjunction disjunction = (Disjunction)result;
        assertHasSimpleTerms(disjunction, "term1", "term2", "-term3");
    }

    @Test
    public void shouldParseStringWithMultipleUnquotedANDedTermsORedTogether() {
        Term result = parser.parse("term1 term2 OR -term3 -term4");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Disjunction.class)));
        Disjunction disjunction = (Disjunction)result;
        assertThat(disjunction.getTerms().size(), is(2));
        Conjunction conjunction1 = (Conjunction)disjunction.getTerms().get(0);
        Conjunction conjunction2 = (Conjunction)disjunction.getTerms().get(1);
        assertHasSimpleTerms(conjunction1, "term1", "term2");
        assertHasSimpleTerms(conjunction2, "-term3", "term4");
    }

    @Test
    public void shouldParseStringWithTwoANDedUnquotedTermsORedWithMultipleUnquotedTerms() {
        Term result = parser.parse("term1 term2 OR -term3 OR -term4 OR term5");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Disjunction.class)));
        Disjunction disjunction = (Disjunction)result;
        assertThat(disjunction.getTerms().size(), is(4));
        Conjunction conjunction1 = (Conjunction)disjunction.getTerms().get(0);
        Term term3 = disjunction.getTerms().get(1);
        Term term4 = disjunction.getTerms().get(2);
        Term term5 = disjunction.getTerms().get(3);
        assertHasSimpleTerms(conjunction1, "term1", "term2");
        assertSimpleTerm(term3, "term3", true, false);
        assertSimpleTerm(term4, "term4", true, false);
        assertSimpleTerm(term5, "term5", false, false);
    }

    @Test
    public void shouldParseStringWithWildcardCharactersAtEndOfSingleTerm() {
        Term result = parser.parse("term*");
        assertSimpleTerm(result, "term*", false, false);
    }

    @Test
    public void shouldParseStringWithWildcardCharactersAtBeginningOfSingleTerm() {
        Term result = parser.parse("*term");
        assertSimpleTerm(result, "*term", false, false);
    }

    @Test
    public void shouldParseStringWithWildcardCharactersInsideSingleTerm() {
        Term result = parser.parse("te*rm");
        assertSimpleTerm(result, "te*rm", false, false);
    }

    @Test
    public void shouldParseStringWithWildcardCharactersInMultipleTerms() {
        Term result = parser.parse("term* term? *term*");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Conjunction.class)));
        Conjunction conjunction = (Conjunction)result;
        assertHasSimpleTerms(conjunction, "term*", "term?", "*term*");
    }

    @Test
    public void shouldParseStringWithWildcardCharactersInSingleQuotedTerm() {
        Term result = parser.parse("'term* term? *term*'");
        assertSimpleTerm(result, "term* term? *term*", false, true);
    }

    @Test
    public void shouldParseStringWithSqlStyleWildcardCharactersInMultipleTerms() {
        Term result = parser.parse("term% term_ %term_");
        assertThat(result, is(notNullValue()));
        assertThat(result, is(instanceOf(Conjunction.class)));
        Conjunction conjunction = (Conjunction)result;
        assertHasSimpleTerms(conjunction, "term%", "term_", "%term_");
    }

    @Test
    public void shouldParseStringWithSqlStyleWildcardCharactersInSingleQuotedTerm() {
        Term result = parser.parse("'term% term_ %term_'");
        assertSimpleTerm(result, "term% term_ %term_", false, true);
    }

    @Test
    public void shouldParseStringWithEscapedWildcardCharacterInSingleTerm() {
        Term result = parser.parse("term\\*");
        assertSimpleTerm(result, "term\\*", false, false);
    }

    public static void assertHasSimpleTerms( CompoundTerm compoundTerm,
                                             String... terms ) {
        List<Term> expectedTerms = new ArrayList<Term>();
        for (String term : terms) {
            if (term.startsWith("-")) {
                term = term.substring(1);
                expectedTerms.add(new NegationTerm(new SimpleTerm(term)));
            } else {
                expectedTerms.add(new SimpleTerm(term));
            }
        }
        assertHasTerms(compoundTerm, expectedTerms.toArray(new Term[expectedTerms.size()]));
    }

    public static void assertSimpleTerm( Term term,
                                         String value,
                                         boolean excluded,
                                         boolean quotingRequired ) {
        assertThat(term, is(notNullValue()));
        if (excluded) {
            assertThat(term, is(instanceOf(NegationTerm.class)));
            NegationTerm negationTerm = (NegationTerm)term;
            Term negated = negationTerm.getNegatedTerm();
            assertThat(negated, is(instanceOf(SimpleTerm.class)));
            SimpleTerm simpleTerm = (SimpleTerm)negated;
            assertThat(simpleTerm.getValue(), is(value));
            assertThat(simpleTerm.isQuotingRequired(), is(quotingRequired));
        } else {
            assertThat(term, is(instanceOf(SimpleTerm.class)));
            SimpleTerm simpleTerm = (SimpleTerm)term;
            assertThat(simpleTerm.getValue(), is(value));
            assertThat(simpleTerm.isQuotingRequired(), is(quotingRequired));
        }
    }

    public static void assertHasTerms( CompoundTerm compoundTerm,
                                       Term... terms ) {
        Iterator<Term> iterator = compoundTerm.iterator();
        for (int i = 0; i != 0; i++) {
            Term expected = terms[i];
            assertThat(iterator.hasNext(), is(true));
            Term term = iterator.next();
            assertThat(term, is(expected));
        }
    }
}
