/*
 *
 * 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.
 *
*/

using Lucene.Net.Analysis;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Queries;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;
using NUnit.Framework;

namespace Lucene.Net.Tests.Queries
{
    public class BooleanFilterTest : LuceneTestCase
    {
        private Directory directory;
        private AtomicReader reader;

        [SetUp]
        public override void SetUp()
        {
            base.SetUp();
            directory = NewDirectory();
            RandomIndexWriter writer = new RandomIndexWriter(
#if FEATURE_INSTANCE_TESTDATA_INITIALIZATION
                this,
#endif
                Random, directory, new MockAnalyzer(Random, MockTokenizer.WHITESPACE, false));

            AddDoc(writer, @"admin guest", @"010", @"20040101", @"Y");
            AddDoc(writer, @"guest", @"020", @"20040101", @"Y");
            AddDoc(writer, @"guest", @"020", @"20050101", @"Y");
            AddDoc(writer, @"admin", @"020", @"20050101", @"Maybe");
            AddDoc(writer, @"admin guest", @"030", @"20050101", @"N");
            reader = SlowCompositeReaderWrapper.Wrap(writer.GetReader());
            writer.Dispose();
        }

        [TearDown]
        public override void TearDown()
        {
            reader.Dispose();
            directory.Dispose();
            base.TearDown();
        }

        private void AddDoc(RandomIndexWriter writer, string accessRights, string price, string date, string inStock)
        {
            Document doc = new Document();
            doc.Add(NewTextField(@"accessRights", accessRights, Field.Store.YES));
            doc.Add(NewTextField(@"price", price, Field.Store.YES));
            doc.Add(NewTextField(@"date", date, Field.Store.YES));
            doc.Add(NewTextField(@"inStock", inStock, Field.Store.YES));
            writer.AddDocument(doc);
        }

        private Filter GetRangeFilter(string field, string lowerPrice, string upperPrice)
        {
            Filter f = TermRangeFilter.NewStringRange(field, lowerPrice, upperPrice, true, true);
            return f;
        }

        private Filter GetTermsFilter(string field, string text)
        {
            return new TermsFilter(new Term(field, text));
        }

        private Filter GetWrappedTermQuery(string field, string text)
        {
            return new QueryWrapperFilter(new TermQuery(new Term(field, text)));
        }

        private Filter GetEmptyFilter()
        {
            return new AnonymousFilter(this);
        }

        private sealed class AnonymousFilter : Filter
        {
            public AnonymousFilter(BooleanFilterTest parent)
            {
                this.parent = parent;
            }

            private readonly BooleanFilterTest parent;
            public override DocIdSet GetDocIdSet(AtomicReaderContext context, IBits acceptDocs)
            {
                return new FixedBitSet(context.AtomicReader.MaxDoc);
            }
        }

        private Filter GetNullDISFilter()
        {
            return new AnonymousFilter1(this);
        }

        private sealed class AnonymousFilter1 : Filter
        {
            public AnonymousFilter1(BooleanFilterTest parent)
            {
                this.parent = parent;
            }

            private readonly BooleanFilterTest parent;
            public override DocIdSet GetDocIdSet(AtomicReaderContext context, IBits acceptDocs)
            {
                return null;
            }
        }

        private Filter GetNullDISIFilter()
        {
            return new AnonymousFilter2(this);
        }

        private sealed class AnonymousDocIdSet : DocIdSet
        {
            public override DocIdSetIterator GetIterator()
            {
                return null;
            }

            public override bool IsCacheable => true;
        }

        private sealed class AnonymousFilter2 : Filter
        {
            public AnonymousFilter2(BooleanFilterTest parent)
            {
                this.parent = parent;
            }

            private readonly BooleanFilterTest parent;
            public override DocIdSet GetDocIdSet(AtomicReaderContext context, IBits acceptDocs)
            {
                return new AnonymousDocIdSet();
            }
        }

        private void TstFilterCard(string mes, int expected, Filter filt)
        {
            DocIdSet docIdSet = filt.GetDocIdSet(reader.AtomicContext, reader.LiveDocs);
            int actual = 0;
            if (docIdSet != null)
            {
                DocIdSetIterator disi = docIdSet.GetIterator();
                while (disi.NextDoc() != DocIdSetIterator.NO_MORE_DOCS)
                {
                    actual++;
                }
            }

            assertEquals(mes, expected, actual);
        }

        [Test]
        public void TestShould()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.SHOULD);
            TstFilterCard(@"Should retrieves only 1 doc", 1, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetWrappedTermQuery(@"price", @"030"), Occur.SHOULD);
            TstFilterCard(@"Should retrieves only 1 doc", 1, booleanFilter);
        }

        [Test]
        public void TestShoulds()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"010", @"020"), Occur.SHOULD);
            booleanFilter.Add(GetRangeFilter(@"price", @"020", @"030"), Occur.SHOULD);
            TstFilterCard(@"Shoulds are Ored together", 5, booleanFilter);
        }

        [Test]
        public void TestShouldsAndMustNot()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"010", @"020"), Occur.SHOULD);
            booleanFilter.Add(GetRangeFilter(@"price", @"020", @"030"), Occur.SHOULD);
            booleanFilter.Add(GetTermsFilter(@"inStock", @"N"), Occur.MUST_NOT);
            TstFilterCard(@"Shoulds Ored but AndNot", 4, booleanFilter);
            booleanFilter.Add(GetTermsFilter(@"inStock", @"Maybe"), Occur.MUST_NOT);
            TstFilterCard(@"Shoulds Ored but AndNots", 3, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"010", @"020"), Occur.SHOULD);
            booleanFilter.Add(GetRangeFilter(@"price", @"020", @"030"), Occur.SHOULD);
            booleanFilter.Add(GetWrappedTermQuery(@"inStock", @"N"), Occur.MUST_NOT);
            TstFilterCard(@"Shoulds Ored but AndNot", 4, booleanFilter);
            booleanFilter.Add(GetWrappedTermQuery(@"inStock", @"Maybe"), Occur.MUST_NOT);
            TstFilterCard(@"Shoulds Ored but AndNots", 3, booleanFilter);
        }

        [Test]
        public void TestShouldsAndMust()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"010", @"020"), Occur.SHOULD);
            booleanFilter.Add(GetRangeFilter(@"price", @"020", @"030"), Occur.SHOULD);
            booleanFilter.Add(GetTermsFilter(@"accessRights", @"admin"), Occur.MUST);
            TstFilterCard(@"Shoulds Ored but MUST", 3, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"010", @"020"), Occur.SHOULD);
            booleanFilter.Add(GetRangeFilter(@"price", @"020", @"030"), Occur.SHOULD);
            booleanFilter.Add(GetWrappedTermQuery(@"accessRights", @"admin"), Occur.MUST);
            TstFilterCard(@"Shoulds Ored but MUST", 3, booleanFilter);
        }

        [Test]
        public void TestShouldsAndMusts()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"010", @"020"), Occur.SHOULD);
            booleanFilter.Add(GetRangeFilter(@"price", @"020", @"030"), Occur.SHOULD);
            booleanFilter.Add(GetTermsFilter(@"accessRights", @"admin"), Occur.MUST);
            booleanFilter.Add(GetRangeFilter(@"date", @"20040101", @"20041231"), Occur.MUST);
            TstFilterCard(@"Shoulds Ored but MUSTs ANDED", 1, booleanFilter);
        }

        [Test]
        public void TestShouldsAndMustsAndMustNot()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"030", @"040"), Occur.SHOULD);
            booleanFilter.Add(GetTermsFilter(@"accessRights", @"admin"), Occur.MUST);
            booleanFilter.Add(GetRangeFilter(@"date", @"20050101", @"20051231"), Occur.MUST);
            booleanFilter.Add(GetTermsFilter(@"inStock", @"N"), Occur.MUST_NOT);
            TstFilterCard(@"Shoulds Ored but MUSTs ANDED and MustNot", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetRangeFilter(@"price", @"030", @"040"), Occur.SHOULD);
            booleanFilter.Add(GetWrappedTermQuery(@"accessRights", @"admin"), Occur.MUST);
            booleanFilter.Add(GetRangeFilter(@"date", @"20050101", @"20051231"), Occur.MUST);
            booleanFilter.Add(GetWrappedTermQuery(@"inStock", @"N"), Occur.MUST_NOT);
            TstFilterCard(@"Shoulds Ored but MUSTs ANDED and MustNot", 0, booleanFilter);
        }

        [Test]
        public void TestJustMust()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"accessRights", @"admin"), Occur.MUST);
            TstFilterCard(@"MUST", 3, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetWrappedTermQuery(@"accessRights", @"admin"), Occur.MUST);
            TstFilterCard(@"MUST", 3, booleanFilter);
        }

        [Test]
        public void TestJustMustNot()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"inStock", @"N"), Occur.MUST_NOT);
            TstFilterCard(@"MUST_NOT", 4, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetWrappedTermQuery(@"inStock", @"N"), Occur.MUST_NOT);
            TstFilterCard(@"MUST_NOT", 4, booleanFilter);
        }

        [Test]
        public void TestMustAndMustNot()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"inStock", @"N"), Occur.MUST);
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.MUST_NOT);
            TstFilterCard(@"MUST_NOT wins over MUST for same docs", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetWrappedTermQuery(@"inStock", @"N"), Occur.MUST);
            booleanFilter.Add(GetWrappedTermQuery(@"price", @"030"), Occur.MUST_NOT);
            TstFilterCard(@"MUST_NOT wins over MUST for same docs", 0, booleanFilter);
        }

        [Test]
        public void TestEmpty()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            TstFilterCard(@"empty BooleanFilter returns no results", 0, booleanFilter);
        }

        [Test]
        public void TestCombinedNullDocIdSets()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.MUST);
            booleanFilter.Add(GetNullDISFilter(), Occur.MUST);
            TstFilterCard(@"A MUST filter that returns a null DIS should never return documents", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.MUST);
            booleanFilter.Add(GetNullDISIFilter(), Occur.MUST);
            TstFilterCard(@"A MUST filter that returns a null DISI should never return documents", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.SHOULD);
            booleanFilter.Add(GetNullDISFilter(), Occur.SHOULD);
            TstFilterCard(@"A SHOULD filter that returns a null DIS should be invisible", 1, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.SHOULD);
            booleanFilter.Add(GetNullDISIFilter(), Occur.SHOULD);
            TstFilterCard(@"A SHOULD filter that returns a null DISI should be invisible", 1, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.MUST);
            booleanFilter.Add(GetNullDISFilter(), Occur.MUST_NOT);
            TstFilterCard(@"A MUST_NOT filter that returns a null DIS should be invisible", 1, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"price", @"030"), Occur.MUST);
            booleanFilter.Add(GetNullDISIFilter(), Occur.MUST_NOT);
            TstFilterCard(@"A MUST_NOT filter that returns a null DISI should be invisible", 1, booleanFilter);
        }

        [Test]
        public void TestJustNullDocIdSets()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISFilter(), Occur.MUST);
            TstFilterCard(@"A MUST filter that returns a null DIS should never return documents", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISIFilter(), Occur.MUST);
            TstFilterCard(@"A MUST filter that returns a null DISI should never return documents", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISFilter(), Occur.SHOULD);
            TstFilterCard(@"A single SHOULD filter that returns a null DIS should never return documents", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISIFilter(), Occur.SHOULD);
            TstFilterCard(@"A single SHOULD filter that returns a null DISI should never return documents", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISFilter(), Occur.MUST_NOT);
            TstFilterCard(@"A single MUST_NOT filter that returns a null DIS should be invisible", 5, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISIFilter(), Occur.MUST_NOT);
            TstFilterCard(@"A single MUST_NOT filter that returns a null DIS should be invisible", 5, booleanFilter);
        }

        [Test]
        public void TestNonMatchingShouldsAndMusts()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetEmptyFilter(), Occur.SHOULD);
            booleanFilter.Add(GetTermsFilter(@"accessRights", @"admin"), Occur.MUST);
            TstFilterCard(@">0 shoulds with no matches should return no docs", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISFilter(), Occur.SHOULD);
            booleanFilter.Add(GetTermsFilter(@"accessRights", @"admin"), Occur.MUST);
            TstFilterCard(@">0 shoulds with no matches should return no docs", 0, booleanFilter);
            booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetNullDISIFilter(), Occur.SHOULD);
            booleanFilter.Add(GetTermsFilter(@"accessRights", @"admin"), Occur.MUST);
            TstFilterCard(@">0 shoulds with no matches should return no docs", 0, booleanFilter);
        }

        [Test]
        public void TestToStringOfBooleanFilterContainingTermsFilter()
        {
            BooleanFilter booleanFilter = new BooleanFilter();
            booleanFilter.Add(GetTermsFilter(@"inStock", @"N"), Occur.MUST);
            booleanFilter.Add(GetTermsFilter(@"isFragile", @"Y"), Occur.MUST);
            assertEquals(@"BooleanFilter(+inStock:N +isFragile:Y)", booleanFilter.ToString());
        }

        [Test]
        public void TestToStringOfWrappedBooleanFilters()
        {
            BooleanFilter orFilter = new BooleanFilter();
            BooleanFilter stockFilter = new BooleanFilter();
            stockFilter.Add(new FilterClause(GetTermsFilter(@"inStock", @"Y"), Occur.MUST));
            stockFilter.Add(new FilterClause(GetTermsFilter(@"barCode", @"12345678"), Occur.MUST));
            orFilter.Add(new FilterClause(stockFilter, Occur.SHOULD));
            BooleanFilter productPropertyFilter = new BooleanFilter();
            productPropertyFilter.Add(new FilterClause(GetTermsFilter(@"isHeavy", @"N"), Occur.MUST));
            productPropertyFilter.Add(new FilterClause(GetTermsFilter(@"isDamaged", @"Y"), Occur.MUST));
            orFilter.Add(new FilterClause(productPropertyFilter, Occur.SHOULD));
            BooleanFilter composedFilter = new BooleanFilter();
            composedFilter.Add(new FilterClause(orFilter, Occur.MUST));
            assertEquals(@"BooleanFilter(+BooleanFilter(BooleanFilter(+inStock:Y +barCode:12345678) BooleanFilter(+isHeavy:N +isDamaged:Y)))", composedFilter.ToString());
        }
    }
}
