// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "cc/metrics/scroll_jank_v4_frame_timeline_calculator.h"

#include <algorithm>
#include <cstdint>
#include <iterator>
#include <memory>
#include <set>
#include <sstream>
#include <vector>

#include "base/time/time.h"
#include "cc/metrics/event_metrics.h"
#include "cc/metrics/scroll_jank_v4_frame.h"
#include "cc/test/event_metrics_test_creator.h"
#include "components/viz/common/frame_sinks/begin_frame_args.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "ui/events/types/event_type.h"

namespace cc {

namespace {

using DispatchBeginFrameArgs = ScrollEventMetrics::DispatchBeginFrameArgs;
using BeginFrameArgsForScrollJank =
    ScrollJankV4Frame::BeginFrameArgsForScrollJank;
using ScrollDamage = ScrollJankV4Frame::ScrollDamage;
using DamagingFrame = ScrollJankV4Frame::DamagingFrame;
using NonDamagingFrame = ScrollJankV4Frame::NonDamagingFrame;
using ScrollUpdates = ScrollJankV4Frame::Stage::ScrollUpdates;
using ScrollStart = ScrollJankV4Frame::Stage::ScrollStart;
using ScrollEnd = ScrollJankV4Frame::Stage::ScrollEnd;
using Real = ScrollUpdates::Real;
using ::testing::ElementsAre;
using ::testing::IsEmpty;

constexpr uint64_t kSourceId = 999;
constexpr base::TimeDelta kVsyncInterval = base::Milliseconds(16);

}  // namespace

class ScrollJankV4FrameTimelineCalculatorTest : public testing::Test {
 public:
  ScrollJankV4FrameTimelineCalculatorTest() = default;
  ~ScrollJankV4FrameTimelineCalculatorTest() override = default;

 protected:
  static base::TimeTicks MillisecondsTicks(int ms) {
    return base::TimeTicks() + base::Milliseconds(ms);
  }

  static viz::BeginFrameArgs CreateBeginFrameArgs(int sequence_id,
                                                  base::TimeTicks frame_time) {
    return viz::BeginFrameArgs::Create(
        BEGINFRAME_FROM_HERE, kSourceId, sequence_id, frame_time,
        /* deadline= */ frame_time + kVsyncInterval / 3,
        /* interval= */ kVsyncInterval,
        viz::BeginFrameArgs::BeginFrameArgsType::NORMAL);
  }

  static DispatchBeginFrameArgs CreateDispatchBeginFrameArgs(
      int sequence_id,
      base::TimeTicks frame_time) {
    return {
        .frame_time = frame_time,
        .interval = kVsyncInterval,
        .frame_id = viz::BeginFrameId(kSourceId, sequence_id),
    };
  }

  static std::vector<uint64_t> GetResultIds(
      const ScrollJankV4Frame::Timeline& timeline) {
    std::vector<uint64_t> result_ids;
    std::transform(timeline.begin(), timeline.end(),
                   std::back_inserter(result_ids),
                   [](const auto& frame) { return frame.args.result_id; });
    return result_ids;
  }

  static void ExpectResultIdsAreAllUnique(
      const std::vector<uint64_t>& result_ids) {
    EXPECT_THAT(result_ids,
                ::testing::UnorderedElementsAreArray(
                    std::set<uint64_t>(result_ids.begin(), result_ids.end())));
  }

  EventMetricsTestCreator metrics_creator_;
  ScrollJankV4FrameTimelineCalculator timeline_calculator_;
};

TEST_F(ScrollJankV4FrameTimelineCalculatorTest, NoFrames) {
  EventMetrics::List events_metrics;
  viz::BeginFrameArgs presented_args =
      CreateBeginFrameArgs(42, MillisecondsTicks(666));
  auto timeline = timeline_calculator_.CalculateTimeline(
      events_metrics, presented_args,
      /* presentation_ts= */ MillisecondsTicks(777));
  EXPECT_THAT(timeline, IsEmpty());
}

TEST_F(ScrollJankV4FrameTimelineCalculatorTest, IgnoreNonScrollEvents) {
  EventMetrics::List events_metrics;
  events_metrics.push_back(
      metrics_creator_.CreateEventBuilder(ui::EventType::kTouchMoved)
          .SetTimestamp(MillisecondsTicks(10))
          .SetCausedFrameUpdate(false)
          .Build());
  events_metrics.push_back(
      metrics_creator_.CreateEventBuilder(ui::EventType::kTouchReleased)
          .SetTimestamp(MillisecondsTicks(11))
          .SetCausedFrameUpdate(true)
          .Build());
  viz::BeginFrameArgs presented_args =
      CreateBeginFrameArgs(42, MillisecondsTicks(666));
  auto timeline = timeline_calculator_.CalculateTimeline(
      events_metrics, presented_args,
      /* presentation_ts= */ MillisecondsTicks(777));
  EXPECT_THAT(timeline, IsEmpty());
}

TEST_F(ScrollJankV4FrameTimelineCalculatorTest, OneNonDamagingFrame) {
  DispatchBeginFrameArgs dispatch_args =
      CreateDispatchBeginFrameArgs(31, MillisecondsTicks(111));
  EventMetrics::List events_metrics;
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(10))
          .SetDelta(1.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(dispatch_args)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(11))
          .SetDelta(2.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(false)
          .SetDispatchArgs(dispatch_args)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(12))
          .SetDelta(3.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(true)
          .SetDispatchArgs(dispatch_args)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(13))
          .SetDelta(4.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(dispatch_args)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  viz::BeginFrameArgs presented_args =
      CreateBeginFrameArgs(42, MillisecondsTicks(666));
  auto timeline = timeline_calculator_.CalculateTimeline(
      events_metrics, presented_args,
      /* presentation_ts= */ MillisecondsTicks(777));
  EXPECT_EQ(timeline.size(), 1u);
  std::vector<uint64_t> result_ids = GetResultIds(timeline);
  ExpectResultIdsAreAllUnique(result_ids);
  EXPECT_THAT(
      timeline,
      ElementsAre(ScrollJankV4Frame(
          BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(111),
                                      .interval = kVsyncInterval,
                                      .result_id = result_ids[0]},
          NonDamagingFrame{},
          {ScrollJankV4Frame::Stage{ScrollStart{}},
           ScrollJankV4Frame::Stage{ScrollUpdates(
               Real{
                   .first_input_generation_ts = MillisecondsTicks(10),
                   .last_input_generation_ts = MillisecondsTicks(13),
                   .has_inertial_input = true,
                   .total_raw_delta_pixels = 10.0f,
                   .max_abs_inertial_raw_delta_pixels = 4.0f,
               },
               /* synthetic= */ std::nullopt,
               /* scroll_begin_arrival_timestamp= */
               MillisecondsTicks(5))}})));

  // Check that all GSUs were assigned to the single non-damaging frame.
  for (const auto& event : events_metrics) {
    EXPECT_EQ(event->AsScroll()->scroll_jank_v4_result_id(), result_ids[0]);
  }
}

TEST_F(ScrollJankV4FrameTimelineCalculatorTest, MultipleNonDamagingFrames) {
  DispatchBeginFrameArgs args1 =
      CreateDispatchBeginFrameArgs(31, MillisecondsTicks(111));
  DispatchBeginFrameArgs args2 =
      CreateDispatchBeginFrameArgs(32, MillisecondsTicks(222));
  DispatchBeginFrameArgs args3 =
      CreateDispatchBeginFrameArgs(33, MillisecondsTicks(333));
  EventMetrics::List events_metrics;

  events_metrics.push_back(
      metrics_creator_.FirstGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(10))
          .SetDelta(1.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args1)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(11))
          .SetDelta(2.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args1)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(12))
          .SetDelta(10.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(true)
          .SetDispatchArgs(args2)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(13))
          .SetDelta(20.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(true)
          .SetDispatchArgs(args2)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(14))
          .SetDelta(100.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(false)
          .SetDispatchArgs(args3)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(15))
          .SetDelta(200.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(false)
          .SetDispatchArgs(args3)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  viz::BeginFrameArgs presented_args =
      CreateBeginFrameArgs(42, MillisecondsTicks(666));
  auto timeline = timeline_calculator_.CalculateTimeline(
      events_metrics, presented_args,
      /* presentation_ts= */ MillisecondsTicks(777));
  EXPECT_EQ(timeline.size(), 3u);
  std::vector<uint64_t> result_ids = GetResultIds(timeline);
  ExpectResultIdsAreAllUnique(result_ids);
  EXPECT_THAT(
      timeline,
      ElementsAre(
          ScrollJankV4Frame(
              BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(111),
                                          .interval = kVsyncInterval,
                                          .result_id = result_ids[0]},
              NonDamagingFrame{},
              {ScrollJankV4Frame::Stage{ScrollStart{}},
               ScrollJankV4Frame::Stage{ScrollUpdates(
                   Real{
                       .first_input_generation_ts = MillisecondsTicks(10),
                       .last_input_generation_ts = MillisecondsTicks(11),
                       .has_inertial_input = false,
                       .total_raw_delta_pixels = 3.0f,
                       .max_abs_inertial_raw_delta_pixels = 0.0f,
                   },
                   /* synthetic= */ std::nullopt,
                   /* scroll_begin_arrival_timestamp= */
                   MillisecondsTicks(5))}}),
          ScrollJankV4Frame(
              BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(222),
                                          .interval = kVsyncInterval,
                                          .result_id = result_ids[1]},
              NonDamagingFrame{},
              {ScrollJankV4Frame::Stage{ScrollUpdates(
                  Real{
                      .first_input_generation_ts = MillisecondsTicks(12),
                      .last_input_generation_ts = MillisecondsTicks(13),
                      .has_inertial_input = false,
                      .total_raw_delta_pixels = 30.0f,
                      .max_abs_inertial_raw_delta_pixels = 0.0f,
                  },
                  /* synthetic= */ std::nullopt,
                  /* scroll_begin_arrival_timestamp= */
                  MillisecondsTicks(5))}}),
          ScrollJankV4Frame(
              BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(333),
                                          .interval = kVsyncInterval,
                                          .result_id = result_ids[2]},
              NonDamagingFrame{},
              {ScrollJankV4Frame::Stage{ScrollUpdates(
                  Real{
                      .first_input_generation_ts = MillisecondsTicks(14),
                      .last_input_generation_ts = MillisecondsTicks(15),
                      .has_inertial_input = true,
                      .total_raw_delta_pixels = 300.0f,
                      .max_abs_inertial_raw_delta_pixels = 200.0f,
                  },
                  /* synthetic= */ std::nullopt,
                  /* scroll_begin_arrival_timestamp= */
                  MillisecondsTicks(5))}})));

  // Check that each GSU was assigned to the correct non-damaging frame.
  EXPECT_EQ(events_metrics[0]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[0]);
  EXPECT_EQ(events_metrics[1]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[0]);
  EXPECT_EQ(events_metrics[2]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[1]);
  EXPECT_EQ(events_metrics[3]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[1]);
  EXPECT_EQ(events_metrics[4]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
  EXPECT_EQ(events_metrics[5]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
}

TEST_F(ScrollJankV4FrameTimelineCalculatorTest, OneDamagingFrame) {
  DispatchBeginFrameArgs args1 =
      CreateDispatchBeginFrameArgs(31, MillisecondsTicks(111));
  DispatchBeginFrameArgs args2 =
      CreateDispatchBeginFrameArgs(32, MillisecondsTicks(222));
  DispatchBeginFrameArgs args3 =
      CreateDispatchBeginFrameArgs(33, MillisecondsTicks(333));
  EventMetrics::List events_metrics;

  events_metrics.push_back(
      metrics_creator_.FirstGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(10))
          .SetDelta(1.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args1)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  // events_metrics[1] below is the single damaging input which causes all
  // events to be associated with the presented frame.
  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(11))
          .SetDelta(2.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(true)
          .SetDispatchArgs(args1)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(12))
          .SetDelta(10.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(true)
          .SetDispatchArgs(args2)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(13))
          .SetDelta(20.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(true)
          .SetDispatchArgs(args2)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(14))
          .SetDelta(100.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(false)
          .SetDispatchArgs(args3)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(15))
          .SetDelta(200.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(false)
          .SetDispatchArgs(args3)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  viz::BeginFrameArgs presented_args =
      CreateBeginFrameArgs(42, MillisecondsTicks(666));
  auto timeline = timeline_calculator_.CalculateTimeline(
      events_metrics, presented_args,
      /* presentation_ts= */ MillisecondsTicks(777));
  EXPECT_EQ(timeline.size(), 1u);
  std::vector<uint64_t> result_ids = GetResultIds(timeline);
  ExpectResultIdsAreAllUnique(result_ids);
  EXPECT_THAT(
      timeline,
      ElementsAre(ScrollJankV4Frame(
          BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(666),
                                      .interval = kVsyncInterval,
                                      .result_id = result_ids[0]},

          DamagingFrame{.presentation_ts = MillisecondsTicks(777)},
          {ScrollJankV4Frame::Stage{ScrollStart{}},
           ScrollJankV4Frame::Stage{ScrollUpdates(
               Real{
                   .first_input_generation_ts = MillisecondsTicks(10),
                   .last_input_generation_ts = MillisecondsTicks(15),
                   .has_inertial_input = true,
                   .total_raw_delta_pixels = 333.0f,
                   .max_abs_inertial_raw_delta_pixels = 200.0f,
               },
               /* synthetic= */ std::nullopt,
               /* scroll_begin_arrival_timestamp= */
               MillisecondsTicks(5))}})));

  // Check that all GSUs were assigned to the single damaging frame.
  for (const auto& event : events_metrics) {
    EXPECT_EQ(event->AsScroll()->scroll_jank_v4_result_id(), result_ids[0]);
  }
}

// Example from `ScrollJankV4Frame::Timeline CalculateTimeline()`'s
// documentation.
TEST_F(ScrollJankV4FrameTimelineCalculatorTest,
       MultipleNonDamagingFramesAndOneDamagingFrame) {
  DispatchBeginFrameArgs args1 =
      CreateDispatchBeginFrameArgs(31, MillisecondsTicks(111));
  DispatchBeginFrameArgs args2 =
      CreateDispatchBeginFrameArgs(32, MillisecondsTicks(222));
  DispatchBeginFrameArgs args3 =
      CreateDispatchBeginFrameArgs(33, MillisecondsTicks(333));
  DispatchBeginFrameArgs args4 =
      CreateDispatchBeginFrameArgs(34, MillisecondsTicks(444));
  DispatchBeginFrameArgs args5 =
      CreateDispatchBeginFrameArgs(35, MillisecondsTicks(555));
  EventMetrics::List events_metrics;

  // 1. Non-damaging GSB for BFA1
  // 2. Non-damaging GSU for BFA1
  // 3. Non-damaging GSU for BFA1
  events_metrics.push_back(
      metrics_creator_.GestureScrollBeginBuilder()
          .SetTimestamp(MillisecondsTicks(10))
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .SetCausedFrameUpdate(false)
          .SetDispatchArgs(args1)
          .Build());
  events_metrics.push_back(
      metrics_creator_.FirstGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(11))
          .SetDelta(1.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args1)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());
  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(12))
          .SetDelta(2.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args1)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  // 4. Non-damaging GSE for BFA2
  events_metrics.push_back(
      metrics_creator_.GestureScrollEndBuilder()
          .SetTimestamp(MillisecondsTicks(13))
          .SetCausedFrameUpdate(false)
          .SetDispatchArgs(args2)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(5))
          .Build());

  // 5. Non-damaging GSU for BFA3
  // 6. Damaging GSU for BFA3
  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(14))
          .SetDelta(10.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args3)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(20))
          .Build());
  events_metrics.push_back(
      metrics_creator_.GestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(15))
          .SetDelta(20.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(true)
          .SetDispatchArgs(args3)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(20))
          .Build());

  // 7. Non-damaging GSU for BFA4
  // 8. Non-damaging GSU for BFA4
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(16))
          .SetDelta(100.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args4)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(20))
          .Build());
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(17))
          .SetDelta(200.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args4)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(20))
          .Build());

  // 9. Damaging GSU for BFA5
  // 10. Non-damaging GSU for BFA5
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(18))
          .SetDelta(1000.0f)
          .SetCausedFrameUpdate(true)
          .SetDidScroll(true)
          .SetDispatchArgs(args5)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(20))
          .Build());
  events_metrics.push_back(
      metrics_creator_.InertialGestureScrollUpdateBuilder()
          .SetTimestamp(MillisecondsTicks(19))
          .SetDelta(2000.0f)
          .SetCausedFrameUpdate(false)
          .SetDidScroll(false)
          .SetDispatchArgs(args5)
          .SetScrollBeginArrivalTimestamp(MillisecondsTicks(20))
          .Build());

  viz::BeginFrameArgs presented_args =
      CreateBeginFrameArgs(42, MillisecondsTicks(666));
  auto timeline = timeline_calculator_.CalculateTimeline(
      events_metrics, presented_args,
      /* presentation_ts= */ MillisecondsTicks(777));
  EXPECT_EQ(timeline.size(), 3u);
  std::vector<uint64_t> result_ids = GetResultIds(timeline);
  ExpectResultIdsAreAllUnique(result_ids);
  EXPECT_THAT(
      timeline,
      ElementsAre(
          ScrollJankV4Frame(
              BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(111),
                                          .interval = kVsyncInterval,
                                          .result_id = result_ids[0]},
              NonDamagingFrame{},
              {ScrollJankV4Frame::Stage{ScrollStart{}},
               ScrollJankV4Frame::Stage{ScrollUpdates(
                   Real{
                       .first_input_generation_ts = MillisecondsTicks(11),
                       .last_input_generation_ts = MillisecondsTicks(12),
                       .has_inertial_input = false,
                       .total_raw_delta_pixels = 3.0f,
                       .max_abs_inertial_raw_delta_pixels = 0.0f,
                   },
                   /* synthetic= */ std::nullopt,
                   /* scroll_begin_arrival_timestamp= */
                   MillisecondsTicks(5))}}),
          ScrollJankV4Frame(
              BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(222),
                                          .interval = kVsyncInterval,
                                          .result_id = result_ids[1]},
              NonDamagingFrame{}, {ScrollJankV4Frame::Stage{ScrollEnd{}}}),
          ScrollJankV4Frame(
              BeginFrameArgsForScrollJank{.frame_time = MillisecondsTicks(666),
                                          .interval = kVsyncInterval,
                                          .result_id = result_ids[2]},

              DamagingFrame{.presentation_ts = MillisecondsTicks(777)},
              {ScrollJankV4Frame::Stage{ScrollStart{}},
               ScrollJankV4Frame::Stage{ScrollUpdates(
                   Real{
                       .first_input_generation_ts = MillisecondsTicks(14),
                       .last_input_generation_ts = MillisecondsTicks(19),
                       .has_inertial_input = true,
                       .total_raw_delta_pixels = 3330.0f,
                       .max_abs_inertial_raw_delta_pixels = 2000.0f,
                   },
                   /* synthetic= */ std::nullopt,
                   /* scroll_begin_arrival_timestamp= */
                   MillisecondsTicks(20))}})));

  // GSB should NOT have a result ID.
  EXPECT_FALSE(
      events_metrics[0]->AsScroll()->scroll_jank_v4_result_id().has_value());

  // Check that all GSUs and GSEs were assigned to the correct frame.
  EXPECT_EQ(events_metrics[1]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[0]);
  EXPECT_EQ(events_metrics[2]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[0]);
  EXPECT_EQ(events_metrics[3]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[1]);
  EXPECT_EQ(events_metrics[4]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
  EXPECT_EQ(events_metrics[5]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
  EXPECT_EQ(events_metrics[6]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
  EXPECT_EQ(events_metrics[7]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
  EXPECT_EQ(events_metrics[8]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
  EXPECT_EQ(events_metrics[9]->AsScroll()->scroll_jank_v4_result_id(),
            result_ids[2]);
}

}  // namespace cc
