/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */
// https://issues.jboss.org/browse/TEIID-2153
// https://issues.jboss.org/browse/TEIID-2171
// https://issues.jboss.org/browse/TEIID-2359
// https://issues.jboss.org/browse/TEIID-2149
// https://issues.jboss.org/browse/TEIID-2358
// https://issues.jboss.org/browse/TEIID-2410
// https://issues.jboss.org/browse/TEIID-2271
// https://issues.jboss.org/browse/TEIID-2467
// https://issues.jboss.org/browse/TEIID-2429
// https://issues.jboss.org/browse/TEIID-2363
// https://issues.jboss.org/browse/TEIID-2160
// https://issues.jboss.org/browse/TEIID-2461
// https://issues.jboss.org/browse/TEIID-2797
// https://issues.jboss.org/browse/TEIID-2326
// https://issues.jboss.org/browse/TEIID-1557
// https://issues.jboss.org/browse/TEIID-2496
// https://issues.jboss.org/browse/TEIID-2499
// https://issues.jboss.org/browse/TEIID-2582
// https://issues.jboss.org/browse/TEIID-2672
// https://issues.jboss.org/browse/TEIID-2168
// https://issues.jboss.org/browse/TEIID-1543
// https://issues.jboss.org/browse/TEIID-2619
// https://issues.jboss.org/browse/TEIID-2684
// https://issues.redhat.com/browse/TEIID-2525
// https://issues.redhat.com/browse/TEIID-3119
package org.teiid.dqp.internal.process;

import static org.junit.jupiter.api.Assertions.*;

import java.sql.ResultSet;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.teiid.adminapi.DataPolicy;
import org.teiid.adminapi.impl.DataPolicyMetadata;
import org.teiid.api.exception.query.QueryResolverException;
import org.teiid.cache.CacheConfiguration;
import org.teiid.cache.DefaultCacheFactory;
import org.teiid.client.RequestMessage;
import org.teiid.client.ResultsMessage;
import org.teiid.client.RequestMessage.StatementType;
import org.teiid.client.lob.LobChunk;
import org.teiid.client.util.ResultsFuture;
import org.teiid.common.buffer.BufferManagerFactory;
import org.teiid.common.buffer.impl.BufferManagerImpl;
import org.teiid.core.TeiidProcessingException;
import org.teiid.core.types.BlobType;
import org.teiid.dqp.internal.datamgr.ConnectorManagerRepository;
import org.teiid.dqp.internal.datamgr.FakeTransactionService;
import org.teiid.dqp.internal.process.AbstractWorkItem.ThreadState;
import org.teiid.dqp.service.AutoGenDataService;
import org.teiid.dqp.service.FakeBufferService;
import org.teiid.query.optimizer.TestOptimizer;
import org.teiid.query.optimizer.capabilities.BasicSourceCapabilities;
import org.teiid.query.optimizer.capabilities.SourceCapabilities.Capability;
import org.teiid.query.unittest.RealMetadataFactory;
import org.teiid.translator.SourceSystemFunctions;

@SuppressWarnings("nls")
public class TestDQPCore {

    private final class LobThread extends Thread {
		BlobType bt;
		private final RequestMessage reqMsg;
		volatile ResultsFuture<LobChunk> chunkFuture;
		protected DQPWorkContext workContext;

		private LobThread(RequestMessage reqMsg) {
			this.reqMsg = reqMsg;
		}
		
		@Override
		public void run() {
			synchronized (this) {
				while (workContext == null) {
					try {
						this.wait();
					} catch (InterruptedException e) {
					}
				}
			}
			workContext.runInContext(new Runnable() {
				
				@Override
				public void run() {
					try {
						chunkFuture = core.requestNextLobChunk(1, reqMsg.getExecutionId(), bt.getReferenceStreamId());
					} catch (TeiidProcessingException e) {
						e.printStackTrace();
					}
				}
			});
		}
	}

	private DQPCore core;
    private DQPConfiguration config;
    private AutoGenDataService agds;

    @BeforeEach public void setUp() throws Exception {
    	agds = new AutoGenDataService();
        DQPWorkContext context = RealMetadataFactory.buildWorkContext(RealMetadataFactory.createTransformationMetadata(RealMetadataFactory.exampleBQTCached().getMetadataStore(), "bqt"));
        context.getVDB().getModel("BQT3").setVisible(false); //$NON-NLS-1$
        context.getVDB().getModel("VQT").setVisible(false); //$NON-NLS-1$
        HashMap<String, DataPolicy> policies = new HashMap<String, DataPolicy>();
        policies.put("foo", new DataPolicyMetadata());
        context.setPolicies(policies);

        ConnectorManagerRepository repo = Mockito.mock(ConnectorManagerRepository.class);
        context.getVDB().addAttchment(ConnectorManagerRepository.class, repo);
        Mockito.when(repo.getConnectorManager(Mockito.anyString())).thenReturn(agds);
        BufferManagerImpl bm = BufferManagerFactory.createBufferManager();
		bm.setInlineLobs(false);
		FakeBufferService bs = new FakeBufferService(bm, bm);
        core = new DQPCore();
        core.setBufferManager(bs.getBufferManager());
	    core.setResultsetCache(new SessionAwareCache<CachedResults>("resultset", new DefaultCacheFactory(new CacheConfiguration()), SessionAwareCache.Type.RESULTSET, 0));
	    core.setPreparedPlanCache(new SessionAwareCache<PreparedPlan>("preparedplan", new DefaultCacheFactory(new CacheConfiguration()), SessionAwareCache.Type.PREPAREDPLAN, 0));
        core.setTransactionService(new FakeTransactionService());
        
        config = new DQPConfiguration();
        config.setMaxActivePlans(1);
        config.setUserRequestSourceConcurrency(2);
        DefaultAuthorizationValidator daa = new DefaultAuthorizationValidator();
        daa.setPolicyDecider(new DataRolePolicyDecider());
        config.setAuthorizationValidator(daa);
        core.start(config);
        core.getPrepPlanCache().setModTime(1);
        core.getRsCache().setTupleBufferCache(bs.getBufferManager());
    }
    
    @AfterEach public void tearDown() throws Exception {
    	DQPWorkContext.setWorkContext(new DQPWorkContext());
    	core.stop();
    }

    public RequestMessage exampleRequestMessage(String sql) {
        RequestMessage msg = new RequestMessage(sql);
        msg.setCursorType(ResultSet.TYPE_SCROLL_INSENSITIVE);
        msg.setFetchSize(10);
        msg.setPartialResults(false);
        msg.setExecutionId(100);
        return msg;
    }
    
    @Test public void testConfigurationSets() {
    	assertEquals(1, core.getMaxActivePlans());
    	assertEquals(2, core.getUserRequestSourceConcurrency());
    }

    @Test public void testRequest1() throws Exception {
    	helpExecute("SELECT IntKey FROM BQT1.SmallA", "a"); //$NON-NLS-1$ //$NON-NLS-2$
    }
    
    @Test public void testHasRole() throws Exception {
        String sql = "SELECT hasRole('foo')"; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        ResultsMessage rm = helpExecute(sql, userName);
        assertTrue((Boolean)rm.getResultsList().get(0).get(0));
    }
    
    @Test public void testNotHasRole() throws Exception {
        String sql = "SELECT hasRole('bar')"; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        ResultsMessage rm = helpExecute(sql, userName);
        assertFalse((Boolean)rm.getResultsList().get(0).get(0));
    }

    @Test public void testUser1() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.SmallA WHERE user() = 'logon'"; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testUser2() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.SmallA WHERE user() LIKE 'logon'"; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testUser3() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.SmallA WHERE user() IN ('logon3') AND StringKey LIKE '1'"; //$NON-NLS-1$
        String userName = "logon3"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testUser4() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.SmallA WHERE 'logon4' = user() AND StringKey = '1'"; //$NON-NLS-1$
        String userName = "logon4"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testUser5() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.SmallA WHERE user() IS NULL "; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testUser6() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.SmallA WHERE user() = 'logon33' "; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Disabled("DVCORE-7868, determinism of some functions is changed")
    @Test public void testUser7() throws Exception {
        String sql = "UPDATE BQT1.SmallA SET IntKey = 2 WHERE user() = 'logon' AND StringKey = '1' "; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testUser8() throws Exception {
        String sql = "SELECT user(), StringKey FROM BQT1.SmallA WHERE IntKey = 1 "; //$NON-NLS-1$
        String userName = "logon"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testUser9() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.SmallA WHERE user() = StringKey AND StringKey = '1' "; //$NON-NLS-1$
        String userName = "1"; //$NON-NLS-1$
        helpExecute(sql, userName);
    }

    @Test public void testTxnAutoWrap() throws Exception {
    	String sql = "SELECT * FROM BQT1.SmallA"; //$NON-NLS-1$
    	helpExecute(sql, "a", 1, true); //$NON-NLS-1$
    }
    
    /**
     * Ensures that VQT visibility does not affect the view query
     */
    @Test public void testViewVisibility() throws Exception {
    	String sql = "SELECT * FROM VQT.SmallA_2589g"; //$NON-NLS-1$
    	helpExecute(sql, "a"); //$NON-NLS-1$
    }
    
    @Test public void testLimitCompensation() throws Exception {
    	String sql = "SELECT * FROM VQT.SmallA_2589g LIMIT 1, 1"; //$NON-NLS-1$
    	BasicSourceCapabilities caps = TestOptimizer.getTypicalCapabilities();
    	caps.setCapabilitySupport(Capability.ROW_LIMIT, true);
    	agds.setCaps(caps);
    	ResultsMessage rm = helpExecute(sql, "a"); //$NON-NLS-1$
    	//we test for > 0 here because the autogen service doesn't obey the limit
    	assertTrue(rm.getResultsList().size() > 0);
    }
    
    @Test public void testLimitCompensation1() throws Exception {
    	String sql = "SELECT * FROM VQT.SmallA_2589g LIMIT 1, 1"; //$NON-NLS-1$
    	ResultsMessage rm = helpExecute(sql, "a"); //$NON-NLS-1$
    	assertEquals(1, rm.getResultsList().size());
    }

    
    /**
     * Tests whether an exception result is sent when an exception occurs
     * @since 4.3
     */
    @Test public void testPlanningException() throws Exception {
        String sql = "SELECT IntKey FROM BQT1.BadIdea "; //$NON-NLS-1$
        
        RequestMessage reqMsg = exampleRequestMessage(sql);

        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        try {
        	message.get(5000, TimeUnit.MILLISECONDS);
        } catch (ExecutionException e) {
        	assertTrue(e.getCause() instanceof QueryResolverException);
        }
    }
    
    @Disabled("visibility no longer ristricts access")
	@Test public void testLookupVisibility() throws Exception {
		helpTestVisibilityFails("select lookup('bqt3.smalla', 'intkey', 'stringkey', '?')"); //$NON-NLS-1$
	}
	
	@Test public void testCancel() throws Exception {
		assertFalse(this.core.cancelRequest(1L));
	}
	
    @Test public void testBufferLimit() throws Exception {
    	//the sql should return 400 rows
        String sql = "SELECT A.IntKey FROM BQT1.SmallA as A, BQT1.SmallA as B, (select intkey from BQT1.SmallA limit 4) as C"; //$NON-NLS-1$
        String userName = "1"; //$NON-NLS-1$
        String sessionid = "1"; //$NON-NLS-1$
        
        RequestMessage reqMsg = exampleRequestMessage(sql);
        reqMsg.setCursorType(ResultSet.TYPE_FORWARD_ONLY);
        DQPWorkContext.getWorkContext().getSession().setSessionId(sessionid);
        DQPWorkContext.getWorkContext().getSession().setUserName(userName);
        ((BufferManagerImpl)core.getBufferManager()).setProcessorBatchSize(1);
        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        ResultsMessage rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());

        int rowsPerBatch = 8;
		assertEquals(rowsPerBatch, rm.getResultsList().size());
        RequestWorkItem item = core.getRequestWorkItem(DQPWorkContext.getWorkContext().getRequestID(reqMsg.getExecutionId()));

        message = core.processCursorRequest(reqMsg.getExecutionId(), 9, rowsPerBatch);
        rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
        assertEquals(rowsPerBatch, rm.getResultsList().size());
        //ensure that we are idle
        for (int i = 0; i < 10 && item.getThreadState() != ThreadState.IDLE; i++) {
        	Thread.sleep(100);
        }
        assertEquals(ThreadState.IDLE, item.getThreadState());
        assertTrue(item.resultsBuffer.getManagedRowCount() <= rowsPerBatch*23);
        //pull the rest of the results
        int start = 17;
        while (true) {
            item = core.getRequestWorkItem(DQPWorkContext.getWorkContext().getRequestID(reqMsg.getExecutionId()));

	        message = core.processCursorRequest(reqMsg.getExecutionId(), start, rowsPerBatch);
	        rm = message.get(5000, TimeUnit.MILLISECONDS);
	        assertNull(rm.getException());
	        assertTrue(rowsPerBatch >= rm.getResultsList().size());
	        start += rm.getResultsList().size();
	        if (rm.getFinalRow() == rm.getLastRow()) {
	        	break;
	        }
        }
        
        //local should not buffer
        reqMsg = exampleRequestMessage(sql);
        reqMsg.setCursorType(ResultSet.TYPE_FORWARD_ONLY);
        reqMsg.setSync(true);
        
        message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        rm = message.get(0, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
        assertEquals(rowsPerBatch, rm.getResultsList().size());
        
        item = core.getRequestWorkItem(DQPWorkContext.getWorkContext().getRequestID(reqMsg.getExecutionId()));
        assertEquals(0, item.resultsBuffer.getManagedRowCount());
	    assertEquals(8, item.resultsBuffer.getRowCount());
    }
    
    @Test public void testBufferReuse() throws Exception {
    	//the sql should return 100 rows
        String sql = "SELECT A.IntKey FROM BQT1.SmallA as A, BQT1.SmallA as B ORDER BY A.IntKey"; //$NON-NLS-1$
        String userName = "1"; //$NON-NLS-1$
        String sessionid = "1"; //$NON-NLS-1$
        
        RequestMessage reqMsg = exampleRequestMessage(sql);
        reqMsg.setCursorType(ResultSet.TYPE_FORWARD_ONLY);
        DQPWorkContext.getWorkContext().getSession().setSessionId(sessionid);
        DQPWorkContext.getWorkContext().getSession().setUserName(userName);
        ((BufferManagerImpl)core.getBufferManager()).setProcessorBatchSize(1);
        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        ResultsMessage rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
        assertEquals(8, rm.getResultsList().size());
        RequestWorkItem item = core.getRequestWorkItem(DQPWorkContext.getWorkContext().getRequestID(reqMsg.getExecutionId()));
    	assertEquals(100, item.resultsBuffer.getRowCount());
    }
    
    @Test public void testFinalRow() throws Exception {
        String sql = "SELECT A.IntKey FROM BQT1.SmallA as A"; //$NON-NLS-1$
        String userName = "1"; //$NON-NLS-1$
        String sessionid = "1"; //$NON-NLS-1$
        
        RequestMessage reqMsg = exampleRequestMessage(sql);
        reqMsg.setCursorType(ResultSet.TYPE_FORWARD_ONLY);
        DQPWorkContext.getWorkContext().getSession().setSessionId(sessionid);
        DQPWorkContext.getWorkContext().getSession().setUserName(userName);
        ((BufferManagerImpl)core.getBufferManager()).setProcessorBatchSize(10);
        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        ResultsMessage rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
        assertEquals(10, rm.getResultsList().size());
        RequestWorkItem item = core.getRequestWorkItem(DQPWorkContext.getWorkContext().getRequestID(reqMsg.getExecutionId()));
        while(item.isProcessing());
        synchronized (item) {
			for (int i = 0; i < 100; i++) {
				Thread.sleep(10);
			}
		}
        assertEquals(10, item.resultsBuffer.getRowCount());
    }
    
    @Test public void testBufferReuse1() throws Exception {
    	//the sql should return 100 rows
        String sql = "SELECT IntKey FROM texttable('1112131415' columns intkey integer width 2 no row delimiter) t " +
        		"union " +
        		"SELECT IntKey FROM bqt1.smalla order by intkey"; //$NON-NLS-1$
        String userName = "1"; //$NON-NLS-1$
        String sessionid = "1"; //$NON-NLS-1$
        agds.sleep = 50;
        agds.setUseIntCounter(true);
        RequestMessage reqMsg = exampleRequestMessage(sql);
        reqMsg.setRowLimit(11);
        reqMsg.setCursorType(ResultSet.TYPE_FORWARD_ONLY);
        DQPWorkContext.getWorkContext().getSession().setSessionId(sessionid);
        DQPWorkContext.getWorkContext().getSession().setUserName(userName);
        BufferManagerImpl bufferManager = (BufferManagerImpl)core.getBufferManager();
		bufferManager.setProcessorBatchSize(20);
        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        ResultsMessage rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
	    assertEquals(10, rm.getResultsList().size());
        
        message = core.processCursorRequest(reqMsg.getExecutionId(), 6, 5);
        rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
        assertEquals(5, rm.getResultsList().size());
        
        message = core.processCursorRequest(reqMsg.getExecutionId(), 11, 5);
        rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
        assertEquals(1, rm.getResultsList().size());
        assertEquals(11, rm.getFirstRow());

        assertEquals(11, rm.getFinalRow());
    }
    
    @Test public void testSourceConcurrency() throws Exception {
    	//setup default of 2
    	agds.setSleep(100);
    	BasicSourceCapabilities bsc = TestOptimizer.getTypicalCapabilities();
    	bsc.setFunctionSupport(SourceSystemFunctions.CONCAT, true);
    	agds.setCaps(bsc);
    	StringBuffer sql = new StringBuffer();
    	int branches = 20;
    	for (int i = 0; i < branches; i++) {
    		if (i > 0) {
    			sql.append(" union all ");
    		}
    		sql.append("select stringkey || " + i + " from bqt1.smalla");
    	}
	    //sql.append(" limit 2");
	    helpExecute(sql.toString(), "a", 1, false);
    	//there's isn't a hard guarantee that only two requests will get started
    	assertTrue(agds.getExecuteCount().get() <= 6);
    	
    	//20 concurrent
    	core.setUserRequestSourceConcurrency(20);
    	agds.getExecuteCount().set(0);
	    helpExecute(sql.toString(), "a", 2, false);
	    assertTrue(agds.getExecuteCount().get() > 0 && agds.getExecuteCount().get() <= 21, "The execute count is " + agds.getExecuteCount().get());
    	
    	//serial
    	core.setUserRequestSourceConcurrency(1);
    	agds.getExecuteCount().set(0);
	    helpExecute(sql.toString(), "a", 3, false);
	    //there's two since 1 is smaller than the expected batch
	    assertTrue(agds.getExecuteCount().get() <= 2);
    }

	@Test public void testSourceConcurrencyWithLimitedUnion() throws Exception {
		agds.setSleep(100);
		helpTestSourceConcurrencyWithLimitedUnion();
	}

	@Test public void testSourceConcurrencyWithLimitedUnionThreadBound() throws Exception {
		agds.setSleep(100);
		agds.threadBound = true;
		helpTestSourceConcurrencyWithLimitedUnion();
	}

	@Test public void testThreadBoundException() throws Exception {
		agds.threadBound = true;
		agds.throwExceptionOnExecute = true;
		String sql = "SELECT IntKey FROM BQT1.SmallA"; //$NON-NLS-1$
		String userName = "logon"; //$NON-NLS-1$
		assertThrows(TeiidProcessingException.class, () -> helpExecute(sql, userName));
	}

	private void helpTestSourceConcurrencyWithLimitedUnion() throws Exception {
		BasicSourceCapabilities bsc = TestOptimizer.getTypicalCapabilities();
		bsc.setFunctionSupport(SourceSystemFunctions.CONCAT, true);
		agds.setCaps(bsc);
		StringBuffer sql = new StringBuffer();
		int branches = 20;
		for (int i = 0; i < branches; i++) {
			if (i > 0) {
				sql.append(" union all ");
			}
			sql.append("select stringkey || " + i + " from bqt1.smalla");
		}
		sql.append(" limit 11");

		helpExecute(sql.toString(), "a", 1, false);
		assertTrue(agds.getExecuteCount().get() <= 2, String.valueOf(agds.getExecuteCount()));

		//20 concurrent max, but we'll use 6
		core.setUserRequestSourceConcurrency(20);
		agds.getExecuteCount().set(0);
		helpExecute(sql.toString(), "a", 2, false);
		assertTrue(agds.getExecuteCount().get() <= 6, String.valueOf(agds.getExecuteCount()));

		//serial
		core.setUserRequestSourceConcurrency(1);
		agds.getExecuteCount().set(0);
		helpExecute(sql.toString(), "a", 3, false);
		assertTrue(agds.getExecuteCount().get() <= 2);

		//ensure that we'll still consult all sources even if the limit is not met
		core.setUserRequestSourceConcurrency(4);
		agds.getExecuteCount().set(0);
		agds.setRows(0);
		helpExecute(sql.toString(), "a", 4, false);
		assertEquals(20, agds.getExecuteCount().get());
	}
    
    @Test public void testUsingFinalBuffer() throws Exception {
	    String sql = "select intkey from bqt1.smalla order by intkey";
    	((BufferManagerImpl)core.getBufferManager()).setProcessorBatchSize(2);
    	agds.sleep = 50;
        RequestMessage reqMsg = exampleRequestMessage(sql);
        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        ResultsMessage rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
	    assertEquals(10, rm.getResultsList().size());

        message = core.processCursorRequest(reqMsg.getExecutionId(), 3, 2);
        rm = message.get(500000, TimeUnit.MILLISECONDS);
        assertNull(rm.getException());
	    assertEquals(2, rm.getResultsList().size());
    }

    @Test public void testPreparedPlanInvalidation() throws Exception {
        helpTestPlanInvalidation("select * from #temp a, #temp b limit 10");
        
        assertEquals(2, this.core.getPrepPlanCache().getCacheHitCount());
    }

    @Test public void testPreparedPlanSimpleNoInvalidation() throws Exception {
        helpTestPlanInvalidation("select * from #temp");
        
        assertEquals(3, this.core.getPrepPlanCache().getCacheHitCount());
    }

	private void helpTestPlanInvalidation(String query) throws InterruptedException,
			ExecutionException, TimeoutException, TeiidProcessingException {
		String sql = "insert into #temp select * FROM vqt.SmallB"; //$NON-NLS-1$
        String userName = "1"; //$NON-NLS-1$
        int sessionid = 1; //$NON-NLS-1$
        RequestMessage reqMsg = exampleRequestMessage(sql);
        ResultsMessage rm = execute(userName, sessionid, reqMsg);
        assertEquals(1, rm.getResultsList().size()); //$NON-NLS-1$
        
        sql = query;
        reqMsg = exampleRequestMessage(sql);
        reqMsg.setStatementType(StatementType.PREPARED);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(10, rm.getResultsList().size()); //$NON-NLS-1$
        
        sql = query; 
        reqMsg = exampleRequestMessage(sql);
        reqMsg.setStatementType(StatementType.PREPARED);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(10, rm.getResultsList().size()); //$NON-NLS-1$
        
        assertEquals(1, this.core.getPrepPlanCache().getCacheHitCount());

        Thread.sleep(100);

        //perform a minor update, we should still use the cache
        sql = "delete from #temp where a12345 = '11'"; //$NON-NLS-1$
        reqMsg = exampleRequestMessage(sql);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(1, rm.getResultsList().size()); //$NON-NLS-1$

        sql = query;
        reqMsg = exampleRequestMessage(sql);
        reqMsg.setStatementType(StatementType.PREPARED);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(10, rm.getResultsList().size()); //$NON-NLS-1$
        
        assertEquals(2, this.core.getPrepPlanCache().getCacheHitCount());

        //perform a major update, it might purge the plan
        sql = "delete from #temp"; //$NON-NLS-1$
        reqMsg = exampleRequestMessage(sql);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(1, rm.getResultsList().size()); //$NON-NLS-1$
        
        sql = query;
        reqMsg = exampleRequestMessage(sql);
        reqMsg.setStatementType(StatementType.PREPARED);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(0, rm.getResultsList().size()); //$NON-NLS-1$
	}
    
    @Test public void testRsCacheInvalidation() throws Exception {
        String sql = "select * FROM vqt.SmallB"; //$NON-NLS-1$
        String userName = "1"; //$NON-NLS-1$
        int sessionid = 1; //$NON-NLS-1$
        RequestMessage reqMsg = exampleRequestMessage(sql);
        reqMsg.setUseResultSetCache(true);
        ResultsMessage rm = execute(userName, sessionid, reqMsg);
        assertEquals(10, rm.getResultsList().size()); //$NON-NLS-1$
	    assertEquals(0, this.core.getRsCache().getCacheHitCount());
                
        sql = "select * FROM vqt.SmallB"; //$NON-NLS-1$
        reqMsg = exampleRequestMessage(sql);
        reqMsg.setUseResultSetCache(true);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(10, rm.getResultsList().size()); //$NON-NLS-1$
        
        assertEquals(1, this.core.getRsCache().getCacheHitCount());

        Thread.sleep(100);

        sql = "delete from bqt1.smalla"; //$NON-NLS-1$
        reqMsg = exampleRequestMessage(sql);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(1, rm.getResultsList().size()); //$NON-NLS-1$
        
        sql = "select * FROM vqt.SmallB"; //$NON-NLS-1$
        reqMsg = exampleRequestMessage(sql);
        reqMsg.setUseResultSetCache(true);
        rm = execute(userName, sessionid, reqMsg);
        assertEquals(10, rm.getResultsList().size()); //$NON-NLS-1$
        
        assertEquals(1, this.core.getRsCache().getCacheHitCount());
    }
    
    @Test public void testLobConcurrency() throws Exception {
    	RequestMessage reqMsg = exampleRequestMessage("select to_bytes(stringkey, 'utf-8') FROM BQT1.SmallA"); 
        reqMsg.setTxnAutoWrapMode(RequestMessage.TXN_WRAP_OFF);
        agds.setSleep(100);
        ResultsFuture<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        final LobThread t = new LobThread(reqMsg);
        t.start();
        message.addCompletionListener(new ResultsFuture.CompletionListener<ResultsMessage>() {
        	@Override
        	public void onCompletion(ResultsFuture<ResultsMessage> future) {
        		try {
        			final BlobType bt = (BlobType)future.get().getResultsList().get(0).get(0);
        			t.bt = bt;
        			t.workContext = DQPWorkContext.getWorkContext();
        			synchronized (t) {
            			t.notify();
					}
        			Thread.sleep(100); //give the Thread a chance to run
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
        	}
		});
        message.get();
        t.join();
        assertNotNull(t.chunkFuture.get().getBytes());
    }
    
    @Test public void testServerTimeout() throws Exception {
    	RequestMessage reqMsg = exampleRequestMessage("select to_bytes(stringkey, 'utf-8') FROM BQT1.SmallA"); 
        reqMsg.setTxnAutoWrapMode(RequestMessage.TXN_WRAP_OFF);
        agds.setSleep(100);
        this.config.setQueryTimeout(1);
        ResultsMessage rm = execute("A", 1, reqMsg);
        assertNotNull(rm.getException());
    }
    
    @Test public void testLongRunningQuery() throws Exception {
    	RequestMessage reqMsg = exampleRequestMessage("select * FROM BQT1.SmallA"); 
        execute("A", 1, reqMsg);
        this.config.setQueryThresholdInMilli(5000);
        assertEquals(1, this.core.getRequests().size());
    	assertEquals(0, this.core.getLongRunningRequests().size());
    	this.config.setQueryThresholdInMilli(10);
    	Thread.sleep(20);
    	assertEquals(1, this.core.getLongRunningRequests().size());
    }
    
    @Test public void testDataAvailable() throws Exception {
    	agds.dataNotAvailable = -1;
    	agds.dataAvailable = true;
    	RequestMessage reqMsg = exampleRequestMessage("select * FROM BQT1.SmallA"); 
        ResultsMessage results = execute("A", 1, reqMsg);
        if (results.getException() != null) {
        	throw results.getException();
        }
    }
    
    @Test public void testXmlTableStreamingWithLimit() throws Exception {
        String sql = "select * from xmltable('/a/b' passing xmlparse(document '<a x=''1''><b>foo</b><b>bar</b><b>zed</b></a>') columns y string path '.') as x limit 2"; //$NON-NLS-1$
        
        ResultsMessage rm = execute("A", 1, exampleRequestMessage(sql));
        assertNull(rm.getException());
        assertEquals(2, rm.getResultsList().size());
    }
    
    /**
     * Ensure that the row limit is not misapplied.
     * Note that it still could be applied in this example, but the
     * resultset only returns a single row
     */
    @Test public void testProcedureMaxRows() throws Exception {
    	String sql = "{? = call TEIIDSP9(1, ?)}"; //$NON-NLS-1$
    	RequestMessage request = exampleRequestMessage(sql);
    	request.setRowLimit(1);
    	request.setStatementType(StatementType.CALLABLE);
        ResultsMessage rm = execute("A", 1, request);
        
        assertNull(rm.getException());
        assertEquals(2, rm.getResultsList().size());
    }
    
	public void helpTestVisibilityFails(String sql) throws Exception {
        RequestMessage reqMsg = exampleRequestMessage(sql); 
        reqMsg.setTxnAutoWrapMode(RequestMessage.TXN_WRAP_OFF);
        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        ResultsMessage results = message.get(5000, TimeUnit.MILLISECONDS);
        assertEquals("[QueryValidatorException]Group does not exist: BQT3.SmallA", results.getException().toString()); //$NON-NLS-1$
	}

    ///////////////////////////Helper method///////////////////////////////////
    private ResultsMessage helpExecute(String sql, String userName) throws Exception {
    	return helpExecute(sql, userName, 1, false);
    }

    private ResultsMessage helpExecute(String sql, String userName, int sessionid, boolean txnAutoWrap) throws Exception {
        RequestMessage reqMsg = exampleRequestMessage(sql);
        if (txnAutoWrap) {
        	reqMsg.setTxnAutoWrapMode(RequestMessage.TXN_WRAP_ON);
        }
        ResultsMessage results = execute(userName, sessionid, reqMsg);
        core.terminateSession(String.valueOf(sessionid));
        assertNull(core.getClientState(String.valueOf(sessionid), false));
        if (results.getException() != null) {
        	throw results.getException();
        }
        return results;
    }

	private ResultsMessage execute(String userName, int sessionid, RequestMessage reqMsg)
			throws InterruptedException, ExecutionException, TimeoutException, TeiidProcessingException {
		DQPWorkContext.getWorkContext().getSession().setSessionId(String.valueOf(sessionid));
        DQPWorkContext.getWorkContext().getSession().setUserName(userName);

        Future<ResultsMessage> message = core.executeRequest(reqMsg.getExecutionId(), reqMsg);
        assertNotNull(core.getClientState(String.valueOf(sessionid), false));
        ResultsMessage results = message.get(500000, TimeUnit.MILLISECONDS);
		return results;
	}
}
