// Copyright (c) 2012-2017 VideoStitch SAS
// Copyright (c) 2018 stitchEm

#include "gpu/testing.hpp"

#include "libvideostitch/parse.hpp"
#include "libvideostitch/panoDef.hpp"

#include "exposure/metadataProcessor.hpp"

namespace VideoStitch {
namespace Testing {

static const int NUM_INPUTS{4};

std::unique_ptr<Core::PanoDefinition> getTestPanoDef() {
  Potential<Ptv::Parser> parser(Ptv::Parser::create());
  if (!parser->parseData("{"
                         " \"width\": 513, "
                         " \"height\": 315, "
                         " \"hfov\": 90.0, "
                         " \"proj\": \"rectilinear\", "
                         " \"inputs\": [ "
                         "  { "
                         "   \"width\": 17, "
                         "   \"height\": 13, "
                         "   \"hfov\": 90.0, "
                         "   \"yaw\": 0.0, "
                         "   \"pitch\": 0.0, "
                         "   \"roll\": 0.0, "
                         "   \"proj\": \"rectilinear\", "
                         "   \"viewpoint_model\": \"ptgui\", "
                         "   \"response\": \"linear\", "
                         "   \"filename\": \"\" "
                         "  }, "
                         "  { "
                         "   \"width\": 17, "
                         "   \"height\": 13, "
                         "   \"hfov\": 90.0, "
                         "   \"yaw\": 0.0, "
                         "   \"pitch\": 0.0, "
                         "   \"roll\": 0.0, "
                         "   \"proj\": \"rectilinear\", "
                         "   \"viewpoint_model\": \"ptgui\", "
                         "   \"response\": \"linear\", "
                         "   \"filename\": \"\" "
                         "  }, "
                         "  { "
                         "   \"width\": 17, "
                         "   \"height\": 13, "
                         "   \"hfov\": 90.0, "
                         "   \"yaw\": 0.0, "
                         "   \"pitch\": 0.0, "
                         "   \"roll\": 0.0, "
                         "   \"proj\": \"rectilinear\", "
                         "   \"viewpoint_model\": \"ptgui\", "
                         "   \"response\": \"linear\", "
                         "   \"filename\": \"\" "
                         "  }, "
                         "  { "
                         "   \"width\": 17, "
                         "   \"height\": 13, "
                         "   \"hfov\": 90.0, "
                         "   \"yaw\": 0.0, "
                         "   \"pitch\": 0.0, "
                         "   \"roll\": 0.0, "
                         "   \"proj\": \"rectilinear\", "
                         "   \"viewpoint_model\": \"ptgui\", "
                         "   \"response\": \"linear\", "
                         "   \"filename\": \"\" "
                         "  } "
                         " ]"
                         "}")) {
    std::cerr << parser->getErrorMessage() << std::endl;
    ENSURE(false, "could not parse");
    return NULL;
  }
  std::unique_ptr<Core::PanoDefinition> panoDef(Core::PanoDefinition::create(parser->getRoot()));
  ENSURE((bool)panoDef);
  return panoDef;
}

void ENSURE_CURVE_POINTS(const Core::Curve& curve, const std::vector<std::pair<int, double>> points,
                         const char* msg = "") {
  for (const std::pair<int, double>& point : points) {
    ENSURE_EQ(curve.at(point.first), point.second, msg);
  }
}

void testAppendExposure() {
  std::unique_ptr<Core::PanoDefinition> panoDef = getTestPanoDef();
  ENSURE(panoDef->numVideoInputs() == NUM_INPUTS);

  FrameRate frameRate{30, 1};

  std::vector<std::pair<int, GPU::Buffer<const uint32_t>>> frames{{0, {}}};

  Exposure::MetadataProcessor mp;

  frameid_t exposureDataFrame{1000};
  mtime_t exposureDataTime = frameRate.frameToTimestamp(exposureDataFrame);
  Metadata::Exposure exposure{exposureDataTime, 800, 0.03f, 0.03f};

  Input::MetadataChunk metadata;
  metadata.exposure.push_back({{0, exposure}});

  auto applyExposureToPanoAtFrame = [&mp, &metadata, &frameRate, &panoDef](frameid_t currentStitchingFrame) {
    std::unique_ptr<Core::PanoDefinition> updated =
        mp.createUpdatedPano(metadata, *panoDef, frameRate, currentStitchingFrame);
    if (updated) {
      panoDef.reset(updated.release());
    }
  };

  ENSURE_CURVE_POINTS(panoDef->getExposureValue(), {{0, 0.}, {1, 0.}}, "Test pano exposure should be uninitialized");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "First reader exposure value should be uninitialized");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "Second reader exposure value should be uninitialized");

  applyExposureToPanoAtFrame(0);

  // checking global exposure
  {
    // global exposure compensation should be average of inputs at all time
    auto globalExposureComp = exposure.computeEv() / NUM_INPUTS;

    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                          // current frame unchanged
        {exposureDataFrame / 2, globalExposureComp / 2},  // interpolation --> half time at half value
        {exposureDataFrame, globalExposureComp},          // final exposure value that was set
        {exposureDataFrame + 100, globalExposureComp},    // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getExposureValue(), expectedCurve, "Panorama global exposure, see comments");
  }

  // checking exposure for input 0
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                             // current frame unchanged
        {exposureDataFrame / 2, -exposure.computeEv() / 2},  // interpolation --> half time at half value
        {exposureDataFrame, -exposure.computeEv()},          // final exposure value that was set
        {exposureDataFrame + 100, -exposure.computeEv()},    // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), expectedCurve,
                        "Exposure of input 0, see comments");
  }

  // exposure for input 1 shouldn't have changed
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},
        {exposureDataFrame / 2, 0.},
        {exposureDataFrame, 0.},
        {exposureDataFrame + 100, 0.},
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), expectedCurve,
                        "Exposure of input 1 should remain unchanged");
  }

  frameid_t nextExposureDataFrame = 2000;
  mtime_t nextExposureDataTime = frameRate.frameToTimestamp(nextExposureDataFrame);

  Metadata::Exposure nextExposure{nextExposureDataTime, 400, 0.03f, 0.03f};

  metadata.clear();
  metadata.exposure.push_back({{0, nextExposure}});

  applyExposureToPanoAtFrame(0);

  // checking global exposure
  {
    // global exposure compensation should be average of inputs at all time
    auto globalExposureComp = exposure.computeEv() / NUM_INPUTS;
    auto nextGlobalExposureComp = nextExposure.computeEv() / NUM_INPUTS;

    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                                // current frame unchanged
        {exposureDataFrame, globalExposureComp},                // previous exposure value that was set
        {nextExposureDataFrame, nextGlobalExposureComp},        // final exposure value that was set
        {nextExposureDataFrame + 100, nextGlobalExposureComp},  // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getExposureValue(), expectedCurve, "Panorama global exposure, see comments");
  }

  // checking exposure for input 0
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                                   // current frame unchanged
        {exposureDataFrame, -exposure.computeEv()},                // previous exposure value that was set
        {nextExposureDataFrame, -nextExposure.computeEv()},        // exposure value that was set last
        {nextExposureDataFrame + 100, -nextExposure.computeEv()},  // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), expectedCurve,
                        "Exposure of input 0, see comments");
  }

  // interpolation --> half time at ~half value, exact interpolation value implementation defined by the curve
  ENSURE_APPROX_EQ(panoDef->getVideoInput(0).getExposureValue().at(exposureDataFrame / 2), -2.06899, 0.1,
                   "Regression test: implementation defined curve interpolation value");

  // exposure for input 1 shouldn't have changed
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},
        {exposureDataFrame / 2, 0.},
        {exposureDataFrame, 0.},
        {exposureDataFrame + 100, 0.},
        {nextExposureDataFrame, 0.},
        {nextExposureDataFrame + 100, 0.},
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), expectedCurve,
                        "Exposure of input 1 should remain unchanged");
  }
}

void testTryAppendInvalidExposure() {
  std::unique_ptr<Core::PanoDefinition> panoDef = getTestPanoDef();
  ENSURE(panoDef->numVideoInputs() == NUM_INPUTS);

  FrameRate frameRate{30, 1};

  std::vector<std::pair<int, GPU::Buffer<const uint32_t>>> frames{{0, {}}};

  Exposure::MetadataProcessor mp;

  frameid_t exposureDataFrame{1000};
  mtime_t exposureDataTime = frameRate.frameToTimestamp(exposureDataFrame);

  // invalid exposure with ISO = 0
  Metadata::Exposure exposure{exposureDataTime, 0, 0.02f, 0.02f};

  Input::MetadataChunk metadata;
  metadata.exposure.push_back({{0, exposure}});

  auto applyExposureToPanoAtFrame = [&mp, &metadata, &frameRate, &panoDef](frameid_t currentStitchingFrame) {
    std::unique_ptr<Core::PanoDefinition> updated =
        mp.createUpdatedPano(metadata, *panoDef, frameRate, currentStitchingFrame);
    if (updated) {
      panoDef.reset(updated.release());
    }
  };

  applyExposureToPanoAtFrame(0);

  ENSURE_CURVE_POINTS(panoDef->getExposureValue(), {{0, 0.}, {1, 0.}}, "Test pano exposure should remain at EV 0");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "First reader exposure value should remain at EV 0");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "Second reader exposure value should be remain EV 0");

  // metadata for non-existing input, should be ignored, not crash
  metadata.clear();
  metadata.exposure.push_back({{NUM_INPUTS, exposure}});
  applyExposureToPanoAtFrame(0);

  ENSURE_CURVE_POINTS(panoDef->getExposureValue(), {{0, 0.}, {1, 0.}}, "Test pano exposure should remain at EV 0");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "First reader exposure value should remain at EV 0");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "Second reader exposure value should be remain EV 0");
}

void testAddMeasurementsForTwoSensors() {
  std::unique_ptr<Core::PanoDefinition> panoDef = getTestPanoDef();
  FrameRate frameRate{30, 1};

  std::vector<std::pair<int, GPU::Buffer<const uint32_t>>> frames{{0, {}}};

  Exposure::MetadataProcessor mp;

  frameid_t exposureDataFrame{1000};
  mtime_t exposureDataTime = frameRate.frameToTimestamp(exposureDataFrame);
  Metadata::Exposure exposure_0{exposureDataTime, 800, 0.03f, 0.03f};
  Metadata::Exposure exposure_1{exposureDataTime, 800, 0.03f, 0.03f};

  Input::MetadataChunk metadata;
  metadata.exposure.push_back({{0, exposure_0}, {1, exposure_1}});

  auto applyExposureToPanoAtFrame = [&mp, &metadata, &frameRate, &panoDef](frameid_t currentStitchingFrame) {
    std::unique_ptr<Core::PanoDefinition> updated =
        mp.createUpdatedPano(metadata, *panoDef, frameRate, currentStitchingFrame);
    if (updated) {
      panoDef.reset(updated.release());
    }
  };

  ENSURE_CURVE_POINTS(panoDef->getExposureValue(), {{0, 0.}, {1, 0.}}, "Test pano exposure should be uninitialized");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "First reader exposure value should be uninitialized");
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), {{0, 0.}, {1, 0.}},
                      "Second reader exposure value should be uninitialized");

  applyExposureToPanoAtFrame(0);

  // checking global exposure
  {
    // global exposure compensation should be average of inputs at all time
    auto globalExposureComp = (exposure_0.computeEv() + exposure_1.computeEv()) / NUM_INPUTS;

    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                          // current frame unchanged
        {exposureDataFrame / 2, globalExposureComp / 2},  // interpolation --> half time at half value
        {exposureDataFrame, globalExposureComp},          // final exposure value that was set
        {exposureDataFrame + 100, globalExposureComp},    // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getExposureValue(), expectedCurve, "Panorama global exposure, see comments");
  }

  // checking exposure for input 0
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                               // current frame unchanged
        {exposureDataFrame / 2, -exposure_0.computeEv() / 2},  // interpolation --> half time at half value
        {exposureDataFrame, -exposure_0.computeEv()},          // final exposure value that was set
        {exposureDataFrame + 100, -exposure_0.computeEv()},    // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), expectedCurve,
                        "Exposure of input 0, see comments");
  }

  // checking exposure for input 0
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                               // current frame unchanged
        {exposureDataFrame / 2, -exposure_1.computeEv() / 2},  // interpolation --> half time at half value
        {exposureDataFrame, -exposure_1.computeEv()},          // final exposure value that was set
        {exposureDataFrame + 100, -exposure_1.computeEv()},    // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), expectedCurve,
                        "Exposure of input 1, see comments");
  }

  frameid_t nextExposureDataFrame = 2000;
  mtime_t nextExposureDataTime = frameRate.frameToTimestamp(nextExposureDataFrame);

  Metadata::Exposure nextExposure_0{nextExposureDataTime, 200, 0.03f, 0.03f};
  Metadata::Exposure nextExposure_1{nextExposureDataTime, 800, 0.03f, 0.03f};

  metadata.clear();
  metadata.exposure.push_back({{0, nextExposure_0}, {1, nextExposure_1}});

  applyExposureToPanoAtFrame(0);

  // checking global exposure
  {
    // global exposure compensation should be average of inputs at all time
    auto globalExposureComp = (exposure_0.computeEv() + exposure_1.computeEv()) / NUM_INPUTS;
    auto nextGlobalExposureComp = (nextExposure_0.computeEv() + nextExposure_1.computeEv()) / NUM_INPUTS;

    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                                // current frame unchanged
        {exposureDataFrame, globalExposureComp},                // previous exposure value that was set
        {nextExposureDataFrame, nextGlobalExposureComp},        // final exposure value that was set
        {nextExposureDataFrame + 100, nextGlobalExposureComp},  // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getExposureValue(), expectedCurve, "Panorama global exposure, see comments");
  }

  // checking exposure for input 0
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                                     // current frame unchanged
        {exposureDataFrame, -exposure_0.computeEv()},                // previous exposure value that was set
        {nextExposureDataFrame, -nextExposure_0.computeEv()},        // exposure value that was set last
        {nextExposureDataFrame + 100, -nextExposure_0.computeEv()},  // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), expectedCurve,
                        "Exposure of input 0, see comments");
  }

  // interpolation --> half time at ~half value, exact interpolation value implementation defined by the curve
  ENSURE_APPROX_EQ(panoDef->getVideoInput(0).getExposureValue().at(exposureDataFrame / 2), -2.13149, 0.1,
                   "Regression test: implementation defined curve interpolation value");

  // checking exposure for input 1
  {
    std::vector<std::pair<int, double>> expectedCurve = {
        {0, 0.},                                                     // current frame unchanged
        {exposureDataFrame, -exposure_1.computeEv()},                // previous exposure value that was set
        {nextExposureDataFrame, -nextExposure_1.computeEv()},        // exposure value that was set last
        {nextExposureDataFrame + 100, -nextExposure_1.computeEv()},  // final exposure value that was set continues
    };

    ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), expectedCurve,
                        "Exposure of input 1, see comments");
  }
}

void testNoJumps() {
  std::unique_ptr<Core::PanoDefinition> panoDef = getTestPanoDef();
  FrameRate frameRate{30, 1};

  std::vector<std::pair<int, GPU::Buffer<const uint32_t>>> frames{{0, {}}};

  Exposure::MetadataProcessor mp;

  frameid_t exposureDataFrame{1000};
  mtime_t exposureDataTime = frameRate.frameToTimestamp(exposureDataFrame);
  Metadata::Exposure exposure{exposureDataTime, 800, 0.02f, 0.02f};

  Input::MetadataChunk metadata;
  metadata.exposure.push_back({{0, exposure}});

  auto applyExposureToPanoAtFrame = [&mp, &metadata, &frameRate, &panoDef](frameid_t currentStitchingFrame) {
    std::unique_ptr<Core::PanoDefinition> updated =
        mp.createUpdatedPano(metadata, *panoDef, frameRate, currentStitchingFrame);
    if (updated) {
      panoDef.reset(updated.release());
    }
  };

  // covered by other tests
  applyExposureToPanoAtFrame(0);

  std::unique_ptr<Core::PanoDefinition> setupPanoDef(panoDef->clone());

  for (frameid_t nextExposureDataFrame : {1000, 1001, 1010, 1500, 2000}) {
    for (int currentStitcherFrame :
         {500, 990, 999, 1000, 1001, 1010, 1500, 1950, 1990, 1995, 1999, 2000, 2001, 2005, 2010, 2020}) {
      panoDef.reset(setupPanoDef->clone());

      mtime_t nextExposureDataTime = frameRate.frameToTimestamp(nextExposureDataFrame);

      Metadata::Exposure nextExposure{nextExposureDataTime, 400, 0.02f, 0.02f};

      metadata.clear();
      metadata.exposure.push_back({{0, nextExposure}});

      applyExposureToPanoAtFrame(currentStitcherFrame);

      // checking global exposure
      {
        auto globalExposureComp = setupPanoDef->getExposureValue().at(currentStitcherFrame);
        auto nextGlobalExposureComp = nextExposure.computeEv() / NUM_INPUTS;
        std::vector<std::pair<int, double>> expectedCurve = {
            {currentStitcherFrame, globalExposureComp},  // the value for the current frame should not have changed
            {2100, nextGlobalExposureComp},              // but it is set some time in the future
        };
        ENSURE_CURVE_POINTS(panoDef->getExposureValue(), expectedCurve, "Panorama global exposure, see comments");
      }

      // checking exposure for input 0
      {
        std::vector<std::pair<int, double>> expectedCurve = {
            {currentStitcherFrame,
             setupPanoDef->getVideoInput(0).getExposureValue().at(
                 currentStitcherFrame)},        // the value for the current frame should not have changed
            {2100, -nextExposure.computeEv()},  // but it is set some time in the future
        };
        ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), expectedCurve,
                            "Exposure of input 0, see comments");
      }

      // exposure for input 1 shouldn't have changed
      {
        std::vector<std::pair<int, double>> expectedCurve = {
            {0, 0.},
            {exposureDataFrame / 2, 0.},
            {exposureDataFrame, 0.},
            {exposureDataFrame + 100, 0.},
            {nextExposureDataFrame, 0.},
            {nextExposureDataFrame + 100, 0.},
        };

        ENSURE_CURVE_POINTS(panoDef->getVideoInput(1).getExposureValue(), expectedCurve,
                            "Exposure of input 1 should remain unchanged");
      }
    }
  }
}

void testPruning() {
  Exposure::MetadataProcessor mp;

  std::unique_ptr<Core::PanoDefinition> panoDef = getTestPanoDef();

  FrameRate frameRate{30, 1};
  Input::MetadataChunk metadata;
  Metadata::Exposure exposure{1000, 800, 0.02f, 0.02f};

  // some fake exposure data for camera 1
  // while we do pruning tests on curves for camera 0
  metadata.exposure.push_back({{1, exposure}});

  Core::Spline* exposureSpline = Core::Spline::point(0, 0.);
  exposureSpline->cubicTo(10, 100.)->cubicTo(20, 200.)->cubicTo(30, 300.);

  Core::Curve* exposureCurve = new Core::Curve(exposureSpline);
  panoDef->getVideoInput(0).replaceExposureValue(exposureCurve);

  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "Curve should have been set");

  auto applyExposureToPanoAtFrame = [&mp, &metadata, &frameRate, &panoDef](frameid_t currentStitchingFrame) {
    std::unique_ptr<Core::PanoDefinition> updated =
        mp.createUpdatedPano(metadata, *panoDef, frameRate, currentStitchingFrame);
    if (updated) {
      panoDef.reset(updated.release());
    }
  };

  // update pano at frame 0, no pruning
  applyExposureToPanoAtFrame(0);
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "Curve should remain unchanged");

  // update pano at frame 5, no pruning
  applyExposureToPanoAtFrame(5);
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "Curve should remain unchanged");

  // update pano at frame 10, no pruning
  applyExposureToPanoAtFrame(10);

  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "Curve should remain unchanged");

  // update pano at frame 11, prune first point
  applyExposureToPanoAtFrame(11);
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "Curve should remain unchanged");

  // update pano at frame 11, no change
  applyExposureToPanoAtFrame(11);
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 0.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "Curve should remain unchanged");

  // update pano at frame 29, prune two points
  applyExposureToPanoAtFrame(29);
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 100.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "First point should have been pruned");

  // update pano at frame 30, no change
  applyExposureToPanoAtFrame(30);
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 100.}, {10, 100.}, {20, 200.}, {30, 300.}},
                      "First point should have been pruned");

  // update pano at frame 31, single point remains
  applyExposureToPanoAtFrame(31);
  ENSURE_CURVE_POINTS(panoDef->getVideoInput(0).getExposureValue(), {{0, 200.}, {10, 200.}, {20, 200.}, {30, 300.}},
                      "First two points should have been pruned");
}

void test_VSA_6423() {
  std::unique_ptr<Core::PanoDefinition> panoDef = getTestPanoDef();
  FrameRate frameRate{30, 1};
  Exposure::MetadataProcessor mp;
  Input::MetadataChunk metadata;
  auto inputID = 0;

  {
    frameid_t frame = 10;
    mtime_t exposureDataTime = frameRate.frameToTimestamp(frame);
    Metadata::Exposure exposure{exposureDataTime, (uint16_t)(1), 1.00999f, 1.f};
    metadata.exposure.push_back({{inputID, exposure}});
  }

  {
    frameid_t frame = 20;
    mtime_t exposureDataTime = frameRate.frameToTimestamp(frame);
    Metadata::Exposure exposure{exposureDataTime, (uint16_t)(1), 0.99999f, 1.f};
    metadata.exposure.push_back({{inputID, exposure}});
  }

  // metadata insertion has a useful assertion self-check that compares
  // that curves return the value of newly inserted measurements
  // make sure it's not over-eager, but allows for slight floating point errors
  std::unique_ptr<Core::PanoDefinition> updated = mp.createUpdatedPano(metadata, *panoDef, frameRate, 0);
}

void benchmarkAddingData(int numFrames) {
  std::unique_ptr<Core::PanoDefinition> panoDef = getTestPanoDef();
  FrameRate frameRate{30, 1};

  std::vector<std::pair<int, GPU::Buffer<const uint32_t>>> frames{{0, {}}};

  Exposure::MetadataProcessor mp;

  for (int i = 0; i < numFrames; i++) {
    frameid_t currentStitchingFrame = i;

    frameid_t metadataAhead = i % 50;

    frameid_t exposureDataFrame{currentStitchingFrame + metadataAhead};
    mtime_t exposureDataTime = frameRate.frameToTimestamp(exposureDataFrame);

    Input::MetadataChunk metadata;

    for (int inputID = 0; inputID < NUM_INPUTS; inputID++) {
      Metadata::Exposure exposure{exposureDataTime, (uint16_t)(200 + i % (50 * (inputID + 1))), 0.02f, 0.02f};
      metadata.exposure.push_back({{inputID, exposure}});
    }

    std::unique_ptr<Core::PanoDefinition> updated =
        mp.createUpdatedPano(metadata, *panoDef, frameRate, currentStitchingFrame);
    if (updated) {
      panoDef.reset(updated.release());
    }
  }
}

}  // namespace Testing
}  // namespace VideoStitch

int main() {
  VideoStitch::Testing::initTest();

  VideoStitch::Testing::testAppendExposure();

  VideoStitch::Testing::testTryAppendInvalidExposure();

  VideoStitch::Testing::testAddMeasurementsForTwoSensors();

  VideoStitch::Testing::testNoJumps();

  VideoStitch::Testing::testPruning();

  VideoStitch::Testing::test_VSA_6423();

  VideoStitch::Testing::benchmarkAddingData(3);
  return 0;
}