////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 - 2019, Thomas Lauf, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////

#include <timew.h>
#include <test.h>
#include <src/IntervalFactory.h>

////////////////////////////////////////////////////////////////////////////////
void test_flatten (
  UnitTest& t,
  const std::string& label,
  const std::string& input,
  const std::vector <Range>& exclusions,
  const std::vector <std::string>& output)
{
  Interval i;
  i = IntervalFactory::fromSerialization (input);

  auto results = flatten (i, exclusions);

  t.is (results.size (), output.size (),   "flatten: " + label + " expected number of results");
  for (unsigned int i = 0; i < std::min (output.size (), results.size ()); ++i)
  {
    Interval tmp;
    tmp = IntervalFactory::fromSerialization (output[i]);

    t.is (tmp.start.toISO (), results[i].start.toISO (), "flatten: " + label + " start matches");
    t.is (tmp.end.toISO (),   results[i].end.toISO (),   "flatten: " + label + " end matches");
    t.ok (tmp.tags () == results[i].tags (),                         "flatten: " + label + " tags match");
  }
}

////////////////////////////////////////////////////////////////////////////////
void test_merge (
  UnitTest& t,
  const std::string& label,
  const std::vector <Range>& input,
  const std::vector <Range>& output)
{
  auto results = merge (input);
  t.is (results.size (), output.size (), "merge: " + label + " expected number of results");
  for (unsigned int i = 0; i < std::min (output.size (), results.size ()); ++i)
  {
    t.is (output[i].start.toISO (), results[i].start.toISO (), "merge: " + label + " start matches");
    t.is (output[i].end.toISO (),   results[i].end.toISO (),   "merge: " + label + " end matches");
  }
}

////////////////////////////////////////////////////////////////////////////////
int main (int, char**)
{
  UnitTest t ((7 + 7 + 7 + 4 + 4 + 10) +
              (1 + 3 + 5 + 5 + 3 + 3 + 7)  +
              30);

  // std::vector <Interval> flatten (const Interval&, std::vector <Range>&);
  // input    [---------------------------------------------------)
  // exc      [----------)          [---)         [---------------)
  // output              [----------)   [---------)
  test_flatten (t,
                "[1] (full day) - (3 non-overlapping exc) = (2 inc)",
                "inc 20160427T000000Z - 20160428T000000Z # foo",
                {{Datetime ("20160427T000000Z"), Datetime ("20160427T080000Z")},
                 {Datetime ("20160427T120000Z"), Datetime ("20160427T130000Z")},
                 {Datetime ("20160427T173000Z"), Datetime ("20160428T000000Z")}},
                {"inc 20160427T080000Z - 20160427T120000Z # foo",
                 "inc 20160427T130000Z - 20160427T173000Z # foo"});

  // input                        [-------)
  // exc      [----------)          [---)         [---------------)
  // output                       [-)   [-)
  test_flatten (t,
                "[2] (inc) - (1 enclosed exc) = (2 inc)",
                "inc 20160427T115500Z - 20160427T130500Z # foo",
                {{Datetime ("20160427T000000Z"), Datetime ("20160427T080000Z")},
                 {Datetime ("20160427T120000Z"), Datetime ("20160427T130000Z")},
                 {Datetime ("20160427T173000Z"), Datetime ("20160428T000000Z")}},
                {"inc 20160427T115500Z - 20160427T120000Z # foo",
                 "inc 20160427T130000Z - 20160427T130500Z # foo"});

  // input                                      [...
  // exc      [----------)          [---)         [---------------)
  // output                                     [-)               [...
  test_flatten (t,
                "[3] (open inc) - (1 overlapping exc) = (2 inc)",
                "inc 20160427T160000Z # foo",
                {{Datetime ("20160427T000000Z"), Datetime ("20160427T080000Z")},
                 {Datetime ("20160427T120000Z"), Datetime ("20160427T130000Z")},
                 {Datetime ("20160427T173000Z"), Datetime ("20160428T000000Z")}},
                {"inc 20160427T160000Z - 20160427T173000Z # foo",
                 "inc 20160428T000000Z # foo"});

  // Exclusion encloses interval. Should have no effect.
  // input       [--)
  // exc      [----------)          [---)         [---------------)
  // output      [--)
  test_flatten (t,
                "[4] (inc) - (1 enclosing exc) = (unmodified inc)",
                "inc 20160427T031500Z - 20160427T041500Z # foo",
                {{Datetime ("20160427T000000Z"), Datetime ("20160427T080000Z")},
                 {Datetime ("20160427T120000Z"), Datetime ("20160427T130000Z")},
                 {Datetime ("20160427T173000Z"), Datetime ("20160428T000000Z")}},
                {"inc 20160427T031500Z - 20160427T041500Z # foo"});

  // Multiple overlapping exclusions, no effect.
  // input               [----)
  // exc      [---------------------------------------------------)
  // exc              [----)
  // output              [----)
  test_flatten (t,
                "[5] (inc) - (2 overlapping exc) = (unmodified inc)",
                "inc 20160512T083000Z - 20160512T093000Z # foo",
                {{Datetime ("20160512T080000Z"), Datetime ("20160512T090000Z")},    // One hour.
                 {Datetime ("20160512T000000Z"), Datetime ("20160513T000000Z")}},   // All day.
                {"inc 20160512T083000Z - 20160512T093000Z # foo"});

  // Long-running open inclusion, multiple enclosed exclusions, split.
  // input      [...
  // exc      [---)   [---)   [---)
  // output     [-----)   [---)   [...
  test_flatten (t,
                "[6] (inc) - (1 overlapping exc, 2 enclosed exc) = (3 inc)",
                "inc 20160523T100000Z # foo",
                {{Datetime ("20160523T080000Z"), Datetime ("20160523T120000Z")},
                 {Datetime ("20160523T160000Z"), Datetime ("20160523T170000Z")},
                 {Datetime ("20160523T213000Z"), Datetime ("20160524T040000Z")}},
                {"inc 20160523T100000Z - 20160523T160000Z # foo",
                 "inc 20160523T170000Z - 20160523T213000Z # foo",
                 "inc 20160524T040000Z # foo"});

  // Simple range merging.
  test_merge (t,
              "Empty range",
              {},
              {});

  test_merge (t,
              "Single range",
              {{Datetime ("20160704T000000Z"), Datetime ("20160704T010000Z")}},
              {{Datetime ("20160704T000000Z"), Datetime ("20160704T010000Z")}});

  test_merge (t,
              "Non-overlapping ranges",
              {{Datetime ("20160704T000000Z"), Datetime ("20160704T010000Z")},
               {Datetime ("20160704T020000Z"), Datetime ("20160704T030000Z")}},
              {{Datetime ("20160704T000000Z"), Datetime ("20160704T010000Z")},
               {Datetime ("20160704T020000Z"), Datetime ("20160704T030000Z")}});

  test_merge (t,
              "Non-overlapping unsorted ranges",
              {{Datetime ("20160704T020000Z"), Datetime ("20160704T030000Z")},
               {Datetime ("20160704T000000Z"), Datetime ("20160704T010000Z")}},
              {{Datetime ("20160704T000000Z"), Datetime ("20160704T010000Z")},
               {Datetime ("20160704T020000Z"), Datetime ("20160704T030000Z")}});

  test_merge (t,
              "Overlapping unsorted ranges",
              {{Datetime ("20160704T010000Z"), Datetime ("20160704T030000Z")},
               {Datetime ("20160704T000000Z"), Datetime ("20160704T020000Z")}},
              {{Datetime ("20160704T000000Z"), Datetime ("20160704T030000Z")}});

  test_merge (t,
              "Multiple overlapping ranges",
              {{Datetime ("20160704T010000Z"), Datetime ("20160704T040000Z")},
               {Datetime ("20160704T020000Z"), Datetime ("20160704T050000Z")},
               {Datetime ("20160704T030000Z"), Datetime ("20160704T060000Z")}},
              {{Datetime ("20160704T010000Z"), Datetime ("20160704T060000Z")}});

  test_merge (t,
              "Multiple overlapping, enveloping ranges",
              {{Datetime ("20160703T000000Z"), Datetime ("20160703T010000Z")},
               {Datetime ("20160704T000000Z"), Datetime ("20160705T000000Z")},
               {Datetime ("20160704T010000Z"), Datetime ("20160704T040000Z")},
               {Datetime ("20160704T020000Z"), Datetime ("20160704T050000Z")},
               {Datetime ("20160706T000000Z"), Datetime ("20160706T010000Z")}},
              {{Datetime ("20160703T000000Z"), Datetime ("20160703T010000Z")},
               {Datetime ("20160704T000000Z"), Datetime ("20160705T000000Z")},
               {Datetime ("20160706T000000Z"), Datetime ("20160706T010000Z")}});

  // bool matchesFilter (const Interval& interval, const Interval& filter);
  Interval refOpen;
  refOpen.setRange (Datetime (2016, 6, 1), Datetime (0));
  refOpen.tag ("tag1");
  refOpen.tag ("tag2");

  Interval refClosed;
  refClosed.setRange (Datetime (2016, 6, 1), Datetime (2016, 6, 30));
  refClosed.tag ("tag1");
  refClosed.tag ("tag2");

  t.ok (matchesFilter (refOpen, refClosed), "matchesFilter 2016-06-01- tag1 tag2 <=> 2016-06-01-2016-06-30 tag1 tag2");

  // this                     [--------)
  // A          [--------)
  // B                   [--------)
  // C                          [----)
  // D                             [--------)
  // E                                      [--------)
  // F                      [-------------)
  // G                      [...
  // H                           [...
  // I                                   [...
  Range testA; testA.start = Datetime (2016, 4,  1); testA.end = Datetime (2016, 4, 30);
  Range testB; testB.start = Datetime (2016, 5, 15); testB.end = Datetime (2016, 6, 15);
  Range testC; testC.start = Datetime (2016, 6, 10); testC.end = Datetime (2016, 6, 20);
  Range testD; testD.start = Datetime (2016, 6, 15); testD.end = Datetime (2016, 7, 15);
  Range testE; testE.start = Datetime (2016, 8,  1); testE.end = Datetime (2016, 8, 31);
  Range testF; testF.start = Datetime (2016, 5, 15); testF.end = Datetime (2016, 7, 15);
  Range testG; testG.start = Datetime (2016, 5, 15);
  Range testH; testH.start = Datetime (2016, 6, 15);
  Range testI; testI.start = Datetime (2016, 7, 15);

  Interval i;
  i.tag ("tag1");
  i.tag ("tag2");
  i.setRange (testA); t.notok (matchesFilter (i, refClosed), "matchesFilter A <!> refClosed");
  i.setRange (testB); t.ok    (matchesFilter (i, refClosed), "matchesFilter B <=> refClosed");
  i.setRange (testC); t.ok    (matchesFilter (i, refClosed), "matchesFilter C <=> refClosed");
  i.setRange (testD); t.ok    (matchesFilter (i, refClosed), "matchesFilter D <=> refClosed");
  i.setRange (testE); t.notok (matchesFilter (i, refClosed), "matchesFilter E <!> refClosed");
  i.setRange (testF); t.ok    (matchesFilter (i, refClosed), "matchesFilter F <=> refClosed");
  i.setRange (testG); t.ok    (matchesFilter (i, refClosed), "matchesFilter G <=> refClosed");
  i.setRange (testH); t.ok    (matchesFilter (i, refClosed), "matchesFilter H <=> refClosed");
  i.setRange (testI); t.notok (matchesFilter (i, refClosed), "matchesFilter I <!> refClosed");

  // this                     [...
  // A          [--------)
  // B                   [--------)
  // C                          [----)
  // D                             [--------)
  // E                                      [--------)
  // F                      [-------------)
  // G                      [...
  // H                           [...
  // I                                   [...
  i.setRange (testA); t.notok (matchesFilter (i, refOpen), "matchesFilter A <!> refOpen");
  i.setRange (testB); t.ok    (matchesFilter (i, refOpen), "matchesFilter B <=> refOpen");
  i.setRange (testC); t.ok    (matchesFilter (i, refOpen), "matchesFilter C <=> refOpen");
  i.setRange (testD); t.ok    (matchesFilter (i, refOpen), "matchesFilter D <=> refOpen");
  i.setRange (testE); t.ok    (matchesFilter (i, refOpen), "matchesFilter E <=> refOpen");
  i.setRange (testF); t.ok    (matchesFilter (i, refOpen), "matchesFilter F <=> refOpen");
  i.setRange (testG); t.ok    (matchesFilter (i, refOpen), "matchesFilter G <=> refOpen");
  i.setRange (testH); t.ok    (matchesFilter (i, refOpen), "matchesFilter H <=> refOpen");
  i.setRange (testI); t.ok    (matchesFilter (i, refOpen), "matchesFilter I <=> refOpen");

  // Range getFullDay (const Datetime&);
  auto r1 = getFullDay (Datetime ("20160501T203112"));
  t.ok (r1.start == Datetime ("20160501T000000"), "getFullDay 2016-05-01T20:31:23 -> start 2016-05-01T00:00:00");
  t.ok (r1.end   == Datetime ("20160502T000000"), "getFullDay 2016-05-01T20:31:23 -> end   2016-05-02T00:00:00");

  // std::vector <Range> subtractRanges (const Range&, const std::vector <Range>&, const std::vector <Range>&);
  // Subtract three non-overlapping ranges from a full day, yielding two resultant ranges.
  Range limit (Datetime ("20160101T000000"), Datetime ("20160101T235959"));
  std::vector <Range> exclusions = {{Datetime ("20160101T000000"), Datetime ("20160101T080000")},
                                    {Datetime ("20160101T120000"), Datetime ("20160101T130000")},
                                    {Datetime ("20160101T173000"), Datetime ("20160101T235959")}};
  auto subtracted = subtractRanges ({limit}, exclusions);
  t.ok (subtracted.size () == 2, "subtractRanges: all_day - 3 non-adjacent ranges = 2 ranges");
  t.ok (subtracted[0].start == Datetime ("20160101T080000"), "subtractRanges: results[0].start = 20160101T080000");
  t.ok (subtracted[0].end   == Datetime ("20160101T120000"), "subtractRanges: results[0].end   = 20160101T120000");
  t.ok (subtracted[1].start == Datetime ("20160101T130000"), "subtractRanges: results[1].start = 20160101T130000");
  t.ok (subtracted[1].end   == Datetime ("20160101T173000"), "subtractRanges: results[1].end   = 20160101T173000");

  // Subtract a set of overlapping ranges.
  exclusions = {{Datetime ("20160101T120000"), Datetime ("20160102T120000")},
                {Datetime ("20160101T130000"), Datetime ("20160102T120000")},
                {Datetime ("20160101T140000"), Datetime ("20160102T120000")}};
  subtracted = subtractRanges ({limit}, exclusions);
  t.ok (subtracted.size () == 1, "subtractRanges: all_day - 3 overlapping ranges = 1 range");
  t.ok (subtracted[0].start == Datetime ("20160101T000000"), "subtractRanges: results[0].start = 20160101T080000");
  t.ok (subtracted[0].end   == Datetime ("20160101T120000"), "subtractRanges: results[0].end   = 20160101T120000");

  // Subtract a single range that extends before and after the limit, yielding
  // no results.
  exclusions = {{Datetime ("20151201T000000"), Datetime ("20160201T000000")}};
  subtracted = subtractRanges ({limit}, exclusions);
  t.ok (subtracted.empty (), "subtractRanges: all_day - 2 overlapping months = 0 ranges");

  return 0;
}

////////////////////////////////////////////////////////////////////////////////

