diff --git a/core/Archive.php b/core/Archive.php
index 54133939724fc1917868e2060e48f75390282bba..80ef151f388792b03bd89454cb268c0be29d1da8 100644
--- a/core/Archive.php
+++ b/core/Archive.php
@@ -11,6 +11,7 @@ namespace Piwik;
 use Piwik\Archive\Parameters;
 use Piwik\ArchiveProcessor\Rules;
 use Piwik\Archive\ArchiveInvalidator;
+use Piwik\Container\StaticContainer;
 use Piwik\DataAccess\ArchiveSelector;
 use Piwik\Period\Factory as PeriodFactory;
 
@@ -166,6 +167,11 @@ class Archive
      */
     private static $cache;
 
+    /**
+     * @var ArchiveInvalidator
+     */
+    private $invalidator;
+
     /**
      * @param Parameters $params
      * @param bool $forceIndexedBySite Whether to force index the result of a query by site ID.
@@ -177,6 +183,8 @@ class Archive
         $this->params = $params;
         $this->forceIndexedBySite = $forceIndexedBySite;
         $this->forceIndexedByDate = $forceIndexedByDate;
+
+        $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
     }
 
     /**
@@ -539,8 +547,7 @@ class Archive
             return; // all requested site ids were already handled
         }
 
-        $invalidator  = new ArchiveInvalidator();
-        $sitesPerDays = $invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
+        $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
 
         foreach ($sitesPerDays as $date => $siteIds) {
             if (empty($siteIds)) {
@@ -554,7 +561,7 @@ class Archive
             }
 
             try {
-                $invalidator->markArchivesAsInvalidated($siteIdsToActuallyInvalidate, array(Date::factory($date)), false);
+                $this->invalidator->markArchivesAsInvalidated($siteIdsToActuallyInvalidate, array(Date::factory($date)), false);
             } catch (\Exception $e) {
                 Site::clearCache();
                 throw $e;
diff --git a/core/Archive/ArchiveInvalidator.php b/core/Archive/ArchiveInvalidator.php
index d89a2911e91fa10ba6867a1dc7f329c3cf78faac..6ecc5b8734765791df966cee4160ed54c745b43b 100644
--- a/core/Archive/ArchiveInvalidator.php
+++ b/core/Archive/ArchiveInvalidator.php
@@ -9,16 +9,16 @@
 
 namespace Piwik\Archive;
 
+use Piwik\Archive\ArchiveInvalidator\InvalidationResult;
 use Piwik\CronArchive\SitesToReprocessDistributedList;
 use Piwik\DataAccess\ArchiveTableCreator;
 use Piwik\DataAccess\Model;
 use Piwik\Date;
 use Piwik\Option;
-use Piwik\Piwik;
 use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
 use Piwik\Plugins\PrivacyManager\PrivacyManager;
 use Piwik\Period;
-use Piwik\Period\Week;
+use Piwik\Segment;
 
 /**
  * Service that can be used to invalidate archives or add archive references to a list so they will
@@ -45,12 +45,18 @@ use Piwik\Period\Week;
  */
 class ArchiveInvalidator
 {
-    private $warningDates = array();
-    private $processedDates = array();
-    private $minimumDateWithLogs = false;
-
     private $rememberArchivedReportIdStart = 'report_to_invalidate_';
 
+    /**
+     * @var Model
+     */
+    private $model;
+
+    public function __construct(Model $model)
+    {
+        $this->model = $model;
+    }
+
     public function rememberToInvalidateArchivedReportsLater($idSite, Date $date)
     {
         $key   = $this->buildRememberArchivedReportId($idSite, $date->toString());
@@ -120,17 +126,30 @@ class ArchiveInvalidator
      * @param $idSites int[]
      * @param $dates Date[]
      * @param $period string
-     * @return array
+     * @param $segment Segment
+     * @param bool $cascadeDown
+     * @return InvalidationResult
      * @throws \Exception
      */
-    public function markArchivesAsInvalidated(array $idSites, $dates, $period)
+    public function markArchivesAsInvalidated(array $idSites, array $dates, $period, Segment $segment = null, $cascadeDown = false)
     {
-        $dates = $this->removeDatesThatHaveBeenPurged($dates);
+        $invalidationInfo = new InvalidationResult();
+
+        $datesToInvalidate = $this->removeDatesThatHaveBeenPurged($dates, $invalidationInfo);
+
+        if (empty($period)) {
+            // if the period is empty, we don't need to cascade in any way, since we'll remove all periods
+            $periodDates = $this->getDatesByYearMonthAndPeriodType($dates);
+        } else {
+            $periods = $this->getPeriodsToInvalidate($datesToInvalidate, $period, $cascadeDown);
+            $periodDates = $this->getPeriodDatesByYearMonthAndPeriodType($periods);
+        }
 
-        $datesByMonth = $this->getDatesByYearMonth($dates);
+        $periodDates = $this->getUniqueDates($periodDates);
+        $this->markArchivesInvalidated($idSites, $periodDates, $segment);
 
-        $this->markArchivesInvalidatedFor($idSites, $period, $datesByMonth);
-        $this->persistInvalidatedArchives($idSites, $datesByMonth);
+        $yearMonths = array_keys($periodDates);
+        $this->markInvalidatedArchivesForReprocessAndPurge($idSites, $yearMonths);
 
         foreach ($idSites as $idSite) {
             foreach ($dates as $date) {
@@ -138,148 +157,161 @@ class ArchiveInvalidator
             }
         }
 
-        return $this->makeOutputLogs();
+        return $invalidationInfo;
     }
 
     /**
-     * @param $idSites
-     * @param $period string
-     * @param $datesByMonth array
-     * @throws \Exception
+     * @param string[][][] $periodDates
+     * @return string[][][]
      */
-    private function markArchivesInvalidatedFor($idSites, $period, $datesByMonth)
+    private function getUniqueDates($periodDates)
     {
-        $invalidateForPeriodId = $this->getPeriodId($period);
-
-        // In each table, invalidate day/week/month/year containing this date
-        $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
-
-        $archiveNumericTables = array_filter($archiveTables, function ($name) {
-            return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::NUMERIC_TABLE;
-        });
-
-        foreach ($archiveNumericTables as $table) {
-            // Extract Y_m from table name
-            $suffix = ArchiveTableCreator::getDateFromTableName($table);
-            if (!isset($datesByMonth[$suffix])) {
-                continue;
+        $result = array();
+        foreach ($periodDates as $yearMonth => $periodsByYearMonth) {
+            foreach ($periodsByYearMonth as $periodType => $periods) {
+                $result[$yearMonth][$periodType] = array_unique($periods);
             }
-            // Dates which are to be deleted from this table
-            $datesToDelete = $datesByMonth[$suffix];
-            self::getModel()->updateArchiveAsInvalidated($table, $idSites, $invalidateForPeriodId, $datesToDelete);
         }
+        return $result;
     }
 
     /**
      * @param Date[] $dates
-     * @return Date[]
+     * @param string $periodType
+     * @param bool $cascadeDown
+     * @return Period[]
      */
-    private function removeDatesThatHaveBeenPurged($dates)
+    private function getPeriodsToInvalidate($dates, $periodType, $cascadeDown)
     {
-        $this->findOlderDateWithLogs();
+        $periodsToInvalidate = array();
 
-        $result = array();
         foreach ($dates as $date) {
-            // we should only delete reports for dates that are more recent than N days
-            if ($this->minimumDateWithLogs
-                && $date->isEarlier($this->minimumDateWithLogs)
-            ) {
-                $this->warningDates[] = $date->toString();
-                continue;
+            if ($periodType == 'range') {
+                $date = $date . ',' . $date;
             }
 
-            $result[] = $date;
+            $period = Period\Factory::build($periodType, $date);
+            $periodsToInvalidate[] = $period;
+
+            if ($cascadeDown) {
+                $periodsToInvalidate = array_merge($periodsToInvalidate, $period->getAllOverlappingChildPeriods());
+            }
+
+            if ($periodType != 'year'
+                && $periodType != 'range'
+            ) {
+                $periodsToInvalidate[] = Period\Factory::build('year', $date);
+            }
         }
-        return $result;
+
+        return $periodsToInvalidate;
     }
 
-    private function findOlderDateWithLogs()
+    /**
+     * @param Period[] $periods
+     * @return string[][][]
+     */
+    private function getPeriodDatesByYearMonthAndPeriodType($periods)
     {
-        // If using the feature "Delete logs older than N days"...
-        $purgeDataSettings = PrivacyManager::getPurgeDataSettings();
-        $logsDeletedWhenOlderThanDays = $purgeDataSettings['delete_logs_older_than'];
-        $logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
+        $result = array();
+        foreach ($periods as $period) {
+            $date = $period->getDateStart();
+            $periodType = $period->getId();
 
-        if ($logsDeleteEnabled
-            && $logsDeletedWhenOlderThanDays
-        ) {
-            $this->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays);
+            $yearMonth = ArchiveTableCreator::getTableMonthFromDate($date);
+            $result[$yearMonth][$periodType][] = $date->toString();
         }
+        return $result;
     }
 
     /**
-     * Given the list of dates, process which tables YYYY_MM we should delete from
+     * Called when deleting all periods.
      *
-     * @param $datesToInvalidate Date[]
-     * @return array
+     * @param Date[] $dates
+     * @return string[][][]
      */
-    private function getDatesByYearMonth(array $datesToInvalidate)
+    private function getDatesByYearMonthAndPeriodType($dates)
     {
-        $datesByMonth = array();
-        foreach ($datesToInvalidate as $date) {
-            $this->processedDates[] = $date->toString();
-
-            $month = $date->toString('Y_m');
-            // For a given date, we must invalidate in the monthly archive table
-            $datesByMonth[$month][] = $date->toString();
-
-            // But also the year stored in January
-            $year = $date->toString('Y_01');
-            $datesByMonth[$year][] = $date->toString();
-
-            // but also weeks overlapping several months stored in the month where the week is starting
-            /* @var $week Week */
-            $week = Period\Factory::build('week', $date);
-            $weekAsString = $week->getDateStart()->toString('Y_m');
-            $datesByMonth[$weekAsString][] = $date->toString();
+        $result = array();
+        foreach ($dates as $date) {
+            $yearMonth = ArchiveTableCreator::getTableMonthFromDate($date);
+            $result[$yearMonth][null][] = $date->toString();
+
+            // since we're removing all periods, we must make sure to remove year periods as well.
+            // this means we have to make sure the january table is processed.
+            $janYearMonth = $date->toString('Y') . '_01';
+            $result[$janYearMonth][null][] = $date->toString();
         }
-        return $datesByMonth;
+        return $result;
     }
 
     /**
-     * @return array
+     * @param int[] $idSites
+     * @param string[][][] $dates
+     * @throws \Exception
      */
-    private function makeOutputLogs()
+    private function markArchivesInvalidated($idSites, $dates, Segment $segment = null)
     {
-        $output = array();
-        if ($this->warningDates) {
-            $output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: ' .
-                implode(", ", $this->warningDates) .
-                "\n The last day with logs is " . $this->minimumDateWithLogs . ". " .
-                "\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'.";
-        }
+        $archiveNumericTables = ArchiveTableCreator::getTablesArchivesInstalled($type = ArchiveTableCreator::NUMERIC_TABLE);
+        foreach ($archiveNumericTables as $table) {
+            $tableDate = ArchiveTableCreator::getDateFromTableName($table);
+            if (empty($dates[$tableDate])) {
+                continue;
+            }
 
-        $output[] = "Success. The following dates were invalidated successfully: " . implode(", ", $this->processedDates);
-        return $output;
+            $this->model->updateArchiveAsInvalidated($table, $idSites, $dates[$tableDate], $segment);
+        }
     }
 
     /**
-     * @param $period
-     * @return int|null
+     * @param Date[] $dates
+     * @param InvalidationResult $invalidationInfo
+     * @return \Piwik\Date[]
      */
-    private function getPeriodId($period)
+    private function removeDatesThatHaveBeenPurged($dates, InvalidationResult $invalidationInfo)
     {
-        return isset(Piwik::$idPeriods[$period]) ? Piwik::$idPeriods[$period] : null;
+        $this->findOlderDateWithLogs($invalidationInfo);
+
+        $result = array();
+        foreach ($dates as $date) {
+            // we should only delete reports for dates that are more recent than N days
+            if ($invalidationInfo->minimumDateWithLogs
+                && $date->isEarlier($invalidationInfo->minimumDateWithLogs)
+            ) {
+                $invalidationInfo->warningDates[] = $date->toString();
+                continue;
+            }
+
+            $result[] = $date;
+            $invalidationInfo->processedDates[] = $date->toString();
+        }
+        return $result;
+    }
+
+    private function findOlderDateWithLogs(InvalidationResult $info)
+    {
+        // If using the feature "Delete logs older than N days"...
+        $purgeDataSettings = PrivacyManager::getPurgeDataSettings();
+        $logsDeletedWhenOlderThanDays = (int)$purgeDataSettings['delete_logs_older_than'];
+        $logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
+
+        if ($logsDeleteEnabled
+            && $logsDeletedWhenOlderThanDays
+        ) {
+            $info->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays);
+        }
     }
 
     /**
      * @param array $idSites
-     * @param $datesByMonth
+     * @param array $yearMonths
      */
-    private function persistInvalidatedArchives(array $idSites, $datesByMonth)
+    private function markInvalidatedArchivesForReprocessAndPurge(array $idSites, $yearMonths)
     {
-        $yearMonths = array_keys($datesByMonth);
-        $yearMonths = array_unique($yearMonths);
-
         $store = new SitesToReprocessDistributedList();
         $store->add($idSites);
 
         $archivesToPurge = new ArchivesToPurgeDistributedList();
         $archivesToPurge->add($yearMonths);
     }
-
-    private static function getModel()
-    {
-        return new Model();
-    }
 }
diff --git a/core/Archive/ArchiveInvalidator/InvalidationResult.php b/core/Archive/ArchiveInvalidator/InvalidationResult.php
new file mode 100644
index 0000000000000000000000000000000000000000..517e113841329a860d3ec671c4f15b04a63bb340
--- /dev/null
+++ b/core/Archive/ArchiveInvalidator/InvalidationResult.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Archive\ArchiveInvalidator;
+
+use Piwik\Date;
+
+/**
+ * Information about the result of an archive invalidation operation.
+ */
+class InvalidationResult
+{
+    /**
+     * Dates that couldn't be invalidated because they are earlier than the configured log
+     * deletion limit.
+     *
+     * @var array
+     */
+    public $warningDates = array();
+
+    /**
+     * Dates that were successfully invalidated.
+     *
+     * @var array
+     */
+    public $processedDates = array();
+
+    /**
+     * The day of the oldest log entry.
+     *
+     * @var Date|bool
+     */
+    public $minimumDateWithLogs = false;
+
+    /**
+     * @return string[]
+     */
+    public function makeOutputLogs()
+    {
+        $output = array();
+        if ($this->warningDates) {
+            $output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: ' .
+                implode(", ", $this->warningDates) .
+                "\n The last day with logs is " . $this->minimumDateWithLogs . ". " .
+                "\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'.";
+        }
+
+        $output[] = "Success. The following dates were invalidated successfully: " . implode(", ", $this->processedDates);
+        return $output;
+    }
+}
\ No newline at end of file
diff --git a/core/ArchiveProcessor/Rules.php b/core/ArchiveProcessor/Rules.php
index caa5a627fb4c85736543ae94cf0906a292b8df57..0ec92f75907dd623c015a08256e3c7dcb3c3a053 100644
--- a/core/ArchiveProcessor/Rules.php
+++ b/core/ArchiveProcessor/Rules.php
@@ -86,7 +86,7 @@ class Rules
         return 'done' . $segment->getHash() . '.' . $plugin ;
     }
 
-    private static function getDoneFlagArchiveContainsAllPlugins(Segment $segment)
+    public static function getDoneFlagArchiveContainsAllPlugins(Segment $segment)
     {
         return 'done' . $segment->getHash();
     }
diff --git a/core/CronArchive.php b/core/CronArchive.php
index e33b918154e2b87ea8bcf98f9ccbefe50aacd94c..4b9d3556d3f43413918c9e9967dd8afa12ca429f 100644
--- a/core/CronArchive.php
+++ b/core/CronArchive.php
@@ -237,6 +237,11 @@ class CronArchive
      */
     private $urlToPiwik = null;
 
+    /**
+     * @var ArchiveInvalidator
+     */
+    private $invalidator;
+
     /**
      * Returns the option name of the option that stores the time core:archive was last executed.
      *
@@ -262,6 +267,8 @@ class CronArchive
 
         $processNewSegmentsFrom = $processNewSegmentsFrom ?: StaticContainer::get('ini.General.process_new_segments_from');
         $this->segmentArchivingRequestUrlProvider = new SegmentArchivingRequestUrlProvider($processNewSegmentsFrom);
+
+        $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
     }
 
     /**
@@ -1069,8 +1076,7 @@ class CronArchive
 
     public function invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain()
     {
-        $invalidator  = new ArchiveInvalidator();
-        $sitesPerDays = $invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
+        $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
 
         foreach ($sitesPerDays as $date => $siteIds) {
             $listSiteIds = implode(',', $siteIds);
diff --git a/core/DataAccess/ArchiveTableCreator.php b/core/DataAccess/ArchiveTableCreator.php
index ca94b7239997b0b19a027fd0d1901119e66bf188..ad95863da48ec61c97d745489b76e3e27efd2439 100644
--- a/core/DataAccess/ArchiveTableCreator.php
+++ b/core/DataAccess/ArchiveTableCreator.php
@@ -71,24 +71,27 @@ class ArchiveTableCreator
     /**
      * Returns all table names archive_*
      *
+     * @param string $type The type of table to return. Either `self::NUMERIC_TABLE` or `self::BLOB_TABLE`.
      * @return array
      */
-    public static function getTablesArchivesInstalled()
+    public static function getTablesArchivesInstalled($type = null)
     {
         if (is_null(self::$tablesAlreadyInstalled)) {
             self::refreshTableList();
         }
 
-        $archiveTables = array();
+        if (empty($type)) {
+            $tableMatchRegex = '/archive_(numeric|blob)_/';
+        } else {
+            $tableMatchRegex = '/archive_' . preg_quote($type) . '_/';
+        }
 
+        $archiveTables = array();
         foreach (self::$tablesAlreadyInstalled as $table) {
-            if (strpos($table, 'archive_numeric_') !== false
-                || strpos($table, 'archive_blob_') !== false
-            ) {
+            if (preg_match($tableMatchRegex, $table)) {
                 $archiveTables[] = $table;
             }
         }
-
         return $archiveTables;
     }
 
diff --git a/core/DataAccess/Model.php b/core/DataAccess/Model.php
index a83db1317a5c99be3fa466563d1351089b6cebb7..4213cd203950c1f1ea06e9332e61e3dfec0ed154 100644
--- a/core/DataAccess/Model.php
+++ b/core/DataAccess/Model.php
@@ -9,10 +9,13 @@
 namespace Piwik\DataAccess;
 
 use Exception;
+use Piwik\ArchiveProcessor\Rules;
 use Piwik\Common;
 use Piwik\Container\StaticContainer;
 use Piwik\Db;
 use Piwik\DbHelper;
+use Piwik\Period;
+use Piwik\Segment;
 use Piwik\Sequence;
 use Psr\Log\LoggerInterface;
 
@@ -95,39 +98,55 @@ class Model
     }
 
     /**
-     * @param $archiveTable
-     * @param $idSites
-     * @param $periodId
-     * @param $datesToDelete
+     * @param string $archiveTable Prefixed table name
+     * @param int[] $idSites
+     * @param string[][] $datesByPeriodType
+     * @param Segment $segment
+     * @return \Zend_Db_Statement
      * @throws Exception
      */
-    public function updateArchiveAsInvalidated($archiveTable, $idSites, $periodId, $datesToDelete)
+    public function updateArchiveAsInvalidated($archiveTable, $idSites, $datesByPeriodType, Segment $segment = null)
     {
-        $sql = $bind = array();
-        $datesToDelete = array_unique($datesToDelete);
-        foreach ($datesToDelete as $dateToDelete) {
-            $sql[] = '(date1 <= ? AND ? <= date2 AND name LIKE \'done%\')';
-            $bind[] = $dateToDelete;
-            $bind[] = $dateToDelete;
-        }
-        $sql = implode(" OR ", $sql);
+        $idSites = array_map('intval', $idSites);
+
+        $bind = array();
 
-        $idSites = array_values($idSites);
-        $sqlSites = " AND idsite IN (" . Common::getSqlStringFieldsArray($idSites) . ")";
-        $bind = array_merge($bind, $idSites);
+        $periodConditions = array();
+        foreach ($datesByPeriodType as $periodType => $dates) {
+            $dateConditions = array();
 
-        $sqlPeriod = "";
-        if ($periodId) {
-            $sqlPeriod = " AND period = ? ";
-            $bind[] = $periodId;
+            foreach ($dates as $date) {
+                $dateConditions[] = "(date1 <= ? AND ? <= date2)";
+                $bind[] = $date;
+                $bind[] = $date;
+            }
+
+            $dateConditionsSql = implode(" OR ", $dateConditions);
+            if (empty($periodType)
+                || $periodType == Period\Day::PERIOD_ID
+            ) {
+                // invalidate all periods if no period supplied or period is day
+                $periodConditions[] = "($dateConditionsSql)";
+            } else if ($periodType == Period\Range::PERIOD_ID) {
+                $periodConditions[] = "(period = " . Period\Range::PERIOD_ID . " AND ($dateConditionsSql))";
+            } else {
+                // for non-day periods, invalidate greater periods, but not range periods
+                $periodConditions[] = "(period >= " . (int)$periodType . " AND period < " . Period\Range::PERIOD_ID . " AND ($dateConditionsSql))";
+            }
+        }
+
+        if ($segment) {
+            $nameCondition = "name LIKE '" . Rules::getDoneFlagArchiveContainsAllPlugins($segment) . "%'";
+        } else {
+            $nameCondition = "name LIKE 'done%'";
         }
 
-        $query = "UPDATE $archiveTable " .
-            " SET value = " . ArchiveWriter::DONE_INVALIDATED .
-            " WHERE ( $sql ) " .
-            $sqlSites .
-            $sqlPeriod;
-        Db::query($query, $bind);
+        $sql = "UPDATE $archiveTable SET value = " . ArchiveWriter::DONE_INVALIDATED
+             . " WHERE $nameCondition
+                   AND idsite IN (" . implode(", ", $idSites) . ")
+                   AND (" . implode(" OR ", $periodConditions) . ")";
+
+        return Db::query($sql, $bind);
     }
 
 
@@ -157,7 +176,7 @@ class Model
             // Individual blob tables could be missing
             $this->logger->debug("Unable to delete archives by period from {blobTable}.", array(
                 'blobTable' => $blobTable,
-                'exception' => $e
+                'exception' => $e,
             ));
         }
 
@@ -179,7 +198,7 @@ class Model
             // Individual blob tables could be missing
             $this->logger->debug("Unable to delete archive IDs from {blobTable}.", array(
                 'blobTable' => $blobTable,
-                'exception' => $e
+                'exception' => $e,
             ));
         }
 
diff --git a/core/Period.php b/core/Period.php
index 80dc9f6f21b145f0de2af82cfd6b4d9d0f096a77..70a47d29055c8fdb8bc2eacea58780f98fee4da3 100644
--- a/core/Period.php
+++ b/core/Period.php
@@ -9,6 +9,7 @@
 namespace Piwik;
 
 use Piwik\Container\StaticContainer;
+use Piwik\Period\Factory;
 use Piwik\Period\Range;
 use Piwik\Translation\Translator;
 
@@ -278,6 +279,31 @@ abstract class Period
      */
     abstract public function getLocalizedLongString();
 
+    /**
+     * Returns the label of the period type that is one size smaller than this one, or null if
+     * it's the smallest.
+     *
+     * Range periods and other such 'period collections' are not considered as separate from
+     * the value type of the collection. So a range period will return the result of the
+     * subperiod's `getImmediateChildPeriodLabel()` method.
+     *
+     * @ignore
+     * @return string|null
+     */
+    abstract public function getImmediateChildPeriodLabel();
+
+    /**
+     * Returns the label of the period type that is one size bigger than this one, or null
+     * if it's the biggest.
+     *
+     * Range periods and other such 'period collections' are not considered as separate from
+     * the value type of the collection. So a range period will return the result of the
+     * subperiod's `getParentPeriodLabel()` method.
+     *
+     * @ignore
+     */
+    abstract public function getParentPeriodLabel();
+
     /**
      * Returns the date range string comprising two dates
      *
@@ -291,7 +317,6 @@ abstract class Period
         return $dateStart->toString("Y-m-d") . "," . $dateEnd->toString("Y-m-d");
     }
 
-
     /**
      * @param string $format
      *
@@ -353,4 +378,33 @@ abstract class Period
                 $maxDifference
             ));
     }
+
+    /**
+     * Returns all child periods that exist within this periods entire date range. Cascades
+     * downwards over all period types that are smaller than this one. For example, month periods
+     * will cascade to week and day periods and year periods will cascade to month, week and day
+     * periods.
+     *
+     * The method will not return periods that are outside the range of this period.
+     *
+     * @return Period[]
+     * @ignore
+     */
+    public function getAllOverlappingChildPeriods()
+    {
+        return $this->getAllOverlappingChildPeriodsInRange($this->getDateStart(), $this->getDateEnd());
+    }
+
+    private function getAllOverlappingChildPeriodsInRange(Date $dateStart, Date $dateEnd)
+    {
+        $result = array();
+
+        $childPeriodType = $this->getImmediateChildPeriodLabel();
+        if (empty($childPeriodType)) {
+            return $result;
+        }
+
+        $childPeriods = Factory::build($childPeriodType, $dateStart->toString() . ',' . $dateEnd->toString());
+        return array_merge($childPeriods->getSubperiods(), $childPeriods->getAllOverlappingChildPeriodsInRange($dateStart, $dateEnd));
+    }
 }
diff --git a/core/Period/Day.php b/core/Period/Day.php
index 4933ddedf6c78b75d694b31f12922794fcb5c537..bcad4cc1a631abb0305ec1b17c2104a5ab8d162c 100644
--- a/core/Period/Day.php
+++ b/core/Period/Day.php
@@ -17,6 +17,8 @@ use Piwik\Piwik;
  */
 class Day extends Period
 {
+    const PERIOD_ID = 1;
+
     protected $label = 'day';
 
     /**
@@ -99,4 +101,14 @@ class Day extends Period
     {
         return $this->toString();
     }
+
+    public function getImmediateChildPeriodLabel()
+    {
+        return null;
+    }
+
+    public function getParentPeriodLabel()
+    {
+        return 'week';
+    }
 }
diff --git a/core/Period/Month.php b/core/Period/Month.php
index 4c779e48537b9c7bfa2a33ac19afab995eba7bbd..7a52bd06f8096e849cf83e53e356ed567b1e9327 100644
--- a/core/Period/Month.php
+++ b/core/Period/Month.php
@@ -15,6 +15,8 @@ use Piwik\Period;
  */
 class Month extends Period
 {
+    const PERIOD_ID = 3;
+
     protected $label = 'month';
 
     /**
@@ -111,4 +113,14 @@ class Month extends Period
             $startDate = $startDate->addDay(1);
         }
     }
+
+    public function getImmediateChildPeriodLabel()
+    {
+        return 'week';
+    }
+
+    public function getParentPeriodLabel()
+    {
+        return 'year';
+    }
 }
diff --git a/core/Period/Range.php b/core/Period/Range.php
index e2b654ca2154c08069ce3415589d434504172ab0..0625b2da47aa18733d2dbc82c9e8649a6051bdba 100644
--- a/core/Period/Range.php
+++ b/core/Period/Range.php
@@ -31,6 +31,8 @@ use Piwik\Piwik;
  */
 class Range extends Period
 {
+    const PERIOD_ID = 5;
+
     protected $label = 'range';
     protected $today;
 
@@ -513,4 +515,15 @@ class Range extends Period
 
         return $dateStart->toString("Y-m-d") . "," . $dateEnd->toString("Y-m-d");
     }
+
+    public function getImmediateChildPeriodLabel()
+    {
+        $subperiods = $this->getSubperiods();
+        return reset($subperiods)->getImmediateChildPeriodLabel();
+    }
+
+    public function getParentPeriodLabel()
+    {
+        return null;
+    }
 }
diff --git a/core/Period/Week.php b/core/Period/Week.php
index ff2a23a844d35945dd468caa2eff9b1a1761ba39..db644be265b9c1b970ec165bd06e20f2581fa2c4 100644
--- a/core/Period/Week.php
+++ b/core/Period/Week.php
@@ -15,6 +15,8 @@ use Piwik\Piwik;
  */
 class Week extends Period
 {
+    const PERIOD_ID = 2;
+
     protected $label = 'week';
 
     /**
@@ -77,4 +79,14 @@ class Week extends Period
             $currentDay = $currentDay->addDay(1);
         }
     }
+
+    public function getImmediateChildPeriodLabel()
+    {
+        return 'day';
+    }
+
+    public function getParentPeriodLabel()
+    {
+        return 'month';
+    }
 }
diff --git a/core/Period/Year.php b/core/Period/Year.php
index cfda052bf575c98a8a7cc9f06052fa272a6e0154..208c6bbfac1858de4fcb63fb0540634b76b97ae2 100644
--- a/core/Period/Year.php
+++ b/core/Period/Year.php
@@ -15,6 +15,8 @@ use Piwik\Period;
  */
 class Year extends Period
 {
+    const PERIOD_ID = 4;
+
     protected $label = 'year';
 
     /**
@@ -87,4 +89,14 @@ class Year extends Period
 
         return $stringMonth;
     }
+
+    public function getImmediateChildPeriodLabel()
+    {
+        return 'month';
+    }
+
+    public function getParentPeriodLabel()
+    {
+        return null;
+    }
 }
diff --git a/core/Piwik.php b/core/Piwik.php
index 5ad50713834ca6428a4c5a3e5872ba6189caf241..9545cff6c8e260c58a2c10cc8ac0e36599e66e59 100644
--- a/core/Piwik.php
+++ b/core/Piwik.php
@@ -10,6 +10,11 @@ namespace Piwik;
 
 use Exception;
 use Piwik\Container\StaticContainer;
+use Piwik\Period\Day;
+use Piwik\Period\Month;
+use Piwik\Period\Range;
+use Piwik\Period\Week;
+use Piwik\Period\Year;
 use Piwik\Plugins\UsersManager\API as APIUsersManager;
 use Piwik\Translation\Translator;
 
@@ -31,11 +36,11 @@ class Piwik
      * @var array
      */
     public static $idPeriods = array(
-        'day'   => 1,
-        'week'  => 2,
-        'month' => 3,
-        'year'  => 4,
-        'range' => 5,
+        'day'   => Day::PERIOD_ID,
+        'week'  => Week::PERIOD_ID,
+        'month' => Month::PERIOD_ID,
+        'year'  => Year::PERIOD_ID,
+        'range' => Range::PERIOD_ID,
     );
 
     /**
diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php
index fc11d0cc3f80c62ea858e88e2ae6b4ca8a88c667..84b702449e0b0083320e5b3bad74340ca8c4531f 100644
--- a/core/Tracker/Visit.php
+++ b/core/Tracker/Visit.php
@@ -69,12 +69,18 @@ class Visit implements VisitInterface
      */
     private $visitorRecognizer;
 
+    /**
+     * @var ArchiveInvalidator
+     */
+    private $invalidator;
+
     public function __construct()
     {
         $this->requestProcessors = StaticContainer::get('tracker.request.processors');
         $this->visitorRecognizer = StaticContainer::get('Piwik\Tracker\VisitorRecognizer');
         $this->visitProperties = null;
         $this->userSettings = StaticContainer::get('Piwik\Tracker\Settings');
+        $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
     }
 
     /**
@@ -566,8 +572,7 @@ class Visit implements VisitInterface
         $date = Date::factory((int)$time, $timezone);
 
         if (!$date->isToday()) { // we don't have to handle in case date is in future as it is not allowed by tracker
-            $invalidReport = new ArchiveInvalidator();
-            $invalidReport->rememberToInvalidateArchivedReportsLater($idSite, $date);
+            $this->invalidator->rememberToInvalidateArchivedReportsLater($idSite, $date);
         }
     }
 
diff --git a/plugins/CoreAdminHome/API.php b/plugins/CoreAdminHome/API.php
index 119d56d74f92b2a22da0349d7e68ad2b4e1c0039..18f5764a5a2c1441fa84fc1bc8ebf50aca53dfbb 100644
--- a/plugins/CoreAdminHome/API.php
+++ b/plugins/CoreAdminHome/API.php
@@ -17,9 +17,9 @@ use Piwik\CronArchive;
 use Piwik\Date;
 use Piwik\Db;
 use Piwik\Piwik;
+use Piwik\Segment;
 use Piwik\Scheduler\Scheduler;
 use Piwik\Site;
-use Psr\Log\LoggerInterface;
 
 /**
  * @method static \Piwik\Plugins\CoreAdminHome\API getInstance()
@@ -31,9 +31,15 @@ class API extends \Piwik\Plugin\API
      */
     private $scheduler;
 
-    public function __construct(Scheduler $scheduler)
+    /**
+     * @var ArchiveInvalidator
+     */
+    private $invalidator;
+
+    public function __construct(Scheduler $scheduler, ArchiveInvalidator $invalidator)
     {
         $this->scheduler = $scheduler;
+        $this->invalidator = $invalidator;
     }
 
     /**
@@ -50,28 +56,25 @@ class API extends \Piwik\Plugin\API
     }
 
     /**
-     * When tracking data in the past (using Tracking API), this function
-     * can be used to invalidate reports for the idSites and dates where new data
-     * was added.
-     * DEV: If you call this API, the UI should display the data correctly, but will process
-     *      in real time, which could be very slow after large data imports.
-     *      After calling this function via REST, you can manually force all data
-     *      to be reprocessed by visiting the script as the Super User:
-     *      http://example.net/piwik/misc/cron/archive.php?token_auth=$SUPER_USER_TOKEN_AUTH_HERE
-     * REQUIREMENTS: On large piwik setups, you will need in PHP configuration: max_execution_time = 0
-     *    We recommend to use an hourly schedule of the script.
-     *    More information: http://piwik.org/setup-auto-archiving/
+     * Invalidates report data, forcing it to be recomputed during the next archiving run.
      *
-     * @param string $idSites Comma separated list of idSite that have had data imported for the specified dates
-     * @param string $dates Comma separated list of dates to invalidate for all these websites
-     * @param string $period If specified (one of day, week, month, year, range) it will only invalidates archives for this period.
-     *                      Note: because week, month, year, range reports aggregate day reports then you need to specifically invalidate day reports to see
-     *                      other periods reports processed..
+     * Note: This is done automatically when tracking or importing visits in the past.
+     *
+     * @param string $idSites Comma separated list of site IDs to invalidate reports for.
+     * @param string $dates Comma separated list of dates of periods to invalidate reports for.
+     * @param string|bool $period The type of period to invalidate: either 'day', 'week', 'month', 'year', 'range'.
+     *                            The command will automatically cascade up, invalidating reports for parent periods as
+     *                            well. So invalidating a day will invalidate the week it's in, the month it's in and the
+     *                            year it's in, since those periods will need to be recomputed too.
+     * @param string|bool $segment Optional. The segment to invalidate reports for.
+     * @param bool $cascadeDown If true, child periods will be invalidated as well. So if it is requested to invalidate
+     *                          a month, then all the weeks and days within that month will also be invalidated. But only
+     *                          if this parameter is set.
      * @throws Exception
      * @return array
      * @hideExceptForSuperUser
      */
-    public function invalidateArchivedReports($idSites, $dates, $period = false)
+    public function invalidateArchivedReports($idSites, $dates, $period = false, $segment = false, $cascadeDown = false)
     {
         $idSites = Site::getIdSitesFromIdSitesString($idSites);
         if (empty($idSites)) {
@@ -80,19 +83,25 @@ class API extends \Piwik\Plugin\API
 
         Piwik::checkUserHasAdminAccess($idSites);
 
+        if (!empty($segment)) {
+            $segment = new Segment($segment, $idSites);
+        } else {
+            $segment = null;
+        }
+
         list($dateObjects, $invalidDates) = $this->getDatesToInvalidateFromString($dates);
 
-        $invalidator = new ArchiveInvalidator();
-        $output = $invalidator->markArchivesAsInvalidated($idSites, $dateObjects, $period);
+        $invalidationResult = $this->invalidator->markArchivesAsInvalidated($idSites, $dateObjects, $period, $segment, (bool)$cascadeDown);
 
+        $output = $invalidationResult->makeOutputLogs();
         if ($invalidDates) {
             $output[] = 'Warning: some of the Dates to invalidate were invalid: ' .
                 implode(", ", $invalidDates) . ". Piwik simply ignored those and proceeded with the others.";
         }
 
-        Site::clearCache();
+        Site::clearCache(); // TODO: is this needed? it shouldn't be needed...
 
-        return $output;
+        return $invalidationResult->makeOutputLogs();
     }
 
     /**
diff --git a/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php b/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php
index fbfbcf1df05fc979da95578114d756636366ae85..cfbfdf82b148674432e55a6fa90d93a7ae1bc4ec 100644
--- a/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php
+++ b/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php
@@ -76,7 +76,7 @@ class FixDuplicateLogActions extends ConsoleCommand
     {
         parent::__construct();
 
-        $this->archiveInvalidator = $invalidator ?: new ArchiveInvalidator();
+        $this->archiveInvalidator = $invalidator ?: StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
         $this->duplicateActionRemover = $duplicateActionRemover ?: new DuplicateActionRemover();
         $this->actionsAccess = $actionsAccess ?: new Actions();
         $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
diff --git a/plugins/CoreAdminHome/Commands/InvalidateReportData.php b/plugins/CoreAdminHome/Commands/InvalidateReportData.php
new file mode 100644
index 0000000000000000000000000000000000000000..f424afc8e8fd7c8ed4d1eeb9ec3f4e7f9afadb89
--- /dev/null
+++ b/plugins/CoreAdminHome/Commands/InvalidateReportData.php
@@ -0,0 +1,200 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\Commands;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Date;
+use Piwik\Period;
+use Piwik\Period\Range;
+use Piwik\Piwik;
+use Piwik\Segment;
+use Piwik\Plugin\ConsoleCommand;
+use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
+use Piwik\Site;
+use Piwik\Period\Factory as PeriodFactory;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Provides a simple interface for invalidating report data by date ranges, site IDs and periods.
+ */
+class InvalidateReportData extends ConsoleCommand
+{
+    const ALL_OPTION_VALUE = 'all';
+
+    protected function configure()
+    {
+        $this->setName('core:invalidate-report-data');
+        $this->setDescription('Invalidate archived report data by date range, site and period.');
+        $this->addOption('dates', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
+            'List of dates or date ranges to invalidate report data for, eg, 2015-01-03 or 2015-01-05,2015-02-12.');
+        $this->addOption('sites', null, InputOption::VALUE_REQUIRED,
+            'List of site IDs to invalidate report data for, eg, "1,2,3,4" or "all" for all sites.',
+            self::ALL_OPTION_VALUE);
+        $this->addOption('periods', null, InputOption::VALUE_REQUIRED,
+            'List of period types to invalidate report data for. Can be one or more of the following values: day, '
+            . 'week, month, year or "all" for all of them.',
+            self::ALL_OPTION_VALUE);
+        $this->addOption('segment', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
+            'List of segments to invalidate report data for.');
+        $this->addOption('cascade', null, InputOption::VALUE_NONE,
+            'If supplied, invalidation will cascade, invalidating child period types even if they aren\'t specified in'
+            . ' --periods. For example, if --periods=week, --cascade will cause the days within those weeks to be '
+            . 'invalidated as well. If --periods=month, then weeks and days will be invalidated. Note: if a period '
+            . 'falls partly outside of a date range, then --cascade will also invalidate data for child periods '
+            . 'outside the date range. For example, if --dates=2015-09-14,2015-09-15 & --periods=week, --cascade will'
+            . ' also invalidate all days within 2015-09-13,2015-09-19, even those outside the date range.');
+        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'For tests. Runs the command w/o actually '
+            . 'invalidating anything.');
+        $this->setHelp('Invalidate archived report data by date range, site and period. Invalidated archive data will '
+            . 'be re-archived during the next core:archive run. If your log data has changed for some reason, this '
+            . 'command can be used to make sure reports are generated using the new, changed log data.');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
+
+        $cascade = $input->getOption('cascade');
+        $dryRun = $input->getOption('dry-run');
+
+        $sites = $this->getSitesToInvalidateFor($input);
+        $periodTypes = $this->getPeriodTypesToInvalidateFor($input);
+        $dateRanges = $this->getDateRangesToInvalidateFor($input);
+        $segments = $this->getSegmentsToInvalidateFor($input, $sites);
+
+        foreach ($periodTypes as $periodType) {
+            foreach ($dateRanges as $dateRange) {
+                foreach ($segments as $segment) {
+                    $segmentStr = $segment ? $segment->getString() : '';
+
+                    $output->writeln("Invalidating $periodType periods in $dateRange [segment = $segmentStr]...");
+
+                    $dates = $this->getPeriodDates($periodType, $dateRange);
+
+                    if ($dryRun) {
+                        $output->writeln("[Dry-run] invalidating archives for site = [ " . implode(', ', $sites)
+                            . " ], dates = [ " . implode(', ', $dates) . " ], period = [ $periodType ], segment = [ "
+                            . "$segmentStr ], cascade = [ " . (int)$cascade . " ]");
+                    } else {
+                        $invalidationResult = $invalidator->markArchivesAsInvalidated($sites, $dates, $periodType, $segment, $cascade);
+
+                        if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
+                            $output->writeln($invalidationResult->makeOutputLogs());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private function getSitesToInvalidateFor(InputInterface $input)
+    {
+        $sites = $input->getOption('sites');
+
+        $siteIds = Site::getIdSitesFromIdSitesString($sites);
+        if (empty($siteIds)) {
+            throw new \InvalidArgumentException("Invalid --sites value: '$sites'.");
+        }
+
+        $allSiteIds = SitesManagerAPI::getInstance()->getAllSitesId();
+        foreach ($siteIds as $idSite) {
+            if (!in_array($idSite, $allSiteIds)) {
+                throw new \InvalidArgumentException("Invalid --sites value: '$sites', there are no sites with IDs = $idSite");
+            }
+        }
+
+        return $siteIds;
+    }
+
+    private function getPeriodTypesToInvalidateFor(InputInterface $input)
+    {
+        $periods = $input->getOption('periods');
+        if (empty($periods)) {
+            throw new \InvalidArgumentException("The --periods argument is required.");
+        }
+
+        if ($periods == self::ALL_OPTION_VALUE) {
+            $result = array_keys(Piwik::$idPeriods);
+            unset($result[4]); // remove 'range' period
+            return $result;
+        }
+
+        $periods = explode(',', $periods);
+        $periods = array_map('trim', $periods);
+
+        foreach ($periods as $periodIdentifier) {
+            if ($periodIdentifier == 'range') {
+                throw new \InvalidArgumentException(
+                    "Invalid period type: invalidating range periods is not currently supported.");
+            }
+
+            if (!isset(Piwik::$idPeriods[$periodIdentifier])) {
+                throw new \InvalidArgumentException("Invalid period type '$periodIdentifier' supplied in --periods.");
+            }
+        }
+
+        return $periods;
+    }
+
+    /**
+     * @param InputInterface $input
+     * @return Date[][]
+     */
+    private function getDateRangesToInvalidateFor(InputInterface $input)
+    {
+        $dateRanges = $input->getOption('dates');
+        if (empty($dateRanges)) {
+            throw new \InvalidArgumentException("The --dates option is required.");
+        }
+
+        return $dateRanges;
+    }
+
+    private function getPeriodDates($periodType, $dateRange)
+    {
+        if (!isset(Piwik::$idPeriods[$periodType])) {
+            throw new \InvalidArgumentException("Invalid period type '$periodType'.");
+        }
+
+        try {
+            $period = PeriodFactory::build($periodType, $dateRange);
+        } catch (\Exception $ex) {
+            throw new \InvalidArgumentException("Invalid date or date range specifier '$dateRange'", $code = 0, $ex);
+        }
+
+        $result = array();
+        if ($period instanceof Range) {
+            foreach ($period->getSubperiods() as $subperiod) {
+                $result[] = $subperiod->getDateStart();
+            }
+        } else {
+            $result[] = $period->getDateStart();
+        }
+        return $result;
+    }
+
+    private function getSegmentsToInvalidateFor(InputInterface $input, $idSites)
+    {
+        $segments = $input->getOption('segment');
+        $segments = array_map('trim', $segments);
+        $segments = array_unique($segments);
+
+        if (empty($segments)) {
+            return array(null);
+        }
+
+        $result = array();
+        foreach ($segments as $segmentString) {
+            $result[] = new Segment($segmentString, $idSites);
+        }
+        return $result;
+    }
+}
diff --git a/plugins/CoreAdminHome/tests/Framework/Mock/API.php b/plugins/CoreAdminHome/tests/Framework/Mock/API.php
index 70070c50e9b27855c6deacd23007b5b80a7f740f..0fc3f106b070c8258b9eaa75e7a794df245efb73 100644
--- a/plugins/CoreAdminHome/tests/Framework/Mock/API.php
+++ b/plugins/CoreAdminHome/tests/Framework/Mock/API.php
@@ -14,7 +14,7 @@ class API extends \Piwik\Plugins\CoreAdminHome\API
 {
     private $invalidatedReports = array();
 
-    public function invalidateArchivedReports($idSites, $dates, $period = false)
+    public function invalidateArchivedReports($idSites, $dates, $period = false, $segment = false, $cascadeDown = false)
     {
         $this->invalidatedReports[] = func_get_args();
     }
diff --git a/plugins/CoreAdminHome/tests/Integration/Commands/InvalidateReportDataTest.php b/plugins/CoreAdminHome/tests/Integration/Commands/InvalidateReportDataTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f565b7105b5abf08b743531a5e8826db1110385a
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Integration/Commands/InvalidateReportDataTest.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\tests\Integration\Commands;
+
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase;
+
+/**
+ * @group CoreAdminHome
+ * @group CoreAdminHome_Integration
+ */
+class InvalidateReportDataTest extends ConsoleCommandTestCase
+{
+    public static function setUpBeforeClass()
+    {
+        parent::setUpBeforeClass();
+
+        Fixture::createWebsite('2012-01-01 00:00:00');
+        Fixture::createWebsite('2012-01-01 00:00:00');
+        Fixture::createWebsite('2012-01-01 00:00:00');
+    }
+
+    /**
+     * @dataProvider getInvalidDateRanges
+     */
+    public function test_Command_FailsWhenAnInvalidDateRangeIsUsed($invalidDateRange)
+    {
+        $code = $this->applicationTester->run(array(
+            'command' => 'core:invalidate-report-data',
+            '--dates' => array($invalidDateRange),
+            '--periods' => 'day',
+            '--sites' => '1',
+            '--dry-run' => true,
+            '-vvv' => true,
+        ));
+
+        $this->assertNotEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+        $this->assertContains("Invalid date or date range specifier", $this->applicationTester->getDisplay());
+    }
+
+    public function getInvalidDateRanges()
+    {
+        return array(
+            array('garbage'),
+            array('2012-01-03 2013-02-01'),
+        );
+    }
+
+    /**
+     * @dataProvider getInvalidPeriodTypes
+     */
+    public function test_Command_FailsWhenAnInvalidPeriodTypeIsUsed($invalidPeriodType)
+    {
+        $code = $this->applicationTester->run(array(
+            'command' => 'core:invalidate-report-data',
+            '--dates' => '2012-01-01',
+            '--periods' => $invalidPeriodType,
+            '--sites' => '1',
+            '--dry-run' => true,
+            '-vvv' => true,
+        ));
+
+        $this->assertNotEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+        $this->assertContains("Invalid period type", $this->applicationTester->getDisplay());
+    }
+
+    public function getInvalidPeriodTypes()
+    {
+        return array(
+            array('cranberries'),
+            array('range'),
+        );
+    }
+
+    /**
+     * @dataProvider getInvalidSiteLists
+     */
+    public function test_Command_FailsWhenAnInvalidSiteListIsUsed($invalidSites)
+    {
+        $code = $this->applicationTester->run(array(
+            'command' => 'core:invalidate-report-data',
+            '--dates' => '2012-01-01',
+            '--periods' => 'day',
+            '--sites' => $invalidSites,
+            '--dry-run' => true,
+            '-vvv' => true,
+        ));
+
+        $this->assertNotEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+        $this->assertContains("Invalid --sites value", $this->applicationTester->getDisplay());
+    }
+
+    public function getInvalidSiteLists()
+    {
+        return array(
+            array('wolfalice'),
+            array(','),
+            array('1,500'),
+        );
+    }
+
+    public function test_Command_FailsWhenAnInvalidSegmentIsUsed()
+    {
+        $code = $this->applicationTester->run(array(
+            'command' => 'core:invalidate-report-data',
+            '--dates' => '2012-01-01',
+            '--periods' => 'day',
+            '--sites' => '1',
+            '--segment' => array('ablksdjfdslkjf'),
+            '--dry-run' => true,
+            '-vvv' => true,
+        ));
+
+        $this->assertNotEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+        $this->assertContains("The segment 'ablksdjfdslkjf' is not valid", $this->applicationTester->getDisplay());
+    }
+
+    /**
+     * @dataProvider getTestDataForSuccessTests
+     */
+    public function test_Command_InvalidatesCorrectSitesAndDates($dates, $periods, $sites, $cascade, $segments, $expectedOutputs)
+    {
+        $code = $this->applicationTester->run(array(
+            'command' => 'core:invalidate-report-data',
+            '--dates' => $dates,
+            '--periods' => $periods,
+            '--sites' => $sites,
+            '--cascade' => $cascade,
+            '--segment' => $segments ?: array(),
+            '--dry-run' => true,
+            '-vvv' => true,
+        ));
+
+        $this->assertEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+
+        foreach ($expectedOutputs as $output) {
+            $this->assertContains($output, $this->applicationTester->getDisplay());
+        }
+    }
+
+    public function getTestDataForSuccessTests()
+    {
+        return array(
+
+            array( // no cascade, single site + single day
+                array('2012-01-01'),
+                'day',
+                '1',
+                false,
+                null,
+                array(
+                    '[Dry-run] invalidating archives for site = [ 1 ], dates = [ 2012-01-01 ], period = [ day ], segment = [  ]',
+                ),
+            ),
+
+            array( // no cascade, single site + single day
+                array('2012-01-01'),
+                'day',
+                '1',
+                true,
+                null,
+                array(
+                    '[Dry-run] invalidating archives for site = [ 1 ], dates = [ 2012-01-01 ], period = [ day ], segment = [  ]',
+                ),
+            ),
+
+            array( // no cascade, single site, date, period
+                array('2012-01-01'),
+                'week',
+                '1',
+                false,
+                null,
+                array(
+                    '[Dry-run] invalidating archives for site = [ 1 ], dates = [ 2011-12-26 ], period = [ week ], segment = [  ]',
+                ),
+            ),
+
+            array( // no cascade, multiple site, date & period
+                array('2012-01-01,2012-02-05', '2012-01-26,2012-01-27', '2013-03-19'),
+                'month,week',
+                '1,3',
+                false,
+                null,
+                array(
+                    '[Dry-run] invalidating archives for site = [ 1, 3 ], dates = [ 2012-01-01, 2012-02-01 ], period = [ month ], segment = [  ], cascade = [ 0 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 3 ], dates = [ 2012-01-01 ], period = [ month ], segment = [  ], cascade = [ 0 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 3 ], dates = [ 2013-03-01 ], period = [ month ], segment = [  ], cascade = [ 0 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 3 ], dates = [ 2011-12-26, 2012-01-02, 2012-01-09, 2012-01-16, 2012-01-23, 2012-01-30 ], period = [ week ], segment = [  ], cascade = [ 0 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 3 ], dates = [ 2012-01-23 ], period = [ week ], segment = [  ], cascade = [ 0 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 3 ], dates = [ 2013-03-18 ], period = [ week ], segment = [  ], cascade = [ 0 ]',
+                ),
+            ),
+
+            array( // cascade, single site, date, period
+                array('2012-01-30,2012-02-10'),
+                'week',
+                '2',
+                true,
+                null,
+                array(
+                    '[Dry-run] invalidating archives for site = [ 2 ], dates = [ 2012-01-30, 2012-02-06 ], period = [ week ], segment = [  ], cascade = [ 1 ]',
+                ),
+            ),
+
+            array( // cascade, multiple site, date & period
+                array('2012-02-03,2012-02-04', '2012-03-15'),
+                'month,week,day',
+                'all',
+                true,
+                null,
+                array(
+                    '[Dry-run] invalidating archives for site = [ 1, 2, 3 ], dates = [ 2012-02-01 ], period = [ month ], segment = [  ], cascade = [ 1 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 2, 3 ], dates = [ 2012-03-01 ], period = [ month ], segment = [  ], cascade = [ 1 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 2, 3 ], dates = [ 2012-01-30 ], period = [ week ], segment = [  ], cascade = [ 1 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 2, 3 ], dates = [ 2012-03-12 ], period = [ week ], segment = [  ], cascade = [ 1 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 2, 3 ], dates = [ 2012-02-03, 2012-02-04 ], period = [ day ], segment = [  ], cascade = [ 1 ]',
+                    '[Dry-run] invalidating archives for site = [ 1, 2, 3 ], dates = [ 2012-03-15 ], period = [ day ], segment = [  ], cascade = [ 1 ]',
+                ),
+            ),
+
+            array( // cascade, one week, date & period + segment
+                array('2012-01-01,2012-01-14'),
+                'week',
+                'all',
+                true,
+                array('browserCode==FF'),
+                array(
+                    '[Dry-run] invalidating archives for site = [ 1, 2, 3 ], dates = [ 2011-12-26, 2012-01-02, 2012-01-09 ], period = [ week ], segment = [ browserCode==FF ], cascade = [ 1 ]',
+                ),
+            ),
+        );
+    }
+}
diff --git a/plugins/SitesManager/SitesManager.php b/plugins/SitesManager/SitesManager.php
index 200f268a8aa659f626f8d2f6b4c27e60dc0135db..4b686e2bdefbacce85fa5f0e1df51a4e6b8cae89 100644
--- a/plugins/SitesManager/SitesManager.php
+++ b/plugins/SitesManager/SitesManager.php
@@ -10,6 +10,7 @@ namespace Piwik\Plugins\SitesManager;
 
 use Piwik\Common;
 use Piwik\Archive\ArchiveInvalidator;
+use Piwik\Container\StaticContainer;
 use Piwik\Db;
 use Piwik\Plugins\PrivacyManager\PrivacyManager;
 use Piwik\Measurable\Settings\Storage;
@@ -69,7 +70,7 @@ class SitesManager extends \Piwik\Plugin
         // we do not delete logs here on purpose (you can run these queries on the log_ tables to delete all data)
         Cache::deleteCacheWebsiteAttributes($idSite);
 
-        $archiveInvalidator = new ArchiveInvalidator();
+        $archiveInvalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
         $archiveInvalidator->forgetRememberedArchivedReportsToInvalidateForSite($idSite);
 
         $measurableStorage = new Storage(Db::get(), $idSite);
diff --git a/plugins/SitesManager/tests/Integration/SitesManagerTest.php b/plugins/SitesManager/tests/Integration/SitesManagerTest.php
index b71695ae5bce125a89d800225f8d3f5940841127..15345fc231969994e220df6bb0d531b40f343090 100644
--- a/plugins/SitesManager/tests/Integration/SitesManagerTest.php
+++ b/plugins/SitesManager/tests/Integration/SitesManagerTest.php
@@ -11,6 +11,7 @@ namespace Piwik\Plugins\SitesManager\tests\Integration;
 use Piwik\Access;
 use Piwik\Cache;
 use Piwik\Archive\ArchiveInvalidator;
+use Piwik\Container\StaticContainer;
 use Piwik\Date;
 use Piwik\Plugins\SitesManager\SitesManager;
 use Piwik\Tests\Framework\Fixture;
@@ -54,7 +55,7 @@ class SitesManagerTest extends IntegrationTestCase
 
     public function test_onSiteDeleted_shouldRemoveRememberedArchiveReports()
     {
-        $archive = new ArchiveInvalidator();
+        $archive = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
         $archive->rememberToInvalidateArchivedReportsLater($this->siteId, Date::factory('2014-04-05'));
         $archive->rememberToInvalidateArchivedReportsLater($this->siteId, Date::factory('2014-04-06'));
         $archive->rememberToInvalidateArchivedReportsLater(4949, Date::factory('2014-04-05'));
diff --git a/tests/PHPUnit/Integration/CronArchiveTest.php b/tests/PHPUnit/Integration/CronArchiveTest.php
index f0e4b7c19c55a7278d966bebe275e193d9f1d438..c64f0c3e625d270c144300dfaa808ec10c41a903 100644
--- a/tests/PHPUnit/Integration/CronArchiveTest.php
+++ b/tests/PHPUnit/Integration/CronArchiveTest.php
@@ -10,6 +10,7 @@ namespace Piwik\Tests\Integration;
 
 use Piwik\Archiver\Request;
 use Piwik\CliMulti;
+use Piwik\Container\StaticContainer;
 use Piwik\CronArchive;
 use Piwik\Archive\ArchiveInvalidator;
 use Piwik\Date;
@@ -31,7 +32,7 @@ class CronArchiveTest extends IntegrationTestCase
         Fixture::createWebsite('2014-12-12 00:01:02');
         Fixture::createWebsite('2014-12-12 00:01:02');
 
-        $ar = new ArchiveInvalidator();
+        $ar = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
         $ar->rememberToInvalidateArchivedReportsLater(1, Date::factory('2014-04-05'));
         $ar->rememberToInvalidateArchivedReportsLater(2, Date::factory('2014-04-05'));
         $ar->rememberToInvalidateArchivedReportsLater(2, Date::factory('2014-04-06'));
diff --git a/tests/PHPUnit/Integration/DataAccess/ArchiveInvalidatorTest.php b/tests/PHPUnit/Integration/DataAccess/ArchiveInvalidatorTest.php
index 6c1ff8cd8839da35b3e8d1974fd842adb9c6da1c..7501a34715cd84276bbb2023d6d5919173905e96 100644
--- a/tests/PHPUnit/Integration/DataAccess/ArchiveInvalidatorTest.php
+++ b/tests/PHPUnit/Integration/DataAccess/ArchiveInvalidatorTest.php
@@ -8,10 +8,20 @@
 
 namespace Piwik\Tests\Integration\DataAccess;
 
+use Piwik\ArchiveProcessor\Rules;
+use Piwik\CronArchive\SitesToReprocessDistributedList;
+use Piwik\DataAccess\ArchiveTableCreator;
+use Piwik\DataAccess\ArchiveWriter;
+use Piwik\DataAccess\Model;
 use Piwik\Date;
+use Piwik\Db;
 use Piwik\Option;
+use Piwik\Piwik;
+use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
+use Piwik\Plugins\PrivacyManager\PrivacyManager;
 use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
 use Piwik\Archive\ArchiveInvalidator;
+use Piwik\Segment;
 
 /**
  * @group Archiver
@@ -20,16 +30,38 @@ use Piwik\Archive\ArchiveInvalidator;
  */
 class ArchiveInvalidatorTest extends IntegrationTestCase
 {
+    const TEST_SEGMENT_1 = 'browserCode==FF';
+    const TEST_SEGMENT_2 = 'countryCode==uk';
+
     /**
      * @var ArchiveInvalidator
      */
     private $invalidator;
 
+    /**
+     * @var Segment
+     */
+    private static $segment1;
+
+    /**
+     * @var Segment
+     */
+    private static $segment2;
+
+    public static function setUpBeforeClass()
+    {
+        parent::setUpBeforeClass();
+
+        // these are static because it takes a long time to create new Segment instances (for some reason)
+        self::$segment1 = new Segment(self::TEST_SEGMENT_1, array());
+        self::$segment2 = new Segment(self::TEST_SEGMENT_2, array());
+    }
+
     public function setUp()
     {
         parent::setUp();
 
-        $this->invalidator = new ArchiveInvalidator();
+        $this->invalidator = new ArchiveInvalidator(new Model());
     }
 
     public function test_rememberToInvalidateArchivedReportsLater_shouldCreateAnEntryInCaseThereIsNoneYet()
@@ -141,7 +173,7 @@ class ArchiveInvalidatorTest extends IntegrationTestCase
             Date::factory('2010-10-10'),
         );
 
-        $this->invalidator->markArchivesAsInvalidated($idSites, $dates, false);
+        $this->invalidator->markArchivesAsInvalidated($idSites, $dates, 'week');
         $reports = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
 
         $expected = array(
@@ -183,4 +215,452 @@ class ArchiveInvalidatorTest extends IntegrationTestCase
         $this->rememberReport(7, '2014-05-08');
         $this->rememberReport(7, '2014-04-08');
     }
+
+    public function test_markArchivesAsInvalidated_DoesNotInvalidateDatesBeforePurgeThreshold()
+    {
+        PrivacyManager::savePurgeDataSettings(array(
+            'delete_logs_enable' => 1,
+            'delete_logs_older_than' => 180,
+        ));
+
+        $dateBeforeThreshold = Date::factory('today')->subDay(190);
+        $thresholdDate = Date::factory('today')->subDay(180);
+        $dateAfterThreshold = Date::factory('today')->subDay(170);
+
+        // can't test more than day since today will change, causing the test to fail w/ other periods randomly
+        $this->insertArchiveRow(1, $dateBeforeThreshold, 'day');
+        $this->insertArchiveRow(1, $dateAfterThreshold, 'day');
+
+        /** @var ArchiveInvalidator $archiveInvalidator */
+        $archiveInvalidator = self::$fixture->piwikEnvironment->getContainer()->get('Piwik\Archive\ArchiveInvalidator');
+        $result = $archiveInvalidator->markArchivesAsInvalidated(array(1), array($dateBeforeThreshold, $dateAfterThreshold), 'day');
+
+        $this->assertEquals($thresholdDate->toString(), $result->minimumDateWithLogs);
+
+        $expectedProcessedDates = array($dateAfterThreshold->toString());
+        $this->assertEquals($expectedProcessedDates, $result->processedDates);
+
+        $expectedWarningDates = array($dateBeforeThreshold->toString());
+        $this->assertEquals($expectedWarningDates, $result->warningDates);
+
+        $invalidatedArchives = $this->getInvalidatedIdArchives();
+
+        $countInvalidatedArchives = 0;
+        foreach ($invalidatedArchives as $idarchives) {
+            $countInvalidatedArchives += count($idarchives);
+        }
+
+        $this->assertEquals(1, $countInvalidatedArchives);
+    }
+
+    public function test_markArchivesAsInvalidated_CorrectlyModifiesDistributedLists()
+    {
+        /** @var ArchiveInvalidator $archiveInvalidator */
+        $archiveInvalidator = self::$fixture->piwikEnvironment->getContainer()->get('Piwik\Archive\ArchiveInvalidator');
+
+        $idSites = array(1, 3, 5);
+        $dates = array(
+            Date::factory('2014-12-31'),
+            Date::factory('2015-01-01'),
+            Date::factory('2015-01-10'),
+        );
+        $archiveInvalidator->markArchivesAsInvalidated($idSites, $dates, 'day');
+
+        $idSites = array(1, 3, 5);
+        $dates = array(
+            Date::factory('2014-12-21'),
+            Date::factory('2015-01-01'),
+            Date::factory('2015-03-08'),
+        );
+        $archiveInvalidator->markArchivesAsInvalidated($idSites, $dates, 'week');
+
+        $expectedSitesToProcessListContents = array(1, 3, 5);
+        $this->assertEquals($expectedSitesToProcessListContents, $this->getSitesToReprocessListContents());
+
+        $expectedArchivesToPurgeListContents = array('2014_12', '2014_01', '2015_01', '2015_03');
+        $this->assertEquals($expectedArchivesToPurgeListContents, $this->getArchivesToPurgeListContents());
+    }
+
+    /**
+     * @dataProvider getTestDataForMarkArchivesAsInvalidated
+     */
+    public function test_markArchivesAsInvalidated_MarksCorrectArchivesAsInvalidated($idSites, $dates, $period, $segment, $cascadeDown,
+                                                                                     $expectedIdArchives)
+    {
+        $dates = array_map(array('Piwik\Date', 'factory'), $dates);
+
+        $this->insertArchiveRowsForTest();
+
+        if (!empty($segment)) {
+            $segment = new Segment($segment, $idSites);
+        }
+
+        /** @var ArchiveInvalidator $archiveInvalidator */
+        $archiveInvalidator = self::$fixture->piwikEnvironment->getContainer()->get('Piwik\Archive\ArchiveInvalidator');
+        $result = $archiveInvalidator->markArchivesAsInvalidated($idSites, $dates, $period, $segment, $cascadeDown);
+
+        $this->assertEquals($dates, $result->processedDates);
+
+        $idArchives = $this->getInvalidatedArchives();
+        $this->assertEquals($expectedIdArchives, $idArchives);
+    }
+
+    public function getTestDataForMarkArchivesAsInvalidated()
+    {
+        return array(
+            // day period, multiple sites, multiple dates across tables, cascade = true
+            array(
+                array(1, 2),
+                array('2015-01-01', '2015-02-05', '2015-04-30'),
+                'day',
+                null,
+                true,
+                array(
+                    '2014_01' => array(),
+                    '2015_03' => array(),
+                    '2015_04' => array(
+                        '1.2015-04-30.2015-04-30.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '2.2015-04-30.2015-04-30.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-04-27.2015-05-03.2.done',
+                        '2.2015-04-27.2015-05-03.2.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-04-01.2015-04-30.3.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '2.2015-04-01.2015-04-30.3.done5447835b0a861475918e79e932abdfd8',
+                    ),
+                    '2014_12' => array(),
+                    '2015_01' => array(
+                        '1.2015-01-01.2015-01-01.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '2.2015-01-01.2015-01-01.1.done.VisitsSummary',
+                        '1.2015-01-01.2015-01-31.3.done3736b708e4d20cfc10610e816a1b2341',
+                        '2.2015-01-01.2015-01-31.3.done.VisitsSummary',
+                        '1.2015-01-01.2015-12-31.4.done5447835b0a861475918e79e932abdfd8',
+                        '2.2015-01-01.2015-12-31.4.done',
+                        '1.2015-01-01.2015-01-10.5.done.VisitsSummary',
+                    ),
+                    '2015_02' => array(
+                        '1.2015-02-05.2015-02-05.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '2.2015-02-05.2015-02-05.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-02-02.2015-02-08.2.done',
+                        '2.2015-02-02.2015-02-08.2.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-02-01.2015-02-28.3.done.VisitsSummary',
+                        '2.2015-02-01.2015-02-28.3.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                    ),
+                    '2015_05' => array(),
+                    '2015_06' => array(),
+                ),
+            ),
+
+            // month period, one site, one date, cascade = false
+            array(
+                array(1),
+                array('2015-01-01'),
+                'month',
+                null,
+                false,
+                array(
+                    '2014_01' => array(),
+                    '2014_12' => array(),
+                    '2015_01' => array(
+                        '1.2015-01-01.2015-01-31.3.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-01.2015-12-31.4.done5447835b0a861475918e79e932abdfd8',
+                    ),
+                    '2015_02' => array(),
+                    '2015_03' => array(),
+                    '2015_04' => array(),
+                    '2015_05' => array(),
+                    '2015_06' => array(),
+                ),
+            ),
+
+            // month period, one site, one date, cascade = true
+            array(
+                array(1),
+                array('2015-01-15'),
+                'month',
+                null,
+                true,
+                array(
+                    '2014_01' => array(),
+                    '2014_12' => array(
+                        '1.2014-12-29.2015-01-04.2.done3736b708e4d20cfc10610e816a1b2341',
+
+                        // doesn't need to be invalidated since the month won't use the week above, but very difficult
+                        // to keep it valid, while keeping invalidation logic simple.
+                        '1.2014-12-01.2014-12-31.3.done5447835b0a861475918e79e932abdfd8',
+                    ),
+                    '2015_01' => array(
+                        '1.2015-01-01.2015-01-01.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-02.2015-01-02.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-03.2015-01-03.1.done.VisitsSummary',
+                        '1.2015-01-04.2015-01-04.1.done',
+                        '1.2015-01-05.2015-01-05.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-06.2015-01-06.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-07.2015-01-07.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-08.2015-01-08.1.done.VisitsSummary',
+                        '1.2015-01-09.2015-01-09.1.done',
+                        '1.2015-01-10.2015-01-10.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-11.2015-01-11.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-12.2015-01-12.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-13.2015-01-13.1.done.VisitsSummary',
+                        '1.2015-01-14.2015-01-14.1.done',
+                        '1.2015-01-15.2015-01-15.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-16.2015-01-16.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-17.2015-01-17.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-18.2015-01-18.1.done.VisitsSummary',
+                        '1.2015-01-19.2015-01-19.1.done',
+                        '1.2015-01-20.2015-01-20.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-21.2015-01-21.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-22.2015-01-22.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-23.2015-01-23.1.done.VisitsSummary',
+                        '1.2015-01-24.2015-01-24.1.done',
+                        '1.2015-01-25.2015-01-25.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-26.2015-01-26.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-27.2015-01-27.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-28.2015-01-28.1.done.VisitsSummary',
+                        '1.2015-01-29.2015-01-29.1.done',
+                        '1.2015-01-30.2015-01-30.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-31.2015-01-31.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-05.2015-01-11.2.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-12.2015-01-18.2.done.VisitsSummary',
+                        '1.2015-01-19.2015-01-25.2.done',
+                        '1.2015-01-26.2015-02-01.2.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-01.2015-01-31.3.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-01.2015-12-31.4.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-01.2015-01-10.5.done.VisitsSummary',
+                    ),
+                    '2015_02' => array(),
+                    '2015_03' => array(),
+                    '2015_04' => array(),
+                    '2015_05' => array(),
+                    '2015_06' => array(),
+                ),
+            ),
+
+            // week period, one site, multiple dates w/ redundant dates & periods, cascade = true
+            array(
+                array(1),
+                array('2015-01-02', '2015-01-03', '2015-01-31'),
+                'week',
+                null,
+                true,
+                array(
+                    '2014_01' => array(),
+                    '2014_12' => array(
+                        '1.2014-12-29.2014-12-29.1.done',
+                        '1.2014-12-30.2014-12-30.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2014-12-31.2014-12-31.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2014-12-29.2015-01-04.2.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2014-12-01.2014-12-31.3.done5447835b0a861475918e79e932abdfd8',
+                        '1.2014-12-05.2015-01-01.5.done.VisitsSummary',
+                    ),
+                    '2015_01' => array(
+                        '1.2015-01-01.2015-01-01.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-02.2015-01-02.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-03.2015-01-03.1.done.VisitsSummary',
+                        '1.2015-01-04.2015-01-04.1.done',
+                        '1.2015-01-26.2015-01-26.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-27.2015-01-27.1.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-28.2015-01-28.1.done.VisitsSummary',
+                        '1.2015-01-29.2015-01-29.1.done',
+                        '1.2015-01-30.2015-01-30.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-31.2015-01-31.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-26.2015-02-01.2.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-01.2015-01-31.3.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-01.2015-12-31.4.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-01-01.2015-01-10.5.done.VisitsSummary',
+                    ),
+                    '2015_02' => array(
+                        '1.2015-02-01.2015-02-01.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-02-01.2015-02-28.3.done.VisitsSummary',
+                    ),
+                    '2015_03' => array(),
+                    '2015_04' => array(),
+                    '2015_05' => array(),
+                    '2015_06' => array(),
+                ),
+            ),
+
+            // range period, one site, cascade = true
+            array(
+                array(1),
+                array('2015-01-02', '2015-03-05'),
+                'range',
+                null,
+                true,
+                array(
+                    '2014_01' => array(),
+                    '2014_12' => array(),
+                    '2015_01' => array(
+                        '1.2015-01-01.2015-01-10.5.done.VisitsSummary',
+                    ),
+                    '2015_02' => array(),
+                    '2015_03' => array(
+                        '1.2015-03-04.2015-03-05.5.done.VisitsSummary',
+                        '1.2015-03-05.2015-03-10.5.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                    ),
+                    '2015_04' => array(),
+                    '2015_05' => array(),
+                    '2015_06' => array(),
+                ),
+            ),
+
+            // week period, one site, cascade = true, segment
+            array(
+                array(1),
+                array('2015-01-05'),
+                'month',
+                self::TEST_SEGMENT_1,
+                true,
+                array(
+                    '2014_01' => array(),
+                    '2014_12' => array(
+                        '1.2014-12-29.2015-01-04.2.done3736b708e4d20cfc10610e816a1b2341',
+                    ),
+                    '2015_01' => array(
+                        '1.2015-01-01.2015-01-01.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-05.2015-01-05.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-06.2015-01-06.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-10.2015-01-10.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-11.2015-01-11.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-15.2015-01-15.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-16.2015-01-16.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-20.2015-01-20.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-21.2015-01-21.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-25.2015-01-25.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-26.2015-01-26.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-30.2015-01-30.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-31.2015-01-31.1.done3736b708e4d20cfc10610e816a1b2341',
+                        '1.2015-01-26.2015-02-01.2.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-01-01.2015-01-31.3.done3736b708e4d20cfc10610e816a1b2341',
+                    ),
+                    '2015_02' => array(),
+                    '2015_03' => array(),
+                    '2015_04' => array(),
+                    '2015_05' => array(),
+                    '2015_06' => array(),
+                ),
+            ),
+
+            // removing all periods
+            array(
+                array(1),
+                array('2015-05-05'),
+                '',
+                null,
+                false,
+                array(
+                    '2014_01' => array(),
+                    '2014_12' => array(),
+                    '2015_01' => array(
+                        '1.2015-01-01.2015-12-31.4.done5447835b0a861475918e79e932abdfd8',
+                    ),
+                    '2015_02' => array(),
+                    '2015_03' => array(),
+                    '2015_04' => array(),
+                    '2015_05' => array(
+                        '1.2015-05-05.2015-05-05.1.done3736b708e4d20cfc10610e816a1b2341.UserCountry',
+                        '1.2015-05-04.2015-05-10.2.done5447835b0a861475918e79e932abdfd8',
+                        '1.2015-05-01.2015-05-31.3.done3736b708e4d20cfc10610e816a1b2341',
+                    ),
+                    '2015_06' => array(),
+                ),
+            ),
+        );
+    }
+
+    private function getInvalidatedIdArchives()
+    {
+        $result = array();
+        foreach (ArchiveTableCreator::getTablesArchivesInstalled(ArchiveTableCreator::NUMERIC_TABLE) as $table) {
+            $date = ArchiveTableCreator::getDateFromTableName($table);
+
+            $idArchives = Db::fetchAll("SELECT idarchive FROM $table WHERE name LIKE 'done%' AND value = ?", array(ArchiveWriter::DONE_INVALIDATED));
+            $idArchives = array_map('reset', $idArchives);
+
+            $result[$date] = $idArchives;
+        }
+        return $result;
+    }
+
+    private function getInvalidatedArchives()
+    {
+        $result = array();
+        foreach (ArchiveTableCreator::getTablesArchivesInstalled(ArchiveTableCreator::NUMERIC_TABLE) as $table) {
+            $date = ArchiveTableCreator::getDateFromTableName($table);
+
+            $sql = "SELECT CONCAT(idsite, '.', date1, '.', date2, '.', period, '.', name) FROM $table WHERE name LIKE 'done%' AND value = ?";
+
+            $archiveSpecs = Db::fetchAll($sql, array(ArchiveWriter::DONE_INVALIDATED));
+            $archiveSpecs = array_map('reset', $archiveSpecs);
+
+            $result[$date] = $archiveSpecs;
+        }
+        return $result;
+    }
+
+    private function insertArchiveRowsForTest()
+    {
+        $periods = array('day', 'week', 'month', 'year');
+        $sites = array(1,2,3);
+
+        $startDate = Date::factory('2014-12-01');
+        $endDate = Date::factory('2015-05-31');
+
+        foreach ($periods as $periodLabel) {
+            $nextEndDate = $endDate->addPeriod(1, $periodLabel);
+            for ($date = $startDate; $date->isEarlier($nextEndDate); $date = $date->addPeriod(1, $periodLabel)) {
+                foreach ($sites as $idSite) {
+                    $this->insertArchiveRow($idSite, $date->toString(), $periodLabel);
+                }
+            }
+        }
+
+        $rangePeriods = array('2015-03-04,2015-03-05', '2014-12-05,2015-01-01', '2015-03-05,2015-03-10', '2015-01-01,2015-01-10');
+        foreach ($rangePeriods as $dateRange) {
+            $this->insertArchiveRow($idSite = 1, $dateRange, 'range');
+        }
+    }
+
+    private function insertArchiveRow($idSite, $date, $periodLabel)
+    {
+        $periodObject = \Piwik\Period\Factory::build($periodLabel, $date);
+        $dateStart = $periodObject->getDateStart();
+        $dateEnd = $periodObject->getDateEnd();
+
+        $table = ArchiveTableCreator::getNumericTable($dateStart);
+
+        $idArchive = (int) Db::fetchOne("SELECT MAX(idarchive) FROM $table WHERE name LIKE 'done%'");
+        $idArchive = $idArchive + 1;
+
+        $periodId = Piwik::$idPeriods[$periodLabel];
+
+        $doneFlag = 'done';
+        if ($idArchive % 5 == 1) {
+            $doneFlag = Rules::getDoneFlagArchiveContainsAllPlugins(self::$segment1);
+        } else if ($idArchive % 5 == 2) {
+            $doneFlag .= '.VisitsSummary';
+        } else if ($idArchive % 5 == 3) {
+            $doneFlag = Rules::getDoneFlagArchiveContainsOnePlugin(self::$segment1, 'UserCountry');
+        } else if ($idArchive % 5 == 4) {
+            $doneFlag = Rules::getDoneFlagArchiveContainsAllPlugins(self::$segment2);
+        }
+
+        $sql = "INSERT INTO $table (idarchive, name, idsite, date1, date2, period, ts_archived)
+                     VALUES ($idArchive, 'nb_visits', $idSite, '$dateStart', '$dateEnd', $periodId, NOW()),
+                            ($idArchive, '$doneFlag', $idSite, '$dateStart', '$dateEnd', $periodId, NOW())";
+        Db::query($sql);
+    }
+
+    private function getSitesToReprocessListContents()
+    {
+        $list = new SitesToReprocessDistributedList();
+        $values = $list->getAll();
+        return array_values($values);
+    }
+
+    private function getArchivesToPurgeListContents()
+    {
+        $list = new ArchivesToPurgeDistributedList();
+        $values = $list->getAll();
+        return array_values($values);
+    }
 }
diff --git a/tests/PHPUnit/Integration/Tracker/VisitTest.php b/tests/PHPUnit/Integration/Tracker/VisitTest.php
index e870fa6c51c01532d3b4200dea3ec87b0761ec4d..e686146f73c49961185c5ec99648f390957aa566 100644
--- a/tests/PHPUnit/Integration/Tracker/VisitTest.php
+++ b/tests/PHPUnit/Integration/Tracker/VisitTest.php
@@ -9,7 +9,7 @@
 namespace Piwik\Tests\Integration\Tracker;
 
 use Piwik\Cache;
-use Piwik\Archive\ArchiveInvalidator;
+use Piwik\Container\StaticContainer;
 use Piwik\Date;
 use Piwik\Network\IPUtils;
 use Piwik\Plugin\Manager;
@@ -411,7 +411,7 @@ class VisitTest extends IntegrationTestCase
 
         $visit->handle();
 
-        $archive = new ArchiveInvalidator();
+        $archive = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
         $remembered = $archive->getRememberedArchivedReportsThatShouldBeInvalidated();
 
         $this->assertSame($expectedRemeberedArchivedReports, $remembered);
diff --git a/tests/PHPUnit/System/ArchiveInvalidationTest.php b/tests/PHPUnit/System/ArchiveInvalidationTest.php
index b64aa82479498e0e75143047d706e4e05537bb61..7e41cd6eae3d773396bd90f4c7aae9b26f44b4d5 100644
--- a/tests/PHPUnit/System/ArchiveInvalidationTest.php
+++ b/tests/PHPUnit/System/ArchiveInvalidationTest.php
@@ -52,9 +52,9 @@ class ArchiveInvalidationTest extends SystemTestCase
         return array(
 
             array($apiToCall, array('idSite'                 => self::$fixture->idSite2,
-                                    'testSuffix'             => 'Website' . self::$fixture->idSite2 . "_NewDataShouldNotAppear_BecauseWeekWasNotInvalidated",
+                                    'testSuffix'             => 'Website' . self::$fixture->idSite2 . "_NewDataShouldNotAppear_BecauseDayWasNotInvalidated",
                                     'date'                   => self::$fixture->dateTimeFirstDateWebsite2,
-                                    'periods'                => 'week',
+                                    'periods'                => 'day',
                                     'segment'                => 'pageUrl=@category/',
                                     'setDateLastN'           => 4, // 4months ahead
                                     'otherRequestParameters' => array('expanded' => 1))
@@ -99,8 +99,8 @@ class ArchiveInvalidationTest extends SystemTestCase
     public function testAnotherApi($api, $params)
     {
         if ($params['periods'] === 'month') {
-            // we do now need to invalidate weeks as well since months are based on weeks
-            $this->invalidateTestArchive(self::$fixture->idSite2, 'week', self::$fixture->dateTimeFirstDateWebsite2);
+            // we do now need to invalidate days as well since weeks are based on weeks
+            $this->invalidateTestArchive(self::$fixture->idSite2, 'week', self::$fixture->dateTimeFirstDateWebsite2, true);
         }
 
         $this->setBrowserArchivingTriggering(1);
@@ -135,16 +135,15 @@ class ArchiveInvalidationTest extends SystemTestCase
         $r = new Request("module=API&method=CoreAdminHome.invalidateArchivedReports&idSites=" . self::$fixture->idSite1 . "&dates=" . $dateToInvalidate1->format('Y-m-d'));
         $this->assertApiResponseHasNoError($r->process());
 
-        // Days & Months reports only are invalidated and we test our weekly report will still show old data.
-        $this->invalidateTestArchive(self::$fixture->idSite2, 'day', self::$fixture->dateTimeFirstDateWebsite2);
-        $this->invalidateTestArchive(self::$fixture->idSite2, 'month', self::$fixture->dateTimeFirstDateWebsite2);
+        // week reports only are invalidated and we test our daily report will still show old data.
+        $this->invalidateTestArchive(self::$fixture->idSite2, 'week', self::$fixture->dateTimeFirstDateWebsite2);
     }
 
-    private function invalidateTestArchive($idSite, $period, $dateTime)
+    private function invalidateTestArchive($idSite, $period, $dateTime, $cascadeDown = false)
     {
         $dates = new \DateTime($dateTime);
         $dates = $dates->format('Y-m-d');
-        $r = new Request("module=API&method=CoreAdminHome.invalidateArchivedReports&period=$period&idSites=$idSite&dates=$dates");
+        $r = new Request("module=API&method=CoreAdminHome.invalidateArchivedReports&period=$period&idSites=$idSite&dates=$dates&cascadeDown=" . (int)$cascadeDown);
         $this->assertApiResponseHasNoError($r->process());
     }
 }
diff --git a/tests/PHPUnit/System/TwoVisitorsTwoWebsitesDifferentDaysConversionsTest.php b/tests/PHPUnit/System/TwoVisitorsTwoWebsitesDifferentDaysConversionsTest.php
index 7e9a576b71cdc8012ed0458cdb1d4e792073c9c2..c02023d7cb01f4b1f49f2ce17aa2b1cdd2701614 100755
--- a/tests/PHPUnit/System/TwoVisitorsTwoWebsitesDifferentDaysConversionsTest.php
+++ b/tests/PHPUnit/System/TwoVisitorsTwoWebsitesDifferentDaysConversionsTest.php
@@ -9,9 +9,7 @@ namespace Piwik\Tests\System;
 
 use Piwik\Archive;
 use Piwik\Cache;
-use Piwik\Archive\ArchiveInvalidator;
-use Piwik\Option;
-use Piwik\Plugins\Goals\Archiver;
+use Piwik\Container\StaticContainer;
 use Piwik\Segment;
 use Piwik\Tests\Framework\TestCase\SystemTestCase;
 use Piwik\Tests\Fixtures\TwoSitesTwoVisitorsDifferentDays;
@@ -172,7 +170,7 @@ class TwoVisitorsTwoWebsitesDifferentDaysConversionsTest extends SystemTestCase
         $this->assertEquals(array(self::$fixture->idSite1, self::$fixture->idSite2),
                             $cache->fetch('Archive.SiteIdsOfRememberedReportsInvalidated'));
 
-        $invalidator = new ArchiveInvalidator();
+        $invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
 
         self::$fixture->trackVisits();
 
diff --git a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__Actions.getPageUrls_week.xml b/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__Actions.getPageUrls_week.xml
deleted file mode 100644
index bcd758b7c59def8c1b05e3f1b2cdaf8e32888f26..0000000000000000000000000000000000000000
--- a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__Actions.getPageUrls_week.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-<?xml version="1.0" encoding="utf-8" ?>
-<results>
-	<result date="From 2010-01-04 to 2010-01-10">
-		<row>
-			<label>category</label>
-			<nb_visits>6</nb_visits>
-			<nb_hits>9</nb_hits>
-			<sum_time_spent>0</sum_time_spent>
-			<entry_nb_visits>2</entry_nb_visits>
-			<entry_nb_actions>18</entry_nb_actions>
-			<entry_sum_visit_length>2</entry_sum_visit_length>
-			<entry_bounce_count>0</entry_bounce_count>
-			<avg_time_on_page>0</avg_time_on_page>
-			<bounce_rate>0%</bounce_rate>
-			<exit_rate>0%</exit_rate>
-			<subtable>
-				<row>
-					<label>/Page1</label>
-					<nb_visits>2</nb_visits>
-					<nb_hits>3</nb_hits>
-					<sum_time_spent>0</sum_time_spent>
-					<entry_nb_visits>2</entry_nb_visits>
-					<entry_nb_actions>18</entry_nb_actions>
-					<entry_sum_visit_length>2</entry_sum_visit_length>
-					<entry_bounce_count>0</entry_bounce_count>
-					<sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors>
-					<sum_daily_entry_nb_uniq_visitors>2</sum_daily_entry_nb_uniq_visitors>
-					<avg_time_on_page>0</avg_time_on_page>
-					<bounce_rate>0%</bounce_rate>
-					<exit_rate>0%</exit_rate>
-					<url>http://example.org/category/Page1</url>
-				</row>
-				<row>
-					<label>/Page2</label>
-					<nb_visits>2</nb_visits>
-					<nb_hits>3</nb_hits>
-					<sum_time_spent>0</sum_time_spent>
-					<sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors>
-					<avg_time_on_page>0</avg_time_on_page>
-					<bounce_rate>0%</bounce_rate>
-					<exit_rate>0%</exit_rate>
-					<url>http://example.org/category/Page2</url>
-				</row>
-				<row>
-					<label>/NewPage</label>
-					<nb_visits>1</nb_visits>
-					<nb_hits>2</nb_hits>
-					<sum_time_spent>0</sum_time_spent>
-					<sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
-					<avg_time_on_page>0</avg_time_on_page>
-					<bounce_rate>0%</bounce_rate>
-					<exit_rate>0%</exit_rate>
-					<url>http://example.org/category/NewPage</url>
-				</row>
-				<row>
-					<label>/Page3</label>
-					<nb_visits>1</nb_visits>
-					<nb_hits>1</nb_hits>
-					<sum_time_spent>0</sum_time_spent>
-					<sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
-					<avg_time_on_page>0</avg_time_on_page>
-					<bounce_rate>0%</bounce_rate>
-					<exit_rate>0%</exit_rate>
-					<url>http://example.org/category/Page3</url>
-				</row>
-			</subtable>
-		</row>
-	</result>
-	<result date="From 2010-01-11 to 2010-01-17" />
-	<result date="From 2010-01-18 to 2010-01-24" />
-	<result date="From 2010-01-25 to 2010-01-31" />
-	<result date="From 2010-02-01 to 2010-02-07" />
-</results>
\ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__VisitsSummary.get_week.xml b/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__VisitsSummary.get_week.xml
deleted file mode 100644
index c494788d70b5d79c3e6d71382ce1237abe7c371d..0000000000000000000000000000000000000000
--- a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__VisitsSummary.get_week.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8" ?>
-<results>
-	<result date="From 2010-01-04 to 2010-01-10">
-		<nb_uniq_visitors>2</nb_uniq_visitors>
-		<nb_users>1</nb_users>
-		<nb_visits>2</nb_visits>
-		<nb_actions>18</nb_actions>
-		<nb_visits_converted>0</nb_visits_converted>
-		<bounce_count>0</bounce_count>
-		<sum_visit_length>2</sum_visit_length>
-		<max_actions>12</max_actions>
-		<bounce_rate>0%</bounce_rate>
-		<nb_actions_per_visit>9</nb_actions_per_visit>
-		<avg_time_on_site>1</avg_time_on_site>
-	</result>
-	<result date="From 2010-01-11 to 2010-01-17" />
-	<result date="From 2010-01-18 to 2010-01-24" />
-	<result date="From 2010-01-25 to 2010-01-31" />
-	<result date="From 2010-02-01 to 2010-02-07" />
-</results>
\ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__Actions.getPageUrls_week.xml b/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseDayWasNotInvalidated__Actions.getPageUrls_day.xml
similarity index 76%
rename from tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__Actions.getPageUrls_week.xml
rename to tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseDayWasNotInvalidated__Actions.getPageUrls_day.xml
index 3f580dfcf7a32545bfdb064b3078e73726dad24c..dea43adf43064b612956850ba5caf34755d4396c 100644
--- a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__Actions.getPageUrls_week.xml
+++ b/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseDayWasNotInvalidated__Actions.getPageUrls_day.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8" ?>
 <results>
-	<result date="From 2010-01-04 to 2010-01-10">
+	<result date="2010-01-06">
 		<row>
 			<label>category</label>
 			<nb_visits>3</nb_visits>
@@ -17,14 +17,14 @@
 				<row>
 					<label>/Page1</label>
 					<nb_visits>1</nb_visits>
+					<nb_uniq_visitors>1</nb_uniq_visitors>
 					<nb_hits>1</nb_hits>
 					<sum_time_spent>0</sum_time_spent>
+					<entry_nb_uniq_visitors>1</entry_nb_uniq_visitors>
 					<entry_nb_visits>1</entry_nb_visits>
 					<entry_nb_actions>6</entry_nb_actions>
 					<entry_sum_visit_length>1</entry_sum_visit_length>
 					<entry_bounce_count>0</entry_bounce_count>
-					<sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
-					<sum_daily_entry_nb_uniq_visitors>1</sum_daily_entry_nb_uniq_visitors>
 					<avg_time_on_page>0</avg_time_on_page>
 					<bounce_rate>0%</bounce_rate>
 					<exit_rate>0%</exit_rate>
@@ -33,9 +33,9 @@
 				<row>
 					<label>/Page2</label>
 					<nb_visits>1</nb_visits>
+					<nb_uniq_visitors>1</nb_uniq_visitors>
 					<nb_hits>1</nb_hits>
 					<sum_time_spent>0</sum_time_spent>
-					<sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
 					<avg_time_on_page>0</avg_time_on_page>
 					<bounce_rate>0%</bounce_rate>
 					<exit_rate>0%</exit_rate>
@@ -44,9 +44,9 @@
 				<row>
 					<label>/Page3</label>
 					<nb_visits>1</nb_visits>
+					<nb_uniq_visitors>1</nb_uniq_visitors>
 					<nb_hits>1</nb_hits>
 					<sum_time_spent>0</sum_time_spent>
-					<sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
 					<avg_time_on_page>0</avg_time_on_page>
 					<bounce_rate>0%</bounce_rate>
 					<exit_rate>0%</exit_rate>
@@ -55,8 +55,8 @@
 			</subtable>
 		</row>
 	</result>
-	<result date="From 2010-01-11 to 2010-01-17" />
-	<result date="From 2010-01-18 to 2010-01-24" />
-	<result date="From 2010-01-25 to 2010-01-31" />
-	<result date="From 2010-02-01 to 2010-02-07" />
+	<result date="2010-01-07" />
+	<result date="2010-01-08" />
+	<result date="2010-01-09" />
+	<result date="2010-01-10" />
 </results>
\ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__VisitsSummary.get_week.xml b/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseDayWasNotInvalidated__VisitsSummary.get_day.xml
similarity index 65%
rename from tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__VisitsSummary.get_week.xml
rename to tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseDayWasNotInvalidated__VisitsSummary.get_day.xml
index 75d267cfc4226333800fe2485fd44ae66dc0d107..241f0a7a02a797112990e673746f6921b16dbf4e 100644
--- a/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseWeekWasNotInvalidated__VisitsSummary.get_week.xml
+++ b/tests/PHPUnit/System/expected/test_Archive_InvalidationWebsite2_NewDataShouldNotAppear_BecauseDayWasNotInvalidated__VisitsSummary.get_day.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8" ?>
 <results>
-	<result date="From 2010-01-04 to 2010-01-10">
+	<result date="2010-01-06">
 		<nb_uniq_visitors>1</nb_uniq_visitors>
 		<nb_users>0</nb_users>
 		<nb_visits>1</nb_visits>
@@ -13,8 +13,8 @@
 		<nb_actions_per_visit>6</nb_actions_per_visit>
 		<avg_time_on_site>1</avg_time_on_site>
 	</result>
-	<result date="From 2010-01-11 to 2010-01-17" />
-	<result date="From 2010-01-18 to 2010-01-24" />
-	<result date="From 2010-01-25 to 2010-01-31" />
-	<result date="From 2010-02-01 to 2010-02-07" />
+	<result date="2010-01-07" />
+	<result date="2010-01-08" />
+	<result date="2010-01-09" />
+	<result date="2010-01-10" />
 </results>
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/DataAccess/ArchiveTableCreatorTest.php b/tests/PHPUnit/Unit/DataAccess/ArchiveTableCreatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7d879aa0da517f47d426425ac5ae1ee4cb681594
--- /dev/null
+++ b/tests/PHPUnit/Unit/DataAccess/ArchiveTableCreatorTest.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Tests\Unit\DataAccess;
+
+use Piwik\DataAccess\ArchiveTableCreator;
+
+/**
+ * @group Core
+ */
+class ArchiveTableCreatorTest extends \PHPUnit_Framework_TestCase
+{
+    private $tables;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->tables = array(
+            'archive_numeric_2015_02',
+            'archive_blob_2015_05',
+            'garbage',
+            'archive_numeric_2014_03',
+            'archive_blob_2015_01',
+            'archive_blob_2015_02',
+            'aslkdfjsd',
+            'prefixed_archive_numeric_2012_01',
+        );
+    }
+
+    public function tearDown()
+    {
+        ArchiveTableCreator::clear();
+
+        parent::tearDown();
+    }
+
+    /**
+     * @dataProvider getTestDataForGetTablesArchivesInstalled
+     */
+    public function test_getTablesArchivesInstalled_CorrectlyFiltersTableNames($type, $expectedTables)
+    {
+        ArchiveTableCreator::$tablesAlreadyInstalled = $this->tables;
+
+        $tables = ArchiveTableCreator::getTablesArchivesInstalled($type);
+
+        $this->assertEquals($expectedTables, $tables);
+    }
+
+    public function getTestDataForGetTablesArchivesInstalled()
+    {
+        return array(
+            array(
+                ArchiveTableCreator::BLOB_TABLE,
+                array(
+                    'archive_blob_2015_05',
+                    'archive_blob_2015_01',
+                    'archive_blob_2015_02',
+                ),
+            ),
+
+            array(
+                ArchiveTableCreator::NUMERIC_TABLE,
+                array(
+                    'archive_numeric_2015_02',
+                    'archive_numeric_2014_03',
+                    'prefixed_archive_numeric_2012_01',
+                ),
+            ),
+
+            array(
+                'qewroufsjdlf',
+                array(),
+            ),
+
+            array(
+                '',
+                array(
+                    'archive_numeric_2015_02',
+                    'archive_blob_2015_05',
+                    'archive_numeric_2014_03',
+                    'archive_blob_2015_01',
+                    'archive_blob_2015_02',
+                    'prefixed_archive_numeric_2012_01',
+                ),
+            ),
+
+            array(
+                null,
+                array(
+                    'archive_numeric_2015_02',
+                    'archive_blob_2015_05',
+                    'archive_numeric_2014_03',
+                    'archive_blob_2015_01',
+                    'archive_blob_2015_02',
+                    'prefixed_archive_numeric_2012_01',
+                ),
+            ),
+        );
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Period/RangeTest.php b/tests/PHPUnit/Unit/Period/RangeTest.php
index 115a45fd8b0cfad74caf900f79dfa8830dbd7a13..9b0a8215511a6e6c21b8d2dfed06fb88fe64decc 100644
--- a/tests/PHPUnit/Unit/Period/RangeTest.php
+++ b/tests/PHPUnit/Unit/Period/RangeTest.php
@@ -10,6 +10,7 @@ namespace Piwik\Tests\Unit\Period;
 
 use Exception;
 use Piwik\Date;
+use Piwik\Period;
 use Piwik\Period\Month;
 use Piwik\Period\Range;
 use Piwik\Period\Week;
diff --git a/tests/PHPUnit/Unit/PeriodTest.php b/tests/PHPUnit/Unit/PeriodTest.php
index c8dc2c757eafcb1d8629a6004b4c628f522e04d3..f4c7d4ee1d88568918f09d8d1fa30e187b4435a7 100644
--- a/tests/PHPUnit/Unit/PeriodTest.php
+++ b/tests/PHPUnit/Unit/PeriodTest.php
@@ -123,4 +123,91 @@ class PeriodTest extends \PHPUnit_Framework_TestCase
             array(3434),
         );
     }
+
+    /**
+     * @dataProvider getTestDataForGetAllOverlappingChildPeriods
+     */
+    public function test_getAllOverlappingChildPeriods_ReturnsTheCorrectChildPeriods($periodType, $dateRange, $expectedChildPeriodRanges)
+    {
+        $period = Period\Factory::build($periodType, $dateRange);
+
+        $overlappingPeriods = $period->getAllOverlappingChildPeriods();
+        $overlappingPeriods = $this->getPeriodInfoForAssert($overlappingPeriods);
+
+        $this->assertEquals($expectedChildPeriodRanges, $overlappingPeriods);
+    }
+
+    public function getTestDataForGetAllOverlappingChildPeriods()
+    {
+        return array(
+            array(
+                'month',
+                '2015-09-10',
+                array(
+                    array('week', '2015-08-31,2015-09-06'),
+                    array('week', '2015-09-07,2015-09-13'),
+                    array('week', '2015-09-14,2015-09-20'),
+                    array('week', '2015-09-21,2015-09-27'),
+                    array('week', '2015-09-28,2015-10-04'),
+                    array('day', '2015-09-01,2015-09-01'),
+                    array('day', '2015-09-02,2015-09-02'),
+                    array('day', '2015-09-03,2015-09-03'),
+                    array('day', '2015-09-04,2015-09-04'),
+                    array('day', '2015-09-05,2015-09-05'),
+                    array('day', '2015-09-06,2015-09-06'),
+                    array('day', '2015-09-07,2015-09-07'),
+                    array('day', '2015-09-08,2015-09-08'),
+                    array('day', '2015-09-09,2015-09-09'),
+                    array('day', '2015-09-10,2015-09-10'),
+                    array('day', '2015-09-11,2015-09-11'),
+                    array('day', '2015-09-12,2015-09-12'),
+                    array('day', '2015-09-13,2015-09-13'),
+                    array('day', '2015-09-14,2015-09-14'),
+                    array('day', '2015-09-15,2015-09-15'),
+                    array('day', '2015-09-16,2015-09-16'),
+                    array('day', '2015-09-17,2015-09-17'),
+                    array('day', '2015-09-18,2015-09-18'),
+                    array('day', '2015-09-19,2015-09-19'),
+                    array('day', '2015-09-20,2015-09-20'),
+                    array('day', '2015-09-21,2015-09-21'),
+                    array('day', '2015-09-22,2015-09-22'),
+                    array('day', '2015-09-23,2015-09-23'),
+                    array('day', '2015-09-24,2015-09-24'),
+                    array('day', '2015-09-25,2015-09-25'),
+                    array('day', '2015-09-26,2015-09-26'),
+                    array('day', '2015-09-27,2015-09-27'),
+                    array('day', '2015-09-28,2015-09-28'),
+                    array('day', '2015-09-29,2015-09-29'),
+                    array('day', '2015-09-30,2015-09-30'),
+                ),
+            ),
+
+            array(
+                'week',
+                '2015-09-03',
+                array(
+                    array('day', '2015-08-31,2015-08-31'),
+                    array('day', '2015-09-01,2015-09-01'),
+                    array('day', '2015-09-02,2015-09-02'),
+                    array('day', '2015-09-03,2015-09-03'),
+                    array('day', '2015-09-04,2015-09-04'),
+                    array('day', '2015-09-05,2015-09-05'),
+                    array('day', '2015-09-06,2015-09-06'),
+                ),
+            ),
+
+            array(
+                'day',
+                '2015-09-05',
+                array(),
+            ),
+        );
+    }
+
+    private function getPeriodInfoForAssert($periods)
+    {
+        return array_map(function (Period $period) {
+            return array($period->getLabel(), $period->getRangeString());
+        }, $periods);
+    }
 }
\ No newline at end of file