/***************************************************************************
    testqgsvectorlayercache.cpp
     --------------------------------------
    Date                 : 20.2.2013
    Copyright            : (C) 2013 Matthias Kuhn
    Email                : matthias at opengis dot ch
 ***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/


#include "qgstest.h"
#include <QObject>
#include <QTemporaryFile>

//qgis includes...
#include "qgsfeatureiterator.h"
#include <qgsvectorlayercache.h>
#include <qgsvectordataprovider.h>
#include <qgsapplication.h>
#include <qgsvectorlayereditbuffer.h>
#include <qgscacheindexfeatureid.h>
#include <QDebug>

/**
 * @ingroup UnitTests
 * This is a unit test for the vector layer cache
 *
 * \see QgsVectorLayerCache
 */
class TestVectorLayerCache : public QObject
{
    Q_OBJECT
  public:
    TestVectorLayerCache() = default;

  private slots:
    void initTestCase();      // will be called before the first testfunction is executed.
    void cleanupTestCase();   // will be called after the last testfunction was executed.
    void init();              // will be called before each testfunction is executed.
    void cleanup();           // will be called after every testfunction.

    void testCacheOverflow();    // Test cache will work if too many features to cache them all are present
    void testCacheAttrActions(); // Test attribute add/ attribute delete
    void testFeatureActions();   // Test adding/removing features works
    void testSubsetRequest();
    void testFullCache();
    void testFullCacheThroughRequest();
    void testCanUseCacheForRequest();
    void testCacheGeom();

    void onCommittedFeaturesAdded( const QString &, const QgsFeatureList & );

  private:
    QgsVectorLayerCache           *mVectorLayerCache = nullptr;
    QgsCacheIndexFeatureId        *mFeatureIdIndex = nullptr;
    QgsVectorLayer                *mPointsLayer = nullptr;
    QgsFeatureList                 mAddedFeatures;
    QMap<QString, QString> mTmpFiles;
};

// runs before all tests
void TestVectorLayerCache::initTestCase()
{
  QgsApplication::init();
  QgsApplication::initQgis();
  QgsApplication::showSettings();

  // Backup test shape file and attributes
  QStringList backupFiles;
  backupFiles << QStringLiteral( "points.shp" ) << QStringLiteral( "points.shx" ) << QStringLiteral( "points.dbf" ) << QStringLiteral( "points.prj" );

  QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
  QString myTestDataDir = myDataDir + '/';

  Q_FOREACH ( const QString &f, backupFiles )
  {
    QString origFileName = myTestDataDir + f;
    QFileInfo origFileInfo( origFileName );

    QString tmpFileName = QDir::tempPath() + '/' + origFileInfo.baseName() + '_' + QString::number( qApp->applicationPid() ) + '.' + origFileInfo.completeSuffix();

    qDebug() << "Copy " << origFileName << " " << tmpFileName;

    qDebug() << QFile::copy( origFileName, tmpFileName );
    mTmpFiles.insert( origFileName, tmpFileName );
  }

  //
  // load a vector layer
  //
  QString myPointsFileName = mTmpFiles.value( myTestDataDir + "points.shp" );
  QFileInfo myPointFileInfo( myPointsFileName );
  mPointsLayer = new QgsVectorLayer( myPointFileInfo.filePath(),
                                     myPointFileInfo.completeBaseName(), QStringLiteral( "ogr" ) );
}

void TestVectorLayerCache::init()
{
  mVectorLayerCache = new QgsVectorLayerCache( mPointsLayer, 10 );
  mFeatureIdIndex = new QgsCacheIndexFeatureId( mVectorLayerCache );
  mVectorLayerCache->addCacheIndex( mFeatureIdIndex );
}

void TestVectorLayerCache::cleanup()
{
  delete mVectorLayerCache;
}

//runs after all tests
void TestVectorLayerCache::cleanupTestCase()
{
  delete mPointsLayer;

  // Clean tmp files
  QMap<QString, QString>::const_iterator it;

  for ( it = mTmpFiles.constBegin(); it != mTmpFiles.constEnd(); ++it )
  {
    QString tmpFileName = it.value();
    qDebug() << "Remove " << tmpFileName;
    QFile::remove( tmpFileName );
  }

  // also clean up newly created .qix file
  QFile::remove( QStringLiteral( TEST_DATA_DIR ) + "/points.qix" );

  QgsApplication::exitQgis();
}

void TestVectorLayerCache::testCacheOverflow()
{
  QgsFeature f;

  // Verify we get all features, even if there are too many to fit into the cache
  QgsFeatureIterator it = mVectorLayerCache->getFeatures();

  int i = 0;
  while ( it.nextFeature( f ) )
  {
    i++;
  }
  it.close();

  QVERIFY( i == 17 );
}

void TestVectorLayerCache::testCacheAttrActions()
{
  QgsFeature f;

  // Add an attribute, make sure it is returned also if a cached feature is requested
  mPointsLayer->startEditing();
  QVariant::Type attrType = QVariant::Int;
  mPointsLayer->addAttribute( QgsField( QStringLiteral( "newAttr" ), attrType, QStringLiteral( "Int" ), 5, 0 ) );
  mPointsLayer->commitChanges();

  QVERIFY( mVectorLayerCache->featureAtId( 15, f ) );
  QVERIFY( f.attribute( "newAttr" ).isValid() );

  QgsFields allFields = mPointsLayer->fields();
  int idx = allFields.indexFromName( QStringLiteral( "newAttr" ) );

  mPointsLayer->startEditing();
  mPointsLayer->deleteAttribute( idx );
  mPointsLayer->commitChanges();

  QVERIFY( mVectorLayerCache->featureAtId( 15, f ) );
  QVERIFY( !f.attribute( "newAttr" ).isValid() );
}

void TestVectorLayerCache::testFeatureActions()
{
  QgsFeature f;

  // Get a random feature to clone
  mPointsLayer->getFeatures( QgsFeatureRequest().setFilterFid( 1 ) ).nextFeature( f );

  // Add feature...
  mPointsLayer->startEditing();
  QVERIFY( mPointsLayer->addFeature( f ) );

  connect( mPointsLayer, SIGNAL( committedFeaturesAdded( QString, QgsFeatureList ) ), SLOT( onCommittedFeaturesAdded( QString, QgsFeatureList ) ) );
  mPointsLayer->commitChanges();
  disconnect( mPointsLayer, SIGNAL( committedFeaturesAdded( QString, QgsFeatureList ) ), this, SLOT( onCommittedFeaturesAdded( QString, QgsFeatureList ) ) );

  QgsFeatureId fid = mAddedFeatures.last().id();

  QVERIFY( mVectorLayerCache->featureAtId( fid, f ) );

  // Delete feature...
  mPointsLayer->startEditing();
  QVERIFY( mPointsLayer->deleteFeature( fid ) );

  QVERIFY( !mVectorLayerCache->featureAtId( fid, f ) );
  mPointsLayer->rollBack();
}

void TestVectorLayerCache::testSubsetRequest()
{
  QgsFeature f;

  QgsFields fields = mPointsLayer->fields();
  QStringList requiredFields;
  requiredFields << QStringLiteral( "Class" ) << QStringLiteral( "Cabin Crew" );

  mVectorLayerCache->featureAtId( 16, f );
  QVariant a = f.attribute( 3 );

  QgsFeatureIterator itSubset = mVectorLayerCache->getFeatures( QgsFeatureRequest().setSubsetOfAttributes( requiredFields, fields ) );
  while ( itSubset.nextFeature( f ) ) {}
  itSubset.close();

  mVectorLayerCache->featureAtId( 16, f );
  QVERIFY( a == f.attribute( 3 ) );
}

void TestVectorLayerCache::testFullCache()
{
  // cache is too small to fit all features
  QgsVectorLayerCache cache( mPointsLayer, 2 );
  QVERIFY( !cache.hasFullCache() );
  QVERIFY( cache.cacheSize() < mPointsLayer->featureCount() );
  // but we set it to full cache
  cache.setFullCache( true );
  // so now it should have sufficient size for all features
  QVERIFY( cache.cacheSize() >= mPointsLayer->featureCount() );
  QVERIFY( cache.hasFullCache() );

  // double check that everything is indeed in the cache
  QgsFeatureIterator it = mPointsLayer->getFeatures();
  QgsFeature f;
  while ( it.nextFeature( f ) )
  {
    QVERIFY( cache.isFidCached( f.id() ) );
  }

  // add a feature to the layer
  mPointsLayer->startEditing();
  QgsFeature f2( mPointsLayer->fields() );
  QVERIFY( mPointsLayer->addFeature( f2 ) );
  QVERIFY( cache.hasFullCache() );
  QVERIFY( cache.isFidCached( f2.id() ) );

  mPointsLayer->rollBack();
}

void TestVectorLayerCache::testFullCacheThroughRequest()
{
  // make sure cache is sufficient size for all features
  QgsVectorLayerCache cache( mPointsLayer, mPointsLayer->featureCount() * 2 );
  QVERIFY( !cache.hasFullCache() );

  // now request all features from cache
  QgsFeatureIterator it = cache.getFeatures( QgsFeatureRequest() );
  QgsFeature f;
  while ( it.nextFeature( f ) )
  {
    // suck in all features
  }

  // cache should now contain all features
  it = mPointsLayer->getFeatures();
  while ( it.nextFeature( f ) )
  {
    QVERIFY( cache.isFidCached( f.id() ) );
  }

  // so it should be a full cache!
  QVERIFY( cache.hasFullCache() );
}

void TestVectorLayerCache::testCanUseCacheForRequest()
{
  //first get some feature ids from layer
  QgsFeature f;
  QgsFeatureIterator it = mPointsLayer->getFeatures();
  it.nextFeature( f );
  QgsFeatureId id1 = f.id();
  it.nextFeature( f );
  QgsFeatureId id2 = f.id();

  QgsVectorLayerCache cache( mPointsLayer, 10 );
  // initially nothing in cache, so can't use it to fulfill the request
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id1 ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id2 ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFids( QgsFeatureIds() << id1 << id2 ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterRect( QgsRectangle( 1, 2, 3, 4 ) ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterExpression( "$x<5" ), it ) );

  // get just the first feature into the cache
  it = cache.getFeatures( QgsFeatureRequest().setFilterFid( id1 ) );
  while ( it.nextFeature( f ) ) { }
  QCOMPARE( cache.cachedFeatureIds(), QgsFeatureIds() << id1 );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id1 ), it ) );
  //verify that the returned iterator was correct
  QVERIFY( it.nextFeature( f ) );
  QCOMPARE( f.id(), id1 );
  QVERIFY( !it.nextFeature( f ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id2 ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFids( QgsFeatureIds() << id1 << id2 ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterRect( QgsRectangle( 1, 2, 3, 4 ) ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterExpression( "$x<5" ), it ) );

  // get feature 2 into cache
  it = cache.getFeatures( QgsFeatureRequest().setFilterFid( id2 ) );
  while ( it.nextFeature( f ) ) { }
  QCOMPARE( cache.cachedFeatureIds(), QgsFeatureIds() << id1 << id2 );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id1 ), it ) );
  QVERIFY( it.nextFeature( f ) );
  QCOMPARE( f.id(), id1 );
  QVERIFY( !it.nextFeature( f ) );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id2 ), it ) );
  QVERIFY( it.nextFeature( f ) );
  QCOMPARE( f.id(), id2 );
  QVERIFY( !it.nextFeature( f ) );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFids( QgsFeatureIds() << id1 << id2 ), it ) );
  QVERIFY( it.nextFeature( f ) );
  QgsFeatureIds result;
  result << f.id();
  QVERIFY( it.nextFeature( f ) );
  result << f.id();
  QCOMPARE( result, QgsFeatureIds() << id1 << id2 );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterRect( QgsRectangle( 1, 2, 3, 4 ) ), it ) );
  QVERIFY( !cache.canUseCacheForRequest( QgsFeatureRequest().setFilterExpression( "$x<5" ), it ) );

  // can only use rect/expression requests if cache has everything
  cache.setFullCache( true );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id1 ), it ) );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFid( id2 ), it ) );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterFids( QgsFeatureIds() << id1 << id2 ), it ) );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterRect( QgsRectangle( 1, 2, 3, 4 ) ), it ) );
  QVERIFY( cache.canUseCacheForRequest( QgsFeatureRequest().setFilterExpression( "$x<5" ), it ) );
}

void TestVectorLayerCache::testCacheGeom()
{
  QgsVectorLayerCache cache( mPointsLayer, 2 );
  // cache geometry
  cache.setCacheGeometry( true );

  //first get some feature ids from layer
  QgsFeature f;
  QgsFeatureIterator it = mPointsLayer->getFeatures();
  it.nextFeature( f );
  QgsFeatureId id1 = f.id();
  it.nextFeature( f );
  QgsFeatureId id2 = f.id();

  QgsFeatureRequest req;
  req.setFlags( QgsFeatureRequest::NoGeometry ); // should be ignored by cache
  req.setFilterFids( QgsFeatureIds() << id1 << id2 );

  it = cache.getFeatures( req );
  while ( it.nextFeature( f ) )
  {
    QVERIFY( f.hasGeometry() );
  }

  // disabled geometry caching
  cache.setCacheGeometry( false );
  // we should still have cached features... no need to lose these!
  QCOMPARE( cache.cachedFeatureIds(), QgsFeatureIds() << id1 << id2 );
  it = cache.getFeatures( req );
  while ( it.nextFeature( f ) )
  {
    QVERIFY( f.hasGeometry() );
  }

  // now upgrade cache from no geometry -> geometry, should be cleared since we
  // cannot be confident that features existing in the cache have geometry
  cache.setCacheGeometry( true );
  QVERIFY( cache.cachedFeatureIds().isEmpty() );
  it = cache.getFeatures( req );
  while ( it.nextFeature( f ) )
  {
    QVERIFY( f.hasGeometry() );
  }

  // another test...
  cache.setCacheGeometry( false );
  cache.setFullCache( true );
  QVERIFY( cache.hasFullCache() );
  cache.setCacheGeometry( true );
  QVERIFY( !cache.hasFullCache() );
}

void TestVectorLayerCache::onCommittedFeaturesAdded( const QString &layerId, const QgsFeatureList &features )
{
  Q_UNUSED( layerId )
  mAddedFeatures.append( features );
}

QGSTEST_MAIN( TestVectorLayerCache )
#include "testqgsvectorlayercache.moc"