/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * 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 3 of the
 * License, or (at your option) any later version.
 * 
 * This program 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "PrecompiledHeadersUnitTests.h"
#include <gtest/gtest.h>

#include "../../OrthancFramework/Sources/Compatibility.h"
#include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
#include "../../OrthancFramework/Sources/Images/Image.h"
#include "../../OrthancFramework/Sources/Logging.h"

#include "../Sources/Database/SQLiteDatabaseWrapper.h"
#include "../Sources/DicomInstanceToStore.h"
#include "../Sources/OrthancConfiguration.h"
#include "../Sources/Search/DatabaseLookup.h"
#include "../Sources/ServerContext.h"
#include "../Sources/ServerToolbox.h"

#include <ctype.h>
#include <algorithm>

using namespace Orthanc;

namespace
{
  class TestDatabaseListener : public IDatabaseListener
  {
  public:
    std::vector<std::string> deletedFiles_;
    std::vector<std::string> deletedResources_;
    std::string ancestorId_;
    ResourceType ancestorType_;

    void Reset()
    {
      ancestorId_ = "";
      deletedFiles_.clear();
    }

    virtual void SignalRemainingAncestor(ResourceType type,
                                         const std::string& publicId)
      ORTHANC_OVERRIDE
    {
      ancestorId_ = publicId;
      ancestorType_ = type;
    }

    virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE
    {
      const std::string fileUuid = info.GetUuid();
      deletedFiles_.push_back(fileUuid);
      LOG(INFO) << "A file must be removed: " << fileUuid;
    }       

    virtual void SignalResourceDeleted(ResourceType type,
                                       const std::string& publicId) ORTHANC_OVERRIDE
    {
      LOG(INFO) << "Deleted resource " << publicId << " of type " << EnumerationToString(type);
      deletedResources_.push_back(publicId);
    }
  };


  class DatabaseWrapperTest : public ::testing::Test
  {
  protected:
    std::unique_ptr<TestDatabaseListener>  listener_;
    std::unique_ptr<SQLiteDatabaseWrapper> index_;
    std::unique_ptr<SQLiteDatabaseWrapper::UnitTestsTransaction>  transaction_;

  public:
    DatabaseWrapperTest()
    {
    }

    virtual void SetUp() ORTHANC_OVERRIDE
    {
      listener_.reset(new TestDatabaseListener);
      index_.reset(new SQLiteDatabaseWrapper);
      index_->Open();
      transaction_.reset(dynamic_cast<SQLiteDatabaseWrapper::UnitTestsTransaction*>(
                           index_->StartTransaction(TransactionType_ReadWrite, *listener_)));
    }

    virtual void TearDown() ORTHANC_OVERRIDE
    {
      transaction_->Commit(0);
      transaction_.reset();
      
      index_->Close();
      index_.reset(NULL);
      listener_.reset(NULL);
    }

    void CheckTableRecordCount(uint32_t expected, const char* table)
    {
      ASSERT_EQ(expected, transaction_->GetTableRecordCount(table));
    }

    void CheckNoParent(int64_t id)
    {
      std::string s;
      ASSERT_FALSE(transaction_->GetParentPublicId(s, id));
    }

    void CheckParentPublicId(const char* expected, int64_t id)
    {
      std::string s;
      ASSERT_TRUE(transaction_->GetParentPublicId(s, id));
      ASSERT_EQ(expected, s);
    }

    void CheckNoChild(int64_t id)
    {
      std::list<std::string> j;
      transaction_->GetChildren(j, id);
      ASSERT_EQ(0u, j.size());
    }

    void CheckOneChild(const char* expected, int64_t id)
    {
      std::list<std::string> j;
      transaction_->GetChildren(j, id);
      ASSERT_EQ(1u, j.size());
      ASSERT_EQ(expected, j.front());
    }

    void CheckTwoChildren(const char* expected1,
                          const char* expected2,
                          int64_t id)
    {
      std::list<std::string> j;
      transaction_->GetChildren(j, id);
      ASSERT_EQ(2u, j.size());
      ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
                  (expected1 == j.back() && expected2 == j.front()));                    
    }

    void DoLookupIdentifier(std::list<std::string>& result,
                            ResourceType level,
                            const DicomTag& tag,
                            ConstraintType type,
                            const std::string& value)
    {
      assert(ServerToolbox::IsIdentifier(tag, level));
      result.clear();

      std::vector<std::string> values;
      values.push_back(value);

      std::unique_ptr<DatabaseDicomTagConstraint> lookup(new DatabaseDicomTagConstraint(level, tag, true, type, values, true, true));

      FindRequest request(level);
      request.GetDicomTagConstraints().AddConstraint(lookup.release());

      FindResponse response;

      transaction_->ExecuteFind(response, request, index_->GetDatabaseCapabilities());

      for (size_t i = 0; i < response.GetSize(); ++i)
      {
        result.push_back(response.GetResourceByIndex(i).GetIdentifier());
      }
    }    

    void DoLookupIdentifier2(std::list<std::string>& result,
                             ResourceType level,
                             const DicomTag& tag,
                             ConstraintType type1,
                             const std::string& value1,
                             ConstraintType type2,
                             const std::string& value2)
    {
      assert(ServerToolbox::IsIdentifier(tag, level));
      result.clear();

      std::vector<std::string> values1, values2;
      
      values1.push_back(value1);
      std::unique_ptr<DatabaseDicomTagConstraint> lookup1(new DatabaseDicomTagConstraint(level, tag, true, type1, values1, true, true));

      values2.push_back(value2);
      std::unique_ptr<DatabaseDicomTagConstraint> lookup2(new DatabaseDicomTagConstraint(level, tag, true, type2, values2, true, true));

      FindRequest request(level);
      request.GetDicomTagConstraints().AddConstraint(lookup1.release());
      request.GetDicomTagConstraints().AddConstraint(lookup2.release());

      FindResponse response;

      transaction_->ExecuteFind(response, request, index_->GetDatabaseCapabilities());

      for (size_t i = 0; i < response.GetSize(); ++i)
      {
        result.push_back(response.GetResourceByIndex(i).GetIdentifier());
      }

    }
  };

  class DummyTransactionContextFactory : public StatelessDatabaseOperations::ITransactionContextFactory
  {
  public:
    virtual StatelessDatabaseOperations::ITransactionContext* Create()
    {
      class DummyTransactionContext : public StatelessDatabaseOperations::ITransactionContext
      {
      public:
        virtual void SignalRemainingAncestor(ResourceType parentType,
                                             const std::string& publicId) ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }

        virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }

        virtual void SignalResourceDeleted(ResourceType type,
                                           const std::string& publicId) ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }

        virtual void Commit() ORTHANC_OVERRIDE
        {
        }

        virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE
        {
          return 0;
        }

        virtual bool IsUnstableResource(Orthanc::ResourceType type,
                                        int64_t id) ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }

        virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */,
                                          ResourceType& remainingLevel   /* out */) ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }

        virtual void MarkAsUnstable(Orthanc::ResourceType type,
                                    int64_t id,
                                    const std::string& publicId) ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }

        virtual void SignalAttachmentsAdded(uint64_t compressedSize)  ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }

        virtual void SignalChange(const ServerIndexChange& change)  ORTHANC_OVERRIDE
        {
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
        }
      };

      return new DummyTransactionContext;
    }
  };
}


TEST_F(DatabaseWrapperTest, Simple)
{
  int64_t a[] = {
    transaction_->CreateResource("a", ResourceType_Patient),   // 0
    transaction_->CreateResource("b", ResourceType_Study),     // 1
    transaction_->CreateResource("c", ResourceType_Series),    // 2
    transaction_->CreateResource("d", ResourceType_Instance),  // 3
    transaction_->CreateResource("e", ResourceType_Instance),  // 4
    transaction_->CreateResource("f", ResourceType_Instance),  // 5
    transaction_->CreateResource("g", ResourceType_Study)      // 6
  };

  ASSERT_EQ("a", transaction_->GetPublicId(a[0]));
  ASSERT_EQ("b", transaction_->GetPublicId(a[1]));
  ASSERT_EQ("c", transaction_->GetPublicId(a[2]));
  ASSERT_EQ("d", transaction_->GetPublicId(a[3]));
  ASSERT_EQ("e", transaction_->GetPublicId(a[4]));
  ASSERT_EQ("f", transaction_->GetPublicId(a[5]));
  ASSERT_EQ("g", transaction_->GetPublicId(a[6]));

  ASSERT_EQ(ResourceType_Patient, transaction_->GetResourceType(a[0]));
  ASSERT_EQ(ResourceType_Study, transaction_->GetResourceType(a[1]));
  ASSERT_EQ(ResourceType_Series, transaction_->GetResourceType(a[2]));
  ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[3]));
  ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[4]));
  ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[5]));
  ASSERT_EQ(ResourceType_Study, transaction_->GetResourceType(a[6]));

  {
    std::list<std::string> t;
    transaction_->GetAllPublicIds(t, ResourceType_Patient);

    ASSERT_EQ(1u, t.size());
    ASSERT_EQ("a", t.front());

    transaction_->GetAllPublicIds(t, ResourceType_Series);
    ASSERT_EQ(1u, t.size());
    ASSERT_EQ("c", t.front());

    transaction_->GetAllPublicIds(t, ResourceType_Study);
    ASSERT_EQ(2u, t.size());

    transaction_->GetAllPublicIds(t, ResourceType_Instance);
    ASSERT_EQ(3u, t.size());
  }

  transaction_->SetGlobalProperty(GlobalProperty_FlushSleep, true, "World");

  transaction_->AttachChild(a[0], a[1]);
  transaction_->AttachChild(a[1], a[2]);
  transaction_->AttachChild(a[2], a[3]);
  transaction_->AttachChild(a[2], a[4]);
  transaction_->AttachChild(a[6], a[5]);

  int64_t parent;
  ASSERT_FALSE(transaction_->LookupParent(parent, a[0]));
  ASSERT_TRUE(transaction_->LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent);
  ASSERT_TRUE(transaction_->LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent);
  ASSERT_TRUE(transaction_->LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent);
  ASSERT_TRUE(transaction_->LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent);
  ASSERT_TRUE(transaction_->LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent);
  ASSERT_FALSE(transaction_->LookupParent(parent, a[6]));

  std::string s;

  CheckNoParent(a[0]);
  CheckNoParent(a[6]);
  CheckParentPublicId("a", a[1]);
  CheckParentPublicId("b", a[2]);
  CheckParentPublicId("c", a[3]);
  CheckParentPublicId("c", a[4]);
  CheckParentPublicId("g", a[5]);

  std::list<std::string> l;
  transaction_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front());
  transaction_->GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front());
  transaction_->GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); 
  transaction_->GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); 
  transaction_->GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); 
  transaction_->GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front());

  transaction_->GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); 
  if (l.front() == "d")
  {
    ASSERT_EQ("e", l.back());
  }
  else
  {
    ASSERT_EQ("d", l.back());
    ASSERT_EQ("e", l.front());
  }

  std::map<MetadataType, std::string> md;
  transaction_->GetAllMetadata(md, a[4]);
  ASSERT_EQ(0u, md.size());

  FileInfo attachment1("my json file", FileContentType_DicomAsJson, 42, "md5",
                       CompressionType_ZlibWithSize, 21, "compressedMD5");
  attachment1.SetCustomData("hello");
  transaction_->AddAttachment(a[4], attachment1, 42);

  FileInfo attachment2("my dicom file", FileContentType_Dicom, 43, "md5_2");
  transaction_->AddAttachment(a[4], attachment2, 43);

  FileInfo attachment3("world", FileContentType_Dicom, 44, "md5_3");
  attachment3.SetCustomData("world");
  transaction_->AddAttachment(a[6], attachment3, 44);
  
  transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42);
  
  transaction_->GetAllMetadata(md, a[4]);
  ASSERT_EQ(1u, md.size());
  ASSERT_EQ("PINNACLE", md[MetadataType_RemoteAet]);
  transaction_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU", 10);
  transaction_->GetAllMetadata(md, a[4]);
  ASSERT_EQ(2u, md.size());

  std::map<MetadataType, std::string> md2;
  transaction_->GetAllMetadata(md2, a[4]);
  ASSERT_EQ(2u, md2.size());
  ASSERT_EQ("TUTU", md2[MetadataType_ModifiedFrom]);
  ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]);

  transaction_->DeleteMetadata(a[4], MetadataType_ModifiedFrom);
  transaction_->GetAllMetadata(md, a[4]);
  ASSERT_EQ(1u, md.size());
  ASSERT_EQ("PINNACLE", md[MetadataType_RemoteAet]);

  transaction_->GetAllMetadata(md2, a[4]);
  ASSERT_EQ(1u, md2.size());
  ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]);


  ASSERT_EQ(21u + 43u + 44u, transaction_->GetTotalCompressedSize());
  ASSERT_EQ(42u + 43u + 44u, transaction_->GetTotalUncompressedSize());

  transaction_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName");

  int64_t b;
  ResourceType t;
  ASSERT_TRUE(transaction_->LookupResource(b, t, "g"));
  ASSERT_EQ(7, b);
  ASSERT_EQ(ResourceType_Study, t);

  int64_t revision;
  ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet));
  ASSERT_EQ(42, revision);
  ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries));
  ASSERT_EQ(42, revision);
  ASSERT_EQ("PINNACLE", s);

  std::string u;
  ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet));
  ASSERT_EQ(42, revision);
  ASSERT_EQ("PINNACLE", u);
  ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries));
  ASSERT_EQ(42, revision);

  ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true));
  ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42), true));
  ASSERT_EQ("World", s);

  FileInfo att;
  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson));
  ASSERT_EQ(42, revision);
  ASSERT_EQ("my json file", att.GetUuid());
  ASSERT_EQ(21u, att.GetCompressedSize());
  ASSERT_EQ("md5", att.GetUncompressedMD5());
  ASSERT_EQ("compressedMD5", att.GetCompressedMD5());
  ASSERT_EQ(42u, att.GetUncompressedSize());
  ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
  ASSERT_EQ("hello", att.GetCustomData());

  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_Dicom));
  ASSERT_EQ(43, revision);
  ASSERT_EQ("my dicom file", att.GetUuid());
  ASSERT_EQ(43u, att.GetCompressedSize());
  ASSERT_EQ("md5_2", att.GetUncompressedMD5());
  ASSERT_EQ("md5_2", att.GetCompressedMD5());
  ASSERT_EQ(43u, att.GetUncompressedSize());
  ASSERT_EQ(CompressionType_None, att.GetCompressionType());
  ASSERT_TRUE(att.GetCustomData().empty());

  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom));
  ASSERT_EQ(44, revision);
  ASSERT_EQ("world", att.GetUuid());
  ASSERT_EQ(44u, att.GetCompressedSize());
  ASSERT_EQ("md5_3", att.GetUncompressedMD5());
  ASSERT_EQ("md5_3", att.GetCompressedMD5());
  ASSERT_EQ(44u, att.GetUncompressedSize());
  ASSERT_EQ(CompressionType_None, att.GetCompressionType());
  ASSERT_EQ("world", att.GetCustomData());

  ASSERT_EQ(0u, listener_->deletedFiles_.size());
  ASSERT_EQ(0u, listener_->deletedResources_.size());

  CheckTableRecordCount(7, "Resources");
  CheckTableRecordCount(3, "AttachedFiles");
  CheckTableRecordCount(1, "Metadata");
  CheckTableRecordCount(1, "MainDicomTags");

  transaction_->DeleteResource(a[0]);
  ASSERT_EQ(5u, listener_->deletedResources_.size());
  ASSERT_EQ(2u, listener_->deletedFiles_.size());
  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
                         listener_->deletedFiles_.end(),
                         "my json file") == listener_->deletedFiles_.end());
  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
                         listener_->deletedFiles_.end(),
                         "my dicom file") == listener_->deletedFiles_.end());

  CheckTableRecordCount(2, "Resources");
  CheckTableRecordCount(0, "Metadata");
  CheckTableRecordCount(1, "AttachedFiles");
  CheckTableRecordCount(0, "MainDicomTags");

  transaction_->DeleteResource(a[5]);
  ASSERT_EQ(7u, listener_->deletedResources_.size());

  CheckTableRecordCount(0, "Resources");
  CheckTableRecordCount(0, "AttachedFiles");
  CheckTableRecordCount(4, "GlobalProperties");

  std::string tmp;
  ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true));
  ASSERT_EQ("6", tmp);
  ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep, true));
  ASSERT_EQ("World", tmp);
  ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true));
  ASSERT_EQ("1", tmp);

  ASSERT_EQ(3u, listener_->deletedFiles_.size());
  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
                         listener_->deletedFiles_.end(),
                         "world") == listener_->deletedFiles_.end());
}


TEST_F(DatabaseWrapperTest, Upward)
{
  int64_t a[] = {
    transaction_->CreateResource("a", ResourceType_Patient),   // 0
    transaction_->CreateResource("b", ResourceType_Study),     // 1
    transaction_->CreateResource("c", ResourceType_Series),    // 2
    transaction_->CreateResource("d", ResourceType_Instance),  // 3
    transaction_->CreateResource("e", ResourceType_Instance),  // 4
    transaction_->CreateResource("f", ResourceType_Study),     // 5
    transaction_->CreateResource("g", ResourceType_Series),    // 6
    transaction_->CreateResource("h", ResourceType_Series)     // 7
  };

  transaction_->AttachChild(a[0], a[1]);
  transaction_->AttachChild(a[1], a[2]);
  transaction_->AttachChild(a[2], a[3]);
  transaction_->AttachChild(a[2], a[4]);
  transaction_->AttachChild(a[1], a[6]);
  transaction_->AttachChild(a[0], a[5]);
  transaction_->AttachChild(a[5], a[7]);

  CheckTwoChildren("b", "f", a[0]);
  CheckTwoChildren("c", "g", a[1]);
  CheckTwoChildren("d", "e", a[2]);
  CheckNoChild(a[3]);
  CheckNoChild(a[4]);
  CheckOneChild("h", a[5]);
  CheckNoChild(a[6]);
  CheckNoChild(a[7]);

  listener_->Reset();
  transaction_->DeleteResource(a[3]);
  ASSERT_EQ("c", listener_->ancestorId_);
  ASSERT_EQ(ResourceType_Series, listener_->ancestorType_);

  listener_->Reset();
  transaction_->DeleteResource(a[4]);
  ASSERT_EQ("b", listener_->ancestorId_);
  ASSERT_EQ(ResourceType_Study, listener_->ancestorType_);

  listener_->Reset();
  transaction_->DeleteResource(a[7]);
  ASSERT_EQ("a", listener_->ancestorId_);
  ASSERT_EQ(ResourceType_Patient, listener_->ancestorType_);

  listener_->Reset();
  transaction_->DeleteResource(a[6]);
  ASSERT_EQ("", listener_->ancestorId_);  // No more ancestor
}


TEST_F(DatabaseWrapperTest, PatientRecycling)
{
  std::vector<int64_t> patients;
  for (int i = 0; i < 10; i++)
  {
    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
    patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
    transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
    ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
  }

  CheckTableRecordCount(10u, "Resources");
  CheckTableRecordCount(10u, "PatientRecyclingOrder");

  listener_->Reset();
  ASSERT_EQ(0u, listener_->deletedResources_.size());

  transaction_->DeleteResource(patients[5]);
  transaction_->DeleteResource(patients[0]);
  ASSERT_EQ(2u, listener_->deletedResources_.size());

  CheckTableRecordCount(8u, "Resources");
  CheckTableRecordCount(8u, "PatientRecyclingOrder");

  ASSERT_EQ(2u, listener_->deletedFiles_.size());
  ASSERT_EQ("Patient 5", listener_->deletedFiles_[0]);
  ASSERT_EQ("Patient 0", listener_->deletedFiles_[1]);

  int64_t p;
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(3u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(4u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(5u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(6u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
  transaction_->DeleteResource(p);
  transaction_->DeleteResource(patients[8]);
  ASSERT_EQ(8u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(9u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
  transaction_->DeleteResource(p);
  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p));
  ASSERT_EQ(10u, listener_->deletedResources_.size());

  ASSERT_EQ(10u, listener_->deletedFiles_.size());

  CheckTableRecordCount(0, "Resources");
  CheckTableRecordCount(0, "PatientRecyclingOrder");
}


TEST_F(DatabaseWrapperTest, PatientProtection)
{
  std::vector<int64_t> patients;
  for (int i = 0; i < 5; i++)
  {
    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
    patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
    transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
    ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
  }

  CheckTableRecordCount(5, "Resources");
  CheckTableRecordCount(5, "PatientRecyclingOrder");

  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
  transaction_->SetProtectedPatient(patients[2], true);
  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2]));
  CheckTableRecordCount(5, "Resources");
  CheckTableRecordCount(4, "PatientRecyclingOrder");

  transaction_->SetProtectedPatient(patients[2], true);
  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2]));
  CheckTableRecordCount(4, "PatientRecyclingOrder");
  transaction_->SetProtectedPatient(patients[2], false);
  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
  CheckTableRecordCount(5, "PatientRecyclingOrder");
  transaction_->SetProtectedPatient(patients[2], false);
  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
  CheckTableRecordCount(5, "PatientRecyclingOrder");
  CheckTableRecordCount(5, "Resources");
  transaction_->SetProtectedPatient(patients[2], true);
  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2]));
  CheckTableRecordCount(4, "PatientRecyclingOrder");
  transaction_->SetProtectedPatient(patients[2], false);
  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
  CheckTableRecordCount(5, "PatientRecyclingOrder");
  transaction_->SetProtectedPatient(patients[3], true);
  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[3]));
  CheckTableRecordCount(4, "PatientRecyclingOrder");

  CheckTableRecordCount(5, "Resources");
  ASSERT_EQ(0u, listener_->deletedFiles_.size());

  // Unprotecting a patient puts it at the last position in the recycling queue
  int64_t p;
  ASSERT_EQ(0u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(1u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(2u, listener_->deletedResources_.size());
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(3u, listener_->deletedResources_.size());
  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p, patients[2]));
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(4u, listener_->deletedResources_.size());
  // "patients[3]" is still protected
  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p));

  ASSERT_EQ(4u, listener_->deletedFiles_.size());
  CheckTableRecordCount(1, "Resources");
  CheckTableRecordCount(0, "PatientRecyclingOrder");

  transaction_->SetProtectedPatient(patients[3], false);
  CheckTableRecordCount(1, "PatientRecyclingOrder");
  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p, patients[3]));
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p, patients[2]));
  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
  transaction_->DeleteResource(p);
  ASSERT_EQ(5u, listener_->deletedResources_.size());

  ASSERT_EQ(5u, listener_->deletedFiles_.size());
  CheckTableRecordCount(0, "Resources");
  CheckTableRecordCount(0, "PatientRecyclingOrder");
}


TEST(ServerIndex, Sequence)
{
  const std::string path = "UnitTestsStorage";

  SystemToolbox::RemoveFile(path + "/index");
  PluginStorageAreaAdapter storage(new FilesystemStorage(path));
  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
  db.Open();
  ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
  context.SetupJobsEngine(true, false);

  ServerIndex& index = context.GetIndex();

  ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));
  ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));
  ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));
  ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));

  context.Stop();
  db.Close();
}


TEST_F(DatabaseWrapperTest, LookupIdentifier)
{
  int64_t a[] = {
    transaction_->CreateResource("a", ResourceType_Study),   // 0
    transaction_->CreateResource("b", ResourceType_Study),   // 1
    transaction_->CreateResource("c", ResourceType_Study),   // 2
    transaction_->CreateResource("d", ResourceType_Series)   // 3
  };

  transaction_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0");
  transaction_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1");
  transaction_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
  transaction_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");

  std::list<std::string> s;

  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "0");
  ASSERT_EQ(2u, s.size());
  ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end());
  ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end());

  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "0");
  ASSERT_EQ(1u, s.size());
  ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end());

  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
  ASSERT_EQ(1u, s.size());
  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());

  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
  ASSERT_EQ(1u, s.size());
  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());

  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "1");
  ASSERT_EQ(0u, s.size());

  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "0");
  ASSERT_EQ(3u, s.size());

  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "1");
  ASSERT_EQ(1u, s.size());

  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "2");
  ASSERT_EQ(0u, s.size());

  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "0");
  ASSERT_EQ(2u, s.size());

  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
                      ConstraintType_GreaterOrEqual, "1", ConstraintType_SmallerOrEqual, "1");
  ASSERT_EQ(1u, s.size());

  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "1");
  ASSERT_EQ(3u, s.size());
}


TEST(ServerIndex, AttachmentRecycling)
{
  const std::string path = "UnitTestsStorage";

  SystemToolbox::RemoveFile(path + "/index");
  PluginStorageAreaAdapter storage(new FilesystemStorage(path));
  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
  db.Open();
  ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
  context.SetupJobsEngine(true, false);
  ServerIndex& index = context.GetIndex();

  index.SetMaximumStorageSize(10);

  uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                            countStudies, countSeries, countInstances);

  ASSERT_EQ(0u, countPatients);
  ASSERT_EQ(0u, diskSize);

  ServerIndex::Attachments attachments;

  std::vector<std::string> ids;
  for (int i = 0; i < 10; i++)
  {
    std::string id = boost::lexical_cast<std::string>(i);
    DicomMap instance;
    instance.SetValue(DICOM_TAG_PATIENT_ID, "patient-" + id, false);
    instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study-" + id, false);
    instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series-" + id, false);
    instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "instance-" + id, false);
    instance.SetValue(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);  // CR image

    ParsedDicomFile dicom(instance, GetDefaultDicomEncoding(), false /* be strict */);

    std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));

    std::map<MetadataType, std::string> instanceMetadata;

    {
      DicomMap summary;
      OrthancConfiguration::DefaultExtractDicomSummary(summary, toStore->GetParsedDicomFile());
      toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());

      DicomTransferSyntax transferSyntax;
      bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax);
      ASSERT_EQ(StoreStatus_Success, index.Store(
                  instanceMetadata, summary, attachments, toStore->GetMetadata(),
                  toStore->GetOrigin(), false /* don't overwrite */,
                  hasTransferSyntax, transferSyntax, true /* has pixel data */, 42 /* pixel data offset */,
                  ValueRepresentation_PersonName /* pixel data VR */, false));
    }
    
    ASSERT_EQ(8u, instanceMetadata.size());
    ASSERT_TRUE(instanceMetadata.find(MetadataType_RemoteAet) != instanceMetadata.end());
    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end());
    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_TransferSyntax) != instanceMetadata.end());
    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_SopClassUid) != instanceMetadata.end());
    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_PixelDataOffset) != instanceMetadata.end());
    ASSERT_TRUE(instanceMetadata.find(MetadataType_MainDicomTagsSignature) != instanceMetadata.end());
    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_PixelDataVR) != instanceMetadata.end());

    ASSERT_EQ("42", instanceMetadata[MetadataType_Instance_PixelDataOffset]);
    ASSERT_EQ("PN", instanceMetadata[MetadataType_Instance_PixelDataVR]);

    // The default transfer syntax depends on the OS endianness
    std::string s = instanceMetadata[MetadataType_Instance_TransferSyntax];
    ASSERT_TRUE(s == "1.2.840.10008.1.2.1" ||
                s == "1.2.840.10008.1.2.2");

    ASSERT_EQ("1.2.840.10008.5.1.4.1.1.1", instanceMetadata[MetadataType_Instance_SopClassUid]);

    DicomInstanceHasher hasher(instance);
    ids.push_back(hasher.HashPatient());
    ids.push_back(hasher.HashStudy());
    ids.push_back(hasher.HashSeries());
    ids.push_back(hasher.HashInstance());
  }

  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                            countStudies, countSeries, countInstances);
  ASSERT_EQ(10u, countPatients);
  ASSERT_EQ(0u, diskSize);

  for (size_t i = 0; i < ids.size(); i++)
  {
    FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
    int64_t revision = -1;
    index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1, "");
    ASSERT_EQ(0, revision);

    index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                              countStudies, countSeries, countInstances);
    ASSERT_GE(10u, diskSize);
  }

  // Because the DB is in memory, the SQLite index must not have been created
  ASSERT_FALSE(SystemToolbox::IsRegularFile(path + "/index"));

  context.Stop();
  db.Close();
}


TEST(ServerIndex, NormalizeIdentifier)
{
  ASSERT_EQ("H^L.LO", ServerToolbox::NormalizeIdentifier("   Hé^l.LO  %_  "));
  ASSERT_EQ("1.2.840.113619.2.176.2025", ServerToolbox::NormalizeIdentifier("   1.2.840.113619.2.176.2025  "));
}


TEST(ServerIndex, Overwrite)
{
  // Create a dummy 1x1 image
  Image image(PixelFormat_Grayscale8, 1, 1, false);
  reinterpret_cast<uint8_t*>(image.GetBuffer()) [0] = 128;

  for (unsigned int i = 0; i < 3; i++)
  {
    OverwriteInstancesMode overwriteMode;
    if (i == 0) 
      overwriteMode = OverwriteInstancesMode_Always;
    else if (i == 1)
      overwriteMode = OverwriteInstancesMode_Never;
    else if (i == 2)
      overwriteMode = OverwriteInstancesMode_IfChanged;

    PluginStorageAreaAdapter storage(new MemoryStorageArea);
    SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
    db.Open();
    ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
    context.SetupJobsEngine(true, false);
    context.SetCompressionEnabled(true);

    DicomMap instance;
    instance.SetValue(DICOM_TAG_PATIENT_ID, "patient", false);
    instance.SetValue(DICOM_TAG_PATIENT_NAME, "name", false);
    instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study", false);
    instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series", false);
    instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "sop", false);
    instance.SetValue(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);  // CR image

    DicomInstanceHasher hasher(instance);
    std::string id = hasher.HashInstance();
    context.SetOverwriteInstances(overwriteMode);

    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                           countStudies, countSeries, countInstances);

    ASSERT_EQ(0u, countInstances);
    ASSERT_EQ(0u, diskSize);

    {
      ParsedDicomFile dicom(instance, GetDefaultDicomEncoding(), false /* be strict */);

      // Add a pixel data so as to have one "FileContentType_DicomUntilPixelData"
      // (because of "context.SetCompressionEnabled(true)")
      dicom.EmbedImage(image);
      
      DicomInstanceHasher hasher(instance);
      
      std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
      toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());
      ASSERT_EQ(id, hasher.HashInstance());

      std::string id2;
      ServerContext::StoreResult result = context.Store(id2, *toStore);
      ASSERT_EQ(StoreStatus_Success, result.GetStatus());
      ASSERT_EQ(id, id2);
    }

    {
      FileInfo nope;
      int64_t revision;
      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, ResourceType_Instance, id, FileContentType_DicomAsJson));
    }

    FileInfo dicom1, pixelData1;
    int64_t revision = -1;
    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, ResourceType_Instance, id, FileContentType_Dicom));
    ASSERT_EQ(0, revision);
    revision = -1;
    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, ResourceType_Instance, id, FileContentType_DicomUntilPixelData));
    ASSERT_EQ(0, revision);

    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                           countStudies, countSeries, countInstances);
    ASSERT_EQ(1u, countInstances);
    ASSERT_EQ(dicom1.GetCompressedSize() + pixelData1.GetCompressedSize(), diskSize);
    ASSERT_EQ(dicom1.GetUncompressedSize() + pixelData1.GetUncompressedSize(), uncompressedSize);

    Json::Value tmp;
    context.ReadDicomAsJson(tmp, id);
    ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
    
    {
      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
      DicomDataSource::Dicom::Lock lock(*dicom);

      std::string tmp;
      lock.GetContent().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
      ASSERT_EQ("name", tmp);
    }

    { // upload the same file with a new PatientName
      DicomMap instance2;
      instance2.Assign(instance);
      instance2.SetValue(DICOM_TAG_PATIENT_NAME, "overwritten", false);

      ParsedDicomFile dicom(instance2, GetDefaultDicomEncoding(), false /* be strict */);

      // Add a pixel data so as to have one "FileContentType_DicomUntilPixelData"
      dicom.EmbedImage(image);

      std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
      toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());

      std::string id2;
      ServerContext::StoreResult result = context.Store(id2, *toStore);
      ASSERT_EQ(overwriteMode != OverwriteInstancesMode_Never ? StoreStatus_Success : StoreStatus_AlreadyStored, result.GetStatus());  // the PatientName has changed -> the result is Success in both overwrite modes
      ASSERT_EQ(id, id2);
    }

    {
      FileInfo nope;
      int64_t revision;
      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, ResourceType_Instance, id, FileContentType_DicomAsJson));
    }

    FileInfo dicom2, pixelData2;
    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, ResourceType_Instance, id, FileContentType_Dicom));
    ASSERT_EQ(0, revision);
    revision = -1;
    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, ResourceType_Instance, id, FileContentType_DicomUntilPixelData));
    ASSERT_EQ(0, revision);

    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                           countStudies, countSeries, countInstances);
    ASSERT_EQ(1u, countInstances);
    ASSERT_EQ(dicom2.GetCompressedSize() + pixelData2.GetCompressedSize(), diskSize);
    ASSERT_EQ(dicom2.GetUncompressedSize() + pixelData2.GetUncompressedSize(), uncompressedSize);

    if (overwriteMode == OverwriteInstancesMode_Always || overwriteMode == OverwriteInstancesMode_IfChanged)
    {
      ASSERT_NE(dicom1.GetUuid(), dicom2.GetUuid());
      ASSERT_NE(pixelData1.GetUuid(), pixelData2.GetUuid());
      ASSERT_NE(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize());
      ASSERT_NE(pixelData1.GetUncompressedSize(), pixelData2.GetUncompressedSize());
    
      context.ReadDicomAsJson(tmp, id);
      ASSERT_EQ("overwritten", tmp["0010,0010"]["Value"].asString());
    
      {
        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
        DicomDataSource::Dicom::Lock lock(*dicom);

        std::string tmp;
        lock.GetContent().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
        ASSERT_EQ("overwritten", tmp);
      }
    }
    else if (overwriteMode == OverwriteInstancesMode_Never)
    {
      ASSERT_EQ(dicom1.GetUuid(), dicom2.GetUuid());
      ASSERT_EQ(pixelData1.GetUuid(), pixelData2.GetUuid());
      ASSERT_EQ(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize());
      ASSERT_EQ(pixelData1.GetUncompressedSize(), pixelData2.GetUncompressedSize());

      context.ReadDicomAsJson(tmp, id);
      ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
    
      {
        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
        DicomDataSource::Dicom::Lock lock(*dicom);

        std::string tmp;
        lock.GetContent().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
        ASSERT_EQ("name", tmp);
      }
    }

    context.Stop();
    db.Close();
  }
}


TEST(ServerIndex, DicomUntilPixelData)
{
  // Create a dummy 1x1 image
  Image image(PixelFormat_Grayscale8, 1, 1, false);
  reinterpret_cast<uint8_t*>(image.GetBuffer()) [0] = 128;

  for (unsigned int i = 0; i < 2; i++)
  {
    const bool compression = (i == 0);
    
    PluginStorageAreaAdapter storage(new MemoryStorageArea);
    SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
    db.Open();
    ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
    context.SetupJobsEngine(true, false);
    context.SetCompressionEnabled(compression);

    for (unsigned int j = 0; j < 2; j++)
    {
      const bool withPixelData = (j == 0);

      ParsedDicomFile dicom(true);
      
      if (withPixelData)
      {
        dicom.EmbedImage(image);
      }

      std::string id;
      size_t dicomSize;

      {
        std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
        dicomSize = toStore->GetBufferSize();
        toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());
        ServerContext::StoreResult result = context.Store(id, *toStore);
        ASSERT_EQ(StoreStatus_Success, result.GetStatus());
      }

      std::set<FileContentType> attachments;
      context.GetIndex().ListAvailableAttachments(attachments, id, ResourceType_Instance);

      ASSERT_TRUE(attachments.find(FileContentType_Dicom) != attachments.end());
      
      if (compression &&
          withPixelData)
      {
        ASSERT_EQ(2u, attachments.size());
        ASSERT_TRUE(attachments.find(FileContentType_DicomUntilPixelData) != attachments.end());
      }
      else
      {
        ASSERT_EQ(1u, attachments.size());
      }

      std::string s;
      int64_t revision;
      bool found = context.GetIndex().LookupMetadata(s, revision, id, ResourceType_Instance,
                                                     MetadataType_Instance_PixelDataOffset);
      
      if (withPixelData)
      {
        ASSERT_TRUE(found);
        ASSERT_EQ(0, revision);
        ASSERT_GT(boost::lexical_cast<int>(s), 128 /* length of the DICOM preamble */);
        ASSERT_LT(boost::lexical_cast<size_t>(s), dicomSize);
      }
      else
      {
        ASSERT_FALSE(found);        
      }
    }
  }
}


TEST(ServerToolbox, ValidLabels)
{
  ASSERT_TRUE(ServerToolbox::IsValidLabel("abcdefghijklmnopqrstuvwxyz"
                                          "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
  ASSERT_TRUE(ServerToolbox::IsValidLabel("0123456789-_"));
  ASSERT_FALSE(ServerToolbox::IsValidLabel(""));
  ASSERT_FALSE(ServerToolbox::IsValidLabel(" "));
  ASSERT_FALSE(ServerToolbox::IsValidLabel("&"));
  ASSERT_FALSE(ServerToolbox::IsValidLabel("."));
}


TEST(SQLiteDatabaseWrapper, KeyValueStores)
{
  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
  db.Open();

  {
    StatelessDatabaseOperations op(db, false);
    op.SetTransactionContextFactory(new DummyTransactionContextFactory);

    for (unsigned int limit = 0; limit < 5; limit++)
    {
      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
      it.SetLimit(limit);
      ASSERT_THROW(it.GetValue(), OrthancException);
      ASSERT_THROW(it.GetKey(), OrthancException);
      ASSERT_FALSE(it.Next());
      ASSERT_THROW(it.GetValue(), OrthancException);
      ASSERT_THROW(it.GetKey(), OrthancException);
    }

    op.StoreKeyValue("test", "hello", "world");

    for (unsigned int limit = 0; limit < 5; limit++)
    {
      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
      it.SetLimit(limit);
      ASSERT_THROW(it.GetValue(), OrthancException);
      ASSERT_THROW(it.GetKey(), OrthancException);
      ASSERT_TRUE(it.Next());
      ASSERT_EQ("hello", it.GetKey());
      ASSERT_EQ("world", it.GetValue());
      ASSERT_FALSE(it.Next());
      ASSERT_THROW(it.GetValue(), OrthancException);
      ASSERT_THROW(it.GetKey(), OrthancException);
    }

    op.StoreKeyValue("test", "hello2", "world2");
    op.StoreKeyValue("test", "hello3", "world3");

    for (unsigned int limit = 0; limit < 5; limit++)
    {
      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
      it.SetLimit(limit);
      ASSERT_THROW(it.GetValue(), OrthancException);
      ASSERT_THROW(it.GetKey(), OrthancException);
      ASSERT_TRUE(it.Next());
      ASSERT_EQ("hello", it.GetKey());
      ASSERT_EQ("world", it.GetValue());
      ASSERT_TRUE(it.Next());
      ASSERT_EQ("hello2", it.GetKey());
      ASSERT_EQ("world2", it.GetValue());
      ASSERT_TRUE(it.Next());
      ASSERT_EQ("hello3", it.GetKey());
      ASSERT_EQ("world3", it.GetValue());
      ASSERT_FALSE(it.Next());
      ASSERT_THROW(it.GetValue(), OrthancException);
      ASSERT_THROW(it.GetKey(), OrthancException);
    }

    op.DeleteKeyValue("test", "hello2");

    for (unsigned int limit = 0; limit < 5; limit++)
    {
      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
      it.SetLimit(limit);
      ASSERT_TRUE(it.Next());
      ASSERT_EQ("hello", it.GetKey());
      ASSERT_EQ("world", it.GetValue());
      ASSERT_TRUE(it.Next());
      ASSERT_EQ("hello3", it.GetKey());
      ASSERT_EQ("world3", it.GetValue());
      ASSERT_FALSE(it.Next());
    }

    std::string s;
    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello"));   ASSERT_EQ("world", s);
    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello3"));  ASSERT_EQ("world3", s);
    ASSERT_FALSE(op.GetKeyValue(s, "test", "hello2"));

    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello"));   ASSERT_EQ("world", s);
    op.StoreKeyValue("test", "hello", "overwritten");
    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello"));   ASSERT_EQ("overwritten", s);

    op.DeleteKeyValue("test", "nope");

    op.DeleteKeyValue("test", "hello");
    op.DeleteKeyValue("test", "hello3");

    for (unsigned int limit = 0; limit < 5; limit++)
    {
      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
      it.SetLimit(limit);
      ASSERT_FALSE(it.Next());
    }

    {
      std::string blob;
      blob.push_back(0);
      blob.push_back(1);
      blob.push_back(0);
      blob.push_back(2);
      op.StoreKeyValue("test", "blob", blob); // Storing binary values
    }

    ASSERT_TRUE(op.GetKeyValue(s, "test", "blob"));
    ASSERT_EQ(4u, s.size());
    ASSERT_EQ(0, static_cast<uint8_t>(s[0]));
    ASSERT_EQ(1, static_cast<uint8_t>(s[1]));
    ASSERT_EQ(0, static_cast<uint8_t>(s[2]));
    ASSERT_EQ(2, static_cast<uint8_t>(s[3]));
    op.DeleteKeyValue("test", "blob");
    ASSERT_FALSE(op.GetKeyValue(s, "test", "blob"));
  }

  db.Close();
}


TEST(SQLiteDatabaseWrapper, Queues)
{
  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
  db.Open();

  {
    StatelessDatabaseOperations op(db, false);
    op.SetTransactionContextFactory(new DummyTransactionContextFactory);

    ASSERT_EQ(0u, op.GetQueueSize("test"));
    op.EnqueueValue("test", "hello");
    ASSERT_EQ(1u, op.GetQueueSize("test"));
    op.EnqueueValue("test", "world");
    ASSERT_EQ(2u, op.GetQueueSize("test"));

    std::string s;
    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back));  ASSERT_EQ("world", s);
    ASSERT_EQ(1u, op.GetQueueSize("test"));
    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back));  ASSERT_EQ("hello", s);
    ASSERT_EQ(0u, op.GetQueueSize("test"));
    ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Back));

    op.EnqueueValue("test", "hello");
    op.EnqueueValue("test", "world");
    ASSERT_EQ(2u, op.GetQueueSize("test"));

    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front));  ASSERT_EQ("hello", s);
    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front));  ASSERT_EQ("world", s);
    ASSERT_EQ(0u, op.GetQueueSize("test"));
    ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front));

    {
      std::string blob;
      blob.push_back(0);
      blob.push_back(1);
      blob.push_back(0);
      blob.push_back(2);
      op.EnqueueValue("test", blob); // Storing binary values
    }

    ASSERT_EQ(1u, op.GetQueueSize("test"));
    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front));
    ASSERT_EQ(0u, op.GetQueueSize("test"));
    ASSERT_EQ(4u, s.size());
    ASSERT_EQ(0, static_cast<uint8_t>(s[0]));
    ASSERT_EQ(1, static_cast<uint8_t>(s[1]));
    ASSERT_EQ(0, static_cast<uint8_t>(s[2]));
    ASSERT_EQ(2, static_cast<uint8_t>(s[3]));
    ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front));
  }

  db.Close();
}

TEST(SQLiteDatabaseWrapper, ReserveQueueValue)
{
  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
  db.Open();

  {
    StatelessDatabaseOperations op(db, false);
    op.SetTransactionContextFactory(new DummyTransactionContextFactory);

    ASSERT_EQ(0u, op.GetQueueSize("test"));
    op.EnqueueValue("test", "a");
    ASSERT_EQ(1u, op.GetQueueSize("test"));
    op.EnqueueValue("test", "b");
    ASSERT_EQ(2u, op.GetQueueSize("test"));
    op.EnqueueValue("test", "c");
    ASSERT_EQ(3u, op.GetQueueSize("test"));

    std::string s;
    uint64_t valueId0, valueId1, valueId2, valueId3;
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Back, 100));  
    ASSERT_EQ("c", s);
    ASSERT_EQ(3u, op.GetQueueSize("test"));  // the reserved values are still counted !
    
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId1, "test", QueueOrigin_Back, 100));  
    ASSERT_EQ("b", s);
    
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId2, "test", QueueOrigin_Back, 100));  
    ASSERT_EQ("a", s);
    ASSERT_EQ(3u, op.GetQueueSize("test"));

    ASSERT_FALSE(op.ReserveQueueValue(s, valueId3, "test", QueueOrigin_Back, 100));

    op.AcknowledgeQueueValue("test", valueId1);
    ASSERT_EQ(2u, op.GetQueueSize("test"));
    ASSERT_THROW(op.AcknowledgeQueueValue("test", valueId1), OrthancException);  // Cannot acknowledge twice
    op.AcknowledgeQueueValue("test", valueId2);
    op.AcknowledgeQueueValue("test", valueId0);
    ASSERT_EQ(0u, op.GetQueueSize("test"));

    op.EnqueueValue("test", "d");
    op.EnqueueValue("test", "e");
    op.EnqueueValue("test", "f");
    op.EnqueueValue("test", "g");
    op.EnqueueValue("test", "h");

    // reserve 2 values front and back and acknowledge only "e" & "f"
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Front, 1));  
    ASSERT_EQ("d", s);
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Front, 1));  
    ASSERT_EQ("e", s);
    op.AcknowledgeQueueValue("test", valueId0);
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Back, 1));  
    ASSERT_EQ("h", s);
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Back, 1));  
    ASSERT_EQ("g", s);
    op.AcknowledgeQueueValue("test", valueId0);

    // "DequeueValue()" must ignore the reserved values
    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front));
    ASSERT_EQ("f", s);

    SystemToolbox::USleep(2000000); // the granularity being the second, we might have to wait up to 2 seconds

    // "d" and "h" remain
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Front, 1));  
    ASSERT_EQ("d", s);
    ASSERT_TRUE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Back, 1));  
    ASSERT_EQ("h", s);

    // the queue is empty at this point since "f" has been reserved already
    ASSERT_FALSE(op.ReserveQueueValue(s, valueId0, "test", QueueOrigin_Back, 1));  
  }

  db.Close();
}


TEST_F(DatabaseWrapperTest, BinaryCustomData)
{
  int64_t patient = transaction_->CreateResource("Patient", ResourceType_Patient);

  {
    FileInfo info("hello", FileContentType_Dicom, 10, "md5");

    {
      std::string blob;
      blob.push_back(0);
      blob.push_back(1);
      blob.push_back(0);
      blob.push_back(2);
      info.SetCustomData(blob);
    }

    transaction_->AddAttachment(patient, info, 43);
  }

  {
    FileInfo info;
    int64_t revision;
    ASSERT_TRUE(transaction_->LookupAttachment(info, revision, patient, FileContentType_Dicom));
    ASSERT_EQ(43u, revision);
    ASSERT_EQ("hello", info.GetUuid());
    ASSERT_EQ(CompressionType_None, info.GetCompressionType());
    ASSERT_EQ(10u, info.GetCompressedSize());
    ASSERT_EQ("md5", info.GetCompressedMD5());
    ASSERT_EQ(4u, info.GetCustomData().size());
    ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[0]));
    ASSERT_EQ(1, static_cast<uint8_t>(info.GetCustomData()[1]));
    ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[2]));
    ASSERT_EQ(2, static_cast<uint8_t>(info.GetCustomData()[3]));
  }

  transaction_->DeleteResource(patient);
}
