package org.apache.cloudberry.pxf.plugins.hbase;

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

import org.apache.hadoop.hbase.filter.CompareFilter;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.FilterList;
import org.apache.hadoop.hbase.filter.NullComparator;
import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;
import org.apache.cloudberry.pxf.api.filter.FilterParser;
import org.apache.cloudberry.pxf.api.filter.Node;
import org.apache.cloudberry.pxf.api.filter.SupportedOperatorPruner;
import org.apache.cloudberry.pxf.api.filter.TreeTraverser;
import org.apache.cloudberry.pxf.api.filter.TreeVisitor;
import org.apache.cloudberry.pxf.api.io.DataType;
import org.apache.cloudberry.pxf.plugins.hbase.utilities.HBaseColumnDescriptor;
import org.apache.cloudberry.pxf.plugins.hbase.utilities.HBaseIntegerComparator;
import org.apache.cloudberry.pxf.plugins.hbase.utilities.HBaseTupleDescription;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.apache.cloudberry.pxf.plugins.hbase.HBaseAccessor.SUPPORTED_OPERATORS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class HBaseFilterBuilderTest {

    private static final TreeVisitor PRUNER = new SupportedOperatorPruner(SUPPORTED_OPERATORS);
    private static final TreeTraverser TRAVERSER = new TreeTraverser();

    private HBaseTupleDescription tupleDescription;
    private byte[][] families = new byte[][]{
            new byte[]{},
            new byte[]{},
            new byte[]{},
            new byte[]{},
    };
    private byte[][] qualifiers = new byte[][]{
            new byte[]{},
            new byte[]{},
            new byte[]{},
            new byte[]{},
    };
    private int[] columnCodes = {
            DataType.INTEGER.getOID(),
            DataType.TEXT.getOID(),
            DataType.REAL.getOID(),
            DataType.TEXT.getOID(),
    };

    @BeforeEach
    public void setup() {
        tupleDescription = mock(HBaseTupleDescription.class);
        for (int i = 0; i < families.length; i++) {
            HBaseColumnDescriptor column = mock(HBaseColumnDescriptor.class);
            when(tupleDescription.getColumn(i)).thenReturn(column);
            when(column.columnFamilyBytes()).thenReturn(families[i]);
            when(column.qualifierBytes()).thenReturn(qualifiers[i]);
            when(column.columnTypeCode()).thenReturn(columnCodes[i]);
        }
    }

    @Test
    public void parseNotExpressionIgnored() throws Exception {
        assertNull(helper("a4c16s4dtrueo0l2", null));
    }

    @Test
    public void parseNotOpCodeInConstant() throws Exception {

        String filter = "a1c25s2dl2o1a1c20s1d2o2l0";
        // Testing that we get past the parsing stage
        // Very crude but it avoids instantiating all the necessary dependencies
        assertThrows(NullPointerException.class, () -> helper(filter, null));
    }

    @Test
    public void parseIsNullExpression() throws Exception {
        Filter filter = helper("a1o8", tupleDescription);
        assertTrue(filter instanceof SingleColumnValueFilter);

        SingleColumnValueFilter result = (SingleColumnValueFilter) filter;
        assertNotNull(result);
        assertSame(families[1], result.getFamily());
        assertSame(qualifiers[1], result.getQualifier());
        assertEquals(CompareFilter.CompareOp.EQUAL, result.getOperator());
        assertTrue(result.getComparator() instanceof NullComparator);
    }

    @Test
    public void parseIsNotNullExpression() throws Exception {
        Filter filter = helper("a1o9", tupleDescription);
        assertTrue(filter instanceof SingleColumnValueFilter);

        SingleColumnValueFilter result = (SingleColumnValueFilter) filter;
        assertNotNull(result);
        assertSame(families[1], result.getFamily());
        assertSame(qualifiers[1], result.getQualifier());
        assertEquals(CompareFilter.CompareOp.NOT_EQUAL, result.getOperator());
        assertTrue(result.getComparator() instanceof NullComparator);
    }

    @Test
    public void testSimpleColumnOperator() throws Exception {
        // id > 5
        Filter filter = helper("a0c20s1d5o2", tupleDescription);

        assertNotNull(filter);
        assertTrue(filter instanceof SingleColumnValueFilter);
        SingleColumnValueFilter scvFilter = (SingleColumnValueFilter) filter;
        assertSame(families[0], scvFilter.getFamily());
        assertSame(qualifiers[0], scvFilter.getQualifier());
        assertEquals(CompareFilter.CompareOp.GREATER, scvFilter.getOperator());
        assertTrue(scvFilter.getComparator() instanceof HBaseIntegerComparator);
        assertEquals(0, scvFilter.getComparator().compareTo("5".getBytes()));
    }

    @Test
    public void testInOperator() throws Exception {
        // IN 'bad'
        Filter filter = helper("a3c25s3dbado10", null);
        assertNull(filter);
    }

    @Test
    public void testOrOperator() throws Exception {
        // a1 > '2008-02-01' or a2 > 1200
        Filter filter = helper("a1c25s10d2008-02-01o2a2c20s4d1200o2l1", tupleDescription);
        assertNotNull(filter);
        assertTrue(filter instanceof FilterList);
        FilterList filterList = (FilterList) filter;
        assertEquals(FilterList.Operator.MUST_PASS_ONE, filterList.getOperator());

        assertNotNull(filterList.getFilters());
        assertEquals(2, filterList.getFilters().size());

        Filter left = filterList.getFilters().get(0);
        Filter right = filterList.getFilters().get(1);
        assertTrue(left instanceof SingleColumnValueFilter);
        assertTrue(right instanceof SingleColumnValueFilter);

        SingleColumnValueFilter scvFilterLeft = (SingleColumnValueFilter) left;
        SingleColumnValueFilter scvFilterRight = (SingleColumnValueFilter) right;

        assertEquals(families[1], scvFilterLeft.getFamily());
        assertEquals(qualifiers[1], scvFilterLeft.getQualifier());
        assertEquals(CompareFilter.CompareOp.GREATER, scvFilterLeft.getOperator());
        assertEquals(0, scvFilterLeft.getComparator().compareTo("2008-02-01".getBytes()));

        assertEquals(families[2], scvFilterRight.getFamily());
        assertEquals(qualifiers[2], scvFilterRight.getQualifier());
        assertEquals(CompareFilter.CompareOp.GREATER, scvFilterRight.getOperator());
        assertEquals(0, scvFilterRight.getComparator().compareTo("1200".getBytes()));
    }

    @Test
    public void testFilterWithIncompatiblePredicate() throws Exception {
        // ((_1_ LIKE row1 AND _2_ < 999) AND _1_ = seq)
        Filter filter = helper("a1c25s4drow1o7a2c23s3d999o1l0a1c25s3dseqo5l0", tupleDescription);
        assertNotNull(filter);
        assertTrue(filter instanceof FilterList);
        FilterList filterList = (FilterList) filter;
        assertEquals(FilterList.Operator.MUST_PASS_ALL, filterList.getOperator());

        // LIKE is not supported so it gets dropped
        assertNotNull(filterList.getFilters());
        assertEquals(2, filterList.getFilters().size());

        Filter left = filterList.getFilters().get(0);
        Filter right = filterList.getFilters().get(1);
        assertTrue(left instanceof SingleColumnValueFilter);
        assertTrue(right instanceof SingleColumnValueFilter);

        SingleColumnValueFilter scvFilterLeft = (SingleColumnValueFilter) left;
        SingleColumnValueFilter scvFilterRight = (SingleColumnValueFilter) right;

        assertEquals(families[2], scvFilterLeft.getFamily());
        assertEquals(qualifiers[2], scvFilterLeft.getQualifier());
        assertEquals(CompareFilter.CompareOp.LESS, scvFilterLeft.getOperator());
        assertEquals(0, scvFilterLeft.getComparator().compareTo("999".getBytes()));

        assertEquals(families[1], scvFilterRight.getFamily());
        assertEquals(qualifiers[1], scvFilterRight.getQualifier());
        assertEquals(CompareFilter.CompareOp.EQUAL, scvFilterRight.getOperator());
        assertEquals(0, scvFilterRight.getComparator().compareTo("seq".getBytes()));
    }

    @Test
    public void testNestedLogicalOperators() throws Exception {
        // cdate > '2008-02-01' OR (cdate < '2008-12-01' AND amt > 1200)
        Filter filter = helper("a1c1082s10d2008-02-01o2a1c1082s10d2008-12-01o1a0c23s4d1200o2l0l1", tupleDescription);
        assertNotNull(filter);
        assertTrue(filter instanceof FilterList);
        FilterList filterList = (FilterList) filter;
        assertEquals(FilterList.Operator.MUST_PASS_ONE, filterList.getOperator());

        assertNotNull(filterList.getFilters());
        assertEquals(2, filterList.getFilters().size());

        Filter left = filterList.getFilters().get(0);
        Filter right = filterList.getFilters().get(1);
        assertTrue(left instanceof SingleColumnValueFilter);
        assertTrue(right instanceof FilterList);

        SingleColumnValueFilter scvFilterLeft = (SingleColumnValueFilter) left;
        FilterList scvFilterListRight = (FilterList) right;

        assertEquals(families[1], scvFilterLeft.getFamily());
        assertEquals(qualifiers[1], scvFilterLeft.getQualifier());
        assertEquals(CompareFilter.CompareOp.GREATER, scvFilterLeft.getOperator());
        assertEquals(0, scvFilterLeft.getComparator().compareTo("2008-02-01".getBytes()));

        assertEquals(FilterList.Operator.MUST_PASS_ALL, scvFilterListRight.getOperator());
        assertNotNull(scvFilterListRight.getFilters());
        assertEquals(2, scvFilterListRight.getFilters().size());

        left = scvFilterListRight.getFilters().get(0);
        right = scvFilterListRight.getFilters().get(1);
        assertTrue(left instanceof SingleColumnValueFilter);
        assertTrue(right instanceof SingleColumnValueFilter);

        scvFilterLeft = (SingleColumnValueFilter) left;
        SingleColumnValueFilter scvFilterRight = (SingleColumnValueFilter) right;

        assertEquals(families[1], scvFilterLeft.getFamily());
        assertEquals(qualifiers[1], scvFilterLeft.getQualifier());
        assertEquals(CompareFilter.CompareOp.LESS, scvFilterLeft.getOperator());
        assertEquals(0, scvFilterLeft.getComparator().compareTo("2008-12-01".getBytes()));

        assertEquals(families[0], scvFilterRight.getFamily());
        assertEquals(qualifiers[0], scvFilterRight.getQualifier());
        assertEquals(CompareFilter.CompareOp.GREATER, scvFilterRight.getOperator());
        assertEquals(0, scvFilterRight.getComparator().compareTo("1200".getBytes()));
    }

    private Filter helper(String filterString, HBaseTupleDescription desc) throws Exception {
        HBaseFilterBuilder hBaseFilterBuilder = new HBaseFilterBuilder(desc);
        Node root = new FilterParser().parse(filterString);
        TRAVERSER.traverse(root, PRUNER, hBaseFilterBuilder);
        return hBaseFilterBuilder.build();
    }
}
