chromium/chrome/browser/ui/ash/glanceables/glanceables_classroom_client_impl_unittest.cc

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

#include "chrome/browser/ui/ash/glanceables/glanceables_classroom_client_impl.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/glanceables/classroom/glanceables_classroom_types.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/default_clock.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/publishers/app_publisher.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/services/app_service/public/cpp/app.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/browser_task_environment.h"
#include "google_apis/common/api_error_codes.h"
#include "google_apis/common/dummy_auth_service.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/common/time_util.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/gaia_urls_overrider_for_testing.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/test/test_shared_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {
namespace {

using ::base::test::TestFuture;
using ::google_apis::ApiErrorCode;
using ::google_apis::util::FormatTimeAsString;
using ::net::test_server::BasicHttpResponse;
using ::net::test_server::HttpMethod;
using ::net::test_server::HttpRequest;
using ::net::test_server::HttpResponse;
using ::testing::_;
using ::testing::AllOf;
using ::testing::ByMove;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::Invoke;
using ::testing::Not;
using ::testing::Return;

using AssignmentListFuture =
    TestFuture<bool,
               std::vector<std::unique_ptr<GlanceablesClassroomAssignment>>>;

// Helper class to simplify mocking `net::EmbeddedTestServer` responses,
// especially useful for subsequent responses when testing pagination logic.
class TestRequestHandler {
 public:
  static std::unique_ptr<HttpResponse> CreateSuccessfulResponse(
      const std::string& content) {
    auto response = std::make_unique<BasicHttpResponse>();
    response->set_code(net::HTTP_OK);
    response->set_content(content);
    response->set_content_type("application/json");
    return response;
  }

  static std::unique_ptr<HttpResponse> CreateFailedResponse() {
    auto response = std::make_unique<BasicHttpResponse>();
    response->set_code(net::HTTP_INTERNAL_SERVER_ERROR);
    return response;
  }

  MOCK_METHOD(std::unique_ptr<HttpResponse>,
              HandleRequest,
              (const HttpRequest&));
};

std::string CreateSubmissionsListResponse(const std::string& course_work_id,
                                          int total_submissions,
                                          int turned_in_submissions,
                                          int graded_submissions) {
  constexpr char kTemplate[] = R"({"id": "student-submissions-%d",)"
                               R"("courseWorkId": "%s", "state": "%s"%s})";
  std::vector<std::string> submissions;
  for (int i = 0; i < total_submissions; ++i) {
    std::string state = i < graded_submissions
                            ? "RETURNED"
                            : (i < turned_in_submissions ? "TURNED_IN" : "NEW");
    std::string grade = i < graded_submissions ? R"(,"assignedGrade": 20)" : "";
    submissions.push_back(base::StringPrintf(
        kTemplate, i, course_work_id.c_str(), state.c_str(), grade.c_str()));
  }
  std::string submissions_string = base::JoinString(submissions, ",");
  return base::StringPrintf(R"({"studentSubmissions": [%s]})",
                            submissions_string.c_str());
}

}  // namespace

class GlanceablesClassroomClientImplIsDisabledByAdminTest
    : public testing::Test {
 public:
  GlanceablesClassroomClientImplIsDisabledByAdminTest()
      : profile_manager_(
            TestingProfileManager(TestingBrowserProcess::GetGlobal())) {}

  void SetUp() override { ASSERT_TRUE(profile_manager_.SetUp()); }

  std::unique_ptr<sync_preferences::TestingPrefServiceSyncable>
  GetDefaultPrefs() const {
    auto prefs =
        std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
    RegisterUserProfilePrefs(prefs->registry());
    return prefs;
  }

  TestingProfile* CreateTestingProfile(
      std::unique_ptr<sync_preferences::TestingPrefServiceSyncable> prefs) {
    return profile_manager_.CreateTestingProfile(
        "[email protected]", std::move(prefs), u"User Name", /*avatar_id=*/0,
        TestingProfile::TestingFactories());
  }

  GlanceablesClassroomClientImpl CreateClientForProfile(
      Profile* profile) const {
    return GlanceablesClassroomClientImpl(
        profile, base::DefaultClock::GetInstance(),
        base::BindLambdaForTesting(
            [](const std::vector<std::string>& scopes,
               const net::NetworkTrafficAnnotationTag& traffic_annotation_tag)
                -> std::unique_ptr<google_apis::RequestSender> {
              return nullptr;
            }));
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
  TestingProfileManager profile_manager_;
};

TEST_F(GlanceablesClassroomClientImplIsDisabledByAdminTest, Default) {
  auto* const profile = CreateTestingProfile(GetDefaultPrefs());
  EXPECT_FALSE(CreateClientForProfile(profile).IsDisabledByAdmin());
}

TEST_F(GlanceablesClassroomClientImplIsDisabledByAdminTest,
       NoClassroomInContextualGoogleIntegrationsPref) {
  auto prefs = GetDefaultPrefs();
  base::Value::List enabled_integrations;
  enabled_integrations.Append(prefs::kGoogleCalendarIntegrationName);
  enabled_integrations.Append(prefs::kGoogleTasksIntegrationName);
  prefs->SetList(prefs::kContextualGoogleIntegrationsConfiguration,
                 std::move(enabled_integrations));

  auto* const profile = CreateTestingProfile(std::move(prefs));
  EXPECT_TRUE(CreateClientForProfile(profile).IsDisabledByAdmin());
}

TEST_F(GlanceablesClassroomClientImplIsDisabledByAdminTest,
       DisabledClassroomApp) {
  auto* const profile = CreateTestingProfile(GetDefaultPrefs());

  std::vector<apps::AppPtr> app_deltas;
  app_deltas.push_back(apps::AppPublisher::MakeApp(
      apps::AppType::kWeb, web_app::kGoogleClassroomAppId,
      apps::Readiness::kDisabledByPolicy, "Classroom",
      apps::InstallReason::kUser, apps::InstallSource::kBrowser));

  apps::AppServiceProxyFactory::GetForProfile(profile)->OnApps(
      std::move(app_deltas), apps::AppType::kWeb,
      /*should_notify_initialized=*/true);

  EXPECT_TRUE(CreateClientForProfile(profile).IsDisabledByAdmin());
}

TEST_F(GlanceablesClassroomClientImplIsDisabledByAdminTest,
       BlockedClassroomUrl) {
  auto prefs = GetDefaultPrefs();
  base::Value::List blocklist;
  blocklist.Append("classroom.google.com");
  prefs->SetManagedPref(policy::policy_prefs::kUrlBlocklist,
                        std::move(blocklist));

  auto* const profile = CreateTestingProfile(std::move(prefs));
  EXPECT_TRUE(CreateClientForProfile(profile).IsDisabledByAdmin());
}

class GlanceablesClassroomClientImplTest : public testing::Test {
 public:
  GlanceablesClassroomClientImplTest()
      : profile_manager_(
            TestingProfileManager(TestingBrowserProcess::GetGlobal())) {}

  void SetUp() override {
    ASSERT_TRUE(profile_manager_.SetUp());

    // This is the time most of the test expect.
    OverrideTime("10 Apr 2023 00:00 GMT");

    auto create_request_sender_callback = base::BindLambdaForTesting(
        [&](const std::vector<std::string>& scopes,
            const net::NetworkTrafficAnnotationTag& traffic_annotation_tag) {
          return std::make_unique<google_apis::RequestSender>(
              std::make_unique<google_apis::DummyAuthService>(),
              url_loader_factory_, task_environment_.GetMainThreadTaskRunner(),
              "test-user-agent", TRAFFIC_ANNOTATION_FOR_TESTS);
        });
    client_ = std::make_unique<GlanceablesClassroomClientImpl>(
        profile_manager_.CreateTestingProfile("[email protected]",
                                              /*is_main_profile=*/true,
                                              url_loader_factory_),
        &test_clock_, create_request_sender_callback);

    test_server_.RegisterRequestHandler(
        base::BindRepeating(&TestRequestHandler::HandleRequest,
                            base::Unretained(&request_handler_)));
    ASSERT_TRUE(test_server_.Start());

    gaia_urls_overrider_ = std::make_unique<GaiaUrlsOverriderForTesting>(
        base::CommandLine::ForCurrentProcess(), "classroom_api_origin_url",
        test_server_.base_url().spec());
    ASSERT_EQ(GaiaUrls::GetInstance()->classroom_api_origin_url(),
              test_server_.base_url().spec());
  }

  void OverrideTime(const char* now_string) {
    base::Time new_now;
    ASSERT_TRUE(base::Time::FromString(now_string, &new_now));
    test_clock_.SetNow(new_now);
  }

  void ExpectActiveCourse(int call_count = 1) {
    EXPECT_CALL(request_handler(),
                HandleRequest(
                    Field(&HttpRequest::relative_url, HasSubstr("/courses?"))))
        .Times(call_count)
        .WillRepeatedly(Invoke([](const HttpRequest&) {
          return TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {
                  "id": "course-id-1",
                  "name": "Active Course 1",
                  "courseState": "ACTIVE"
                }
              ]
            })");
        }));
  }

  base::SimpleTestClock* clock() { return &test_clock_; }
  GlanceablesClassroomClientImpl* client() { return client_.get(); }
  base::HistogramTester* histogram_tester() { return &histogram_tester_; }
  TestRequestHandler& request_handler() { return request_handler_; }

 private:
  base::SimpleTestClock test_clock_;

  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::IO};
  TestingProfileManager profile_manager_;
  net::EmbeddedTestServer test_server_;
  scoped_refptr<network::TestSharedURLLoaderFactory> url_loader_factory_ =
      base::MakeRefCounted<network::TestSharedURLLoaderFactory>(
          /*network_service=*/nullptr,
          /*is_trusted=*/true);
  std::unique_ptr<GaiaUrlsOverriderForTesting> gaia_urls_overrider_;
  testing::StrictMock<TestRequestHandler> request_handler_;
  std::unique_ptr<GlanceablesClassroomClientImpl> client_;
  base::HistogramTester histogram_tester_;
};

// ----------------------------------------------------------------------------
// Fetch all courses:

// Fetches and makes sure only "ACTIVE" courses are converted to
// `GlanceablesClassroomCourse`.
TEST_F(GlanceablesClassroomClientImplTest, FetchCourses) {
  EXPECT_CALL(request_handler(), HandleRequest(Field(&HttpRequest::relative_url,
                                                     HasSubstr("/courses?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {
                  "id": "course-id-1",
                  "name": "Active Course 1",
                  "courseState": "ACTIVE"
                },
                {
                  "id": "course-id-2",
                  "name": "??? Course 2",
                  "courseState": "???"
                }
              ]
            })"))));

  base::RunLoop run_loop;
  client()->FetchStudentCourses(base::BindLambdaForTesting(
      [&](bool success,
          const GlanceablesClassroomClientImpl::CourseList& courses) {
        run_loop.Quit();

        EXPECT_TRUE(success);
        ASSERT_EQ(courses.size(), 1u);

        EXPECT_EQ(courses.at(0)->id, "course-id-1");
        EXPECT_EQ(courses.at(0)->name, "Active Course 1");

        histogram_tester()->ExpectTotalCount(
            "Ash.Glanceables.Api.Classroom.GetCourses.Latency",
            /*expected_count=*/1);
        histogram_tester()->ExpectUniqueSample(
            "Ash.Glanceables.Api.Classroom.GetCourses.Status",
            ApiErrorCode::HTTP_SUCCESS,
            /*expected_bucket_count=*/1);
        histogram_tester()->ExpectUniqueSample(
            "Ash.Glanceables.Api.Classroom.StudentCoursesCount",
            /*sample=*/1,
            /*expected_bucket_count=*/1);
      }));
  run_loop.Run();
}

TEST_F(GlanceablesClassroomClientImplTest, FetchCoursesOnHttpError) {
  EXPECT_CALL(request_handler(), HandleRequest(_))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  base::RunLoop run_loop;
  client()->FetchStudentCourses(base::BindLambdaForTesting(
      [&](bool success,
          const GlanceablesClassroomClientImpl::CourseList& courses) {
        run_loop.Quit();

        EXPECT_FALSE(success);
        EXPECT_EQ(0u, courses.size());

        histogram_tester()->ExpectTotalCount(
            "Ash.Glanceables.Api.Classroom.GetCourses.Latency",
            /*expected_count=*/1);
        histogram_tester()->ExpectUniqueSample(
            "Ash.Glanceables.Api.Classroom.GetCourses.Status",
            ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
            /*expected_bucket_count=*/1);
      }));
  run_loop.Run();
}

TEST_F(GlanceablesClassroomClientImplTest, FetchCoursesMultiplePages) {
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(
                  &HttpRequest::relative_url,
                  AllOf(HasSubstr("/courses?"), Not(HasSubstr("pageToken"))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {"id": "course-id-from-page-1", "courseState": "ACTIVE"}
              ],
              "nextPageToken": "page-2-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courses?"),
                                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {"id": "course-id-from-page-2", "courseState": "ACTIVE"}
              ],
              "nextPageToken": "page-3-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courses?"),
                                        HasSubstr("pageToken=page-3-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {"id": "course-id-from-page-3", "courseState": "ACTIVE"}
              ]
            })"))));

  base::RunLoop run_loop;
  client()->FetchStudentCourses(base::BindLambdaForTesting(
      [&](bool success,
          const GlanceablesClassroomClientImpl::CourseList& courses) {
        run_loop.Quit();
        EXPECT_TRUE(success);
        ASSERT_EQ(courses.size(), 3u);

        EXPECT_EQ(courses.at(0)->id, "course-id-from-page-1");
        EXPECT_EQ(courses.at(1)->id, "course-id-from-page-2");
        EXPECT_EQ(courses.at(2)->id, "course-id-from-page-3");

        histogram_tester()->ExpectUniqueSample(
            "Ash.Glanceables.Api.Classroom.StudentCoursesCount",
            /*sample=*/3,
            /*expected_bucket_count=*/1);
      }));
  run_loop.Run();
}

// ----------------------------------------------------------------------------
// Fetch all course work:

// Fetches and makes sure only "PUBLISHED" course work items are converted to
// `GlanceablesClassroomCourseWorkItem`.
TEST_F(GlanceablesClassroomClientImplTest, FetchCourseWork) {
  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1"
                },
                {
                  "id": "course-work-item-2",
                  "title": "Math multiple choice question",
                  "state": "DRAFT",
                  "alternateLink": "https://classroom.google.com/test-link-2"
                },
                {
                  "id": "course-work-item-3",
                  "title": "Math assignment with due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kStudent;
  client()->FetchCourseWork(
      /*course_id=*/"course-123", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            ASSERT_TRUE(courses_map.contains("course-123"));
            auto& course_work_map = courses_map["course-123"];
            ASSERT_EQ(course_work_map.size(), 2u);

            ASSERT_TRUE(course_work_map.contains("course-work-item-1"));
            const GlanceablesClassroomCourseWorkItem& course_work_1 =
                course_work_map.at("course-work-item-1");
            EXPECT_EQ(course_work_1.title(), "Math assignment");
            EXPECT_EQ(course_work_1.link(),
                      "https://classroom.google.com/test-link-1");
            EXPECT_FALSE(course_work_1.due());

            ASSERT_TRUE(course_work_map.contains("course-work-item-3"));
            const GlanceablesClassroomCourseWorkItem& course_work_3 =
                course_work_map.at("course-work-item-3");
            EXPECT_EQ(course_work_3.title(), "Math assignment with due date");
            EXPECT_EQ(course_work_3.link(),
                      "https://classroom.google.com/test-link-3");
            ASSERT_TRUE(course_work_3.due());
            EXPECT_EQ(FormatTimeAsString(course_work_3.due().value()),
                      "2023-04-25T15:09:25.250Z");

            histogram_tester()->ExpectTotalCount(
                "Ash.Glanceables.Api.Classroom.GetCourseWork.Latency",
                /*expected_count=*/1);
            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetCourseWork.Status",
                ApiErrorCode::HTTP_SUCCESS,
                /*expected_bucket_count=*/1);
            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetCourseWork.PagesCount",
                /*sample=*/1,
                /*expected_bucket_count=*/1);
          }));
  run_loop.Run();
}

// Fetches and makes sure only "PUBLISHED" course work items are converted to
// `GlanceablesClassroomCourseWorkItem`.
TEST_F(GlanceablesClassroomClientImplTest, FetchCourseWorkAndSubmissions) {
  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1"
                },
                {
                  "id": "course-work-item-2",
                  "title": "Math multiple choice question",
                  "state": "DRAFT",
                  "alternateLink": "https://classroom.google.com/test-link-2"
                },
                {
                  "id": "course-work-item-3",
                  "title": "Math assignment with due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-4",
                  "title": "Math assignment with no submissions",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-4"
                }
              ]
            })"))));

  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(
          &HttpRequest::relative_url,
          HasSubstr("courseWork/course-work-item-1/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                }
              ]
            })"))));
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(
          &HttpRequest::relative_url,
          HasSubstr("courseWork/course-work-item-2/studentSubmissions?"))))
      .Times(0);

  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(
          &HttpRequest::relative_url,
          HasSubstr("courseWork/course-work-item-3/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-3",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-3",
                  "state": "TURNED_IN"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "RETURNED",
                  "assignedGrade": 90
                }
              ]
            })"))));
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(
          &HttpRequest::relative_url,
          HasSubstr("courseWork/course-work-item-4/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": []
            })"))));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kTeacher;
  client()->FetchCourseWork(
      /*course_id=*/"course-123", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            ASSERT_TRUE(courses_map.contains("course-123"));
            auto& course_work_map = courses_map["course-123"];
            ASSERT_EQ(course_work_map.size(), 3u);

            ASSERT_TRUE(course_work_map.contains("course-work-item-1"));
            const GlanceablesClassroomCourseWorkItem& course_work_1 =
                course_work_map.at("course-work-item-1");
            EXPECT_EQ(course_work_1.title(), "Math assignment");
            EXPECT_EQ(course_work_1.link(),
                      "https://classroom.google.com/test-link-1");
            EXPECT_FALSE(course_work_1.due());
            EXPECT_EQ(course_work_1.total_submissions(), 1);
            EXPECT_EQ(course_work_1.turned_in_submissions(), 0);
            EXPECT_EQ(course_work_1.graded_submissions(), 0);

            ASSERT_TRUE(course_work_map.contains("course-work-item-3"));
            const GlanceablesClassroomCourseWorkItem& course_work_3 =
                course_work_map.at("course-work-item-3");
            EXPECT_EQ(course_work_3.title(), "Math assignment with due date");
            EXPECT_EQ(course_work_3.link(),
                      "https://classroom.google.com/test-link-3");
            ASSERT_TRUE(course_work_3.due());
            EXPECT_EQ(FormatTimeAsString(course_work_3.due().value()),
                      "2023-04-25T15:09:25.250Z");
            EXPECT_EQ(course_work_3.total_submissions(), 3);
            EXPECT_EQ(course_work_3.turned_in_submissions(), 2);
            EXPECT_EQ(course_work_3.graded_submissions(), 1);

            ASSERT_TRUE(course_work_map.contains("course-work-item-4"));
            const GlanceablesClassroomCourseWorkItem& course_work_4 =
                course_work_map.at("course-work-item-4");
            EXPECT_EQ(course_work_4.title(),
                      "Math assignment with no submissions");
            EXPECT_EQ(course_work_4.link(),
                      "https://classroom.google.com/test-link-4");
            EXPECT_FALSE(course_work_4.due());
            EXPECT_EQ(course_work_4.total_submissions(), 0);
            EXPECT_EQ(course_work_4.turned_in_submissions(), 0);
            EXPECT_EQ(course_work_4.graded_submissions(), 0);
          }));
  run_loop.Run();
}

TEST_F(GlanceablesClassroomClientImplTest, FetchCourseWorkOnHttpError) {
  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kStudent;
  client()->FetchCourseWork(
      /*course_id=*/"course-123", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            auto& course_work_map = courses_map["course-123"];
            ASSERT_TRUE(course_work_map.empty());

            histogram_tester()->ExpectTotalCount(
                "Ash.Glanceables.Api.Classroom.GetCourseWork.Latency",
                /*expected_count=*/1);
            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetCourseWork.Status",
                ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
                /*expected_bucket_count=*/1);
          }));
  run_loop.Run();
}

TEST_F(GlanceablesClassroomClientImplTest,
       FetchCourseWorkAndSubmissionsMultiplePages) {
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        Not(HasSubstr("pageToken"))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {"id": "course-work-item-from-page-1", "state": "PUBLISHED"}
              ],
              "nextPageToken": "page-2-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {"id": "course-work-item-from-page-2", "state": "PUBLISHED"}
              ],
              "nextPageToken": "page-3-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        HasSubstr("pageToken=page-3-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {"id": "course-work-item-from-page-3", "state": "PUBLISHED"}
              ]
            })"))));

  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(
          &HttpRequest::relative_url,
          HasSubstr(
              "courseWork/course-work-item-from-page-1/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-from-page-1",
                  "state": "NEW"
                }
              ]
            })"))));
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(
          &HttpRequest::relative_url,
          HasSubstr(
              "courseWork/course-work-item-from-page-2/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-from-page-2",
                  "state": "TURNED_IN"
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(
                  &HttpRequest::relative_url,
                  AllOf(HasSubstr("courseWork/course-work-item-from-page-3/"
                                  "studentSubmissions?"),
                        Not(HasSubstr("pageToken"))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-from-page-3",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-from-page-3",
                  "state": "TURNED_IN"
                }
              ],
              "nextPageToken": "page-2-token"
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(
                  &HttpRequest::relative_url,
                  AllOf(HasSubstr("courseWork/course-work-item-from-page-3/"
                                  "studentSubmissions?"),
                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-from-page-3",
                  "state": "RETURNED",
                  "assignedGrade": 10.0
                }
              ]
            })"))));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kTeacher;
  client()->FetchCourseWork(
      /*course_id=*/"course-123", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            ASSERT_TRUE(courses_map.contains("course-123"));
            auto& course_work_map = courses_map["course-123"];
            ASSERT_EQ(course_work_map.size(), 3u);

            ASSERT_TRUE(
                course_work_map.contains("course-work-item-from-page-1"));
            const GlanceablesClassroomCourseWorkItem& course_work_1 =
                course_work_map.at("course-work-item-from-page-1");
            EXPECT_EQ(course_work_1.total_submissions(), 1);
            EXPECT_EQ(course_work_1.turned_in_submissions(), 0);
            EXPECT_EQ(course_work_1.graded_submissions(), 0);

            ASSERT_TRUE(
                course_work_map.contains("course-work-item-from-page-2"));
            const GlanceablesClassroomCourseWorkItem& course_work_2 =
                course_work_map.at("course-work-item-from-page-2");
            EXPECT_EQ(course_work_2.total_submissions(), 1);
            EXPECT_EQ(course_work_2.turned_in_submissions(), 1);
            EXPECT_EQ(course_work_2.graded_submissions(), 0);

            ASSERT_TRUE(
                course_work_map.contains("course-work-item-from-page-3"));
            const GlanceablesClassroomCourseWorkItem& course_work_3 =
                course_work_map.at("course-work-item-from-page-3");
            EXPECT_EQ(course_work_3.total_submissions(), 3);
            EXPECT_EQ(course_work_3.turned_in_submissions(), 2);
            EXPECT_EQ(course_work_3.graded_submissions(), 1);

            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetCourseWork.PagesCount",
                /*sample=*/3,
                /*expected_bucket_count=*/1);
          }));
  run_loop.Run();
}

TEST_F(GlanceablesClassroomClientImplTest, FetchCourseWorkMultiplePages) {
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        Not(HasSubstr("pageToken"))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {"id": "course-work-item-from-page-1", "state": "PUBLISHED"}
              ],
              "nextPageToken": "page-2-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {"id": "course-work-item-from-page-2", "state": "PUBLISHED"}
              ],
              "nextPageToken": "page-3-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        HasSubstr("pageToken=page-3-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {"id": "course-work-item-from-page-3", "state": "PUBLISHED"}
              ]
            })"))));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kStudent;
  client()->FetchCourseWork(
      /*course_id=*/"course-123", course_work_type,
      base::BindLambdaForTesting([&]() {
        run_loop.Quit();

        GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
            client()->GetCourseWork(course_work_type);

        ASSERT_TRUE(courses_map.contains("course-123"));
        auto& course_work_map = courses_map["course-123"];

        ASSERT_EQ(course_work_map.size(), 3u);
        ASSERT_TRUE(course_work_map.contains("course-work-item-from-page-1"));
        ASSERT_TRUE(course_work_map.contains("course-work-item-from-page-2"));
        ASSERT_TRUE(course_work_map.contains("course-work-item-from-page-3"));
      }));
  run_loop.Run();
}

// ----------------------------------------------------------------------------
// Fetch all student submissions:

TEST_F(GlanceablesClassroomClientImplTest, FetchStudentSubmissions) {
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("courseWork/-/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-1",
                  "state": "CREATED"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-1",
                  "state": "RECLAIMED_BY_STUDENT"
                },
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-1",
                  "state": "TURNED_IN"
                },
                {
                  "id": "student-submission-5",
                  "courseWorkId": "course-work-1",
                  "state": "RETURNED"
                },
                {
                  "id": "student-submission-6",
                  "courseWorkId": "course-work-1",
                  "state": "RETURNED",
                  "assignedGrade": 50.0
                },
                {
                  "id": "student-submission-7",
                  "courseWorkId": "course-work-1",
                  "state": "???"
                }
              ]
            })"))));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kStudent;
  client()->FetchStudentSubmissions(
      /*course_id=*/"course-123", /*course_work_id=*/"-", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            ASSERT_TRUE(courses_map.contains("course-123"));
            auto& course_work_map = courses_map["course-123"];

            ASSERT_EQ(course_work_map.size(), 1u);
            ASSERT_TRUE(course_work_map.contains("course-work-1"));

            const auto& course_work = course_work_map.at("course-work-1");
            EXPECT_EQ(course_work.total_submissions(), 7);
            EXPECT_EQ(course_work.turned_in_submissions(), 2);
            EXPECT_EQ(course_work.graded_submissions(), 1);

            histogram_tester()->ExpectTotalCount(
                "Ash.Glanceables.Api.Classroom.GetStudentSubmissions.Latency",
                /*expected_count=*/1);
            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetStudentSubmissions.Status",
                ApiErrorCode::HTTP_SUCCESS,
                /*expected_bucket_count=*/1);
            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetStudentSubmissions."
                "PagesCount",
                /*sample=*/1,
                /*expected_bucket_count=*/1);
          }));
  run_loop.Run();
}

TEST_F(GlanceablesClassroomClientImplTest, FetchStudentSubmissionsOnHttpError) {
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kStudent;
  client()->FetchStudentSubmissions(
      /*course_id=*/"course-123", /*course_work_id=*/"-", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            auto& course_work_map = courses_map["course-123"];
            ASSERT_TRUE(course_work_map.empty());

            histogram_tester()->ExpectTotalCount(
                "Ash.Glanceables.Api.Classroom.GetStudentSubmissions.Latency",
                /*expected_count=*/1);
            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetStudentSubmissions.Status",
                ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
                /*expected_bucket_count=*/1);
          }));
  run_loop.Run();
}

TEST_F(GlanceablesClassroomClientImplTest,
       FetchStudentSubmissionsMultiplePages) {
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/studentSubmissions?"),
                                        Not(HasSubstr("pageToken"))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-from-page-1",
                  "courseWorkId" : "courseWork1"
                }
              ],
              "nextPageToken": "page-2-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/studentSubmissions?"),
                                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-from-page-2",
                  "courseWorkId": "courseWork1"
                }
              ],
              "nextPageToken": "page-3-token"
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/studentSubmissions?"),
                                        HasSubstr("pageToken=page-3-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-from-page-3",
                  "courseWorkId": "courseWork2"
                }
              ]
            })"))));

  base::RunLoop run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kStudent;
  client()->FetchStudentSubmissions(
      /*course_id=*/"course-123", /*course_work_id=*/"-", course_work_type,
      base::BindLambdaForTesting([&]() {
        run_loop.Quit();

        GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
            client()->GetCourseWork(course_work_type);

        ASSERT_TRUE(courses_map.contains("course-123"));
        auto& course_work_map = courses_map["course-123"];

        ASSERT_EQ(course_work_map.size(), 2u);

        ASSERT_TRUE(course_work_map.contains("courseWork1"));
        EXPECT_EQ(course_work_map.at("courseWork1").total_submissions(), 2);
        EXPECT_EQ(course_work_map.at("courseWork1").turned_in_submissions(), 0);
        EXPECT_EQ(course_work_map.at("courseWork1").graded_submissions(), 0);

        ASSERT_TRUE(course_work_map.contains("courseWork2"));
        EXPECT_EQ(course_work_map.at("courseWork2").total_submissions(), 1);
        EXPECT_EQ(course_work_map.at("courseWork2").turned_in_submissions(), 0);
        EXPECT_EQ(course_work_map.at("courseWork2").graded_submissions(), 0);

        histogram_tester()->ExpectUniqueSample(
            "Ash.Glanceables.Api.Classroom.GetStudentSubmissions.PagesCount",
            /*sample=*/3,
            /*expected_bucket_count=*/1);
      }));
  run_loop.Run();
}

// Verifies that student submissions can be fetched before course work items,
// and that they don't get overwritten after fetching course work items.
TEST_F(GlanceablesClassroomClientImplTest,
       FetchCourseWorkAfterStudentSubmissions) {
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("courseWork/-/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-1",
                  "state": "CREATED"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-1",
                  "state": "RECLAIMED_BY_STUDENT"
                },
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-1",
                  "state": "TURNED_IN"
                },
                {
                  "id": "student-submission-5",
                  "courseWorkId": "course-work-item-1",
                  "state": "RETURNED"
                },
                {
                  "id": "student-submission-6",
                  "courseWorkId": "course-work-item-1",
                  "state": "RETURNED",
                  "assignedGrade": 50.0
                },
                {
                  "id": "student-submission-7",
                  "courseWorkId": "course-work-item-1",
                  "state": "???"
                }
              ]
            })"))));

  base::RunLoop student_submissions_run_loop;
  const auto course_work_type =
      GlanceablesClassroomClientImpl::CourseWorkType::kStudent;
  client()->FetchStudentSubmissions(
      /*course_id=*/"course-123", /*course_work_id=*/"-", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            student_submissions_run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            ASSERT_TRUE(courses_map.contains("course-123"));
            auto& course_work_map = courses_map["course-123"];

            ASSERT_EQ(course_work_map.size(), 1u);
            ASSERT_TRUE(course_work_map.contains("course-work-item-1"));

            const auto& course_work = course_work_map.at("course-work-item-1");
            EXPECT_EQ(course_work.total_submissions(), 7);
            EXPECT_EQ(course_work.turned_in_submissions(), 2);
            EXPECT_EQ(course_work.graded_submissions(), 1);

            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetStudentSubmissions.Status",
                ApiErrorCode::HTTP_SUCCESS,
                /*expected_bucket_count=*/1);
          }));
  student_submissions_run_loop.Run();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1"
                }
              ]
            })"))));

  base::RunLoop course_work_run_loop;
  client()->FetchCourseWork(
      /*course_id=*/"course-123", course_work_type,
      base::BindLambdaForTesting(
          [&]() {
            course_work_run_loop.Quit();

            GlanceablesClassroomClientImpl::CourseWorkPerCourse& courses_map =
                client()->GetCourseWork(course_work_type);

            ASSERT_TRUE(courses_map.contains("course-123"));
            auto& course_work_map = courses_map["course-123"];

            ASSERT_EQ(course_work_map.size(), 1u);

            ASSERT_TRUE(course_work_map.contains("course-work-item-1"));
            const GlanceablesClassroomCourseWorkItem& course_work =
                course_work_map.at("course-work-item-1");
            EXPECT_EQ(course_work.title(), "Math assignment");
            EXPECT_EQ(course_work.link(),
                      "https://classroom.google.com/test-link-1");
            EXPECT_FALSE(course_work.due());
            EXPECT_EQ(course_work.total_submissions(), 7);
            EXPECT_EQ(course_work.turned_in_submissions(), 2);
            EXPECT_EQ(course_work.graded_submissions(), 1);

            histogram_tester()->ExpectUniqueSample(
                "Ash.Glanceables.Api.Classroom.GetCourseWork.Status",
                ApiErrorCode::HTTP_SUCCESS,
                /*expected_bucket_count=*/1);
          }));
  course_work_run_loop.Run();
}

// ----------------------------------------------------------------------------
// Public interface, student assignments:

TEST_F(GlanceablesClassroomClientImplTest,
       StudentRoleIsActiveWithEnrolledCourses) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .Times(0);
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .Times(0);

  TestFuture<bool> future;
  client()->IsStudentRoleActive(future.GetCallback());

  const bool active = future.Get();
  ASSERT_TRUE(active);

  histogram_tester()->ExpectTotalCount(
      "Ash.Glanceables.Api.Classroom.GetCourses.Status",
      /*expected_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.IsStudentRoleActiveResult",
      /*sample=*/1,
      /*expected_bucket_count=*/1);
}

TEST_F(GlanceablesClassroomClientImplTest,
       StudentRoleIsInactiveWithoutEnrolledCourses) {
  EXPECT_CALL(request_handler(), HandleRequest(Field(&HttpRequest::relative_url,
                                                     HasSubstr("/courses?"))))
      .WillOnce(Return(ByMove(
          TestRequestHandler::CreateSuccessfulResponse(R"({"courses": []})"))));

  TestFuture<bool> future;
  client()->IsStudentRoleActive(future.GetCallback());

  const bool active = future.Get();
  ASSERT_FALSE(active);

  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.IsStudentRoleActiveResult",
      /*sample=*/0,
      /*expected_bucket_count=*/1);
}

TEST_F(GlanceablesClassroomClientImplTest, ReturnsCompletedStudentAssignments) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1"
                },
                {
                  "id": "course-work-item-2",
                  "title": "Math assignment - submission graded",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-2"
                },
                {
                  "id": "course-work-item-3",
                  "title": "Math assignment - submission turned in",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3"
                },
                {
                  "id": "deleted-course-work-item",
                  "title": "Math assignment - draft",
                  "state": "DELETED"
                },
                {
                  "id": "course-work-item-4",
                  "title": "Math assignment - submission graded two",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-4"
                }
              ]
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "updateTime": "2023-03-10T15:09:25.250Z",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "updateTime": "2023-03-10T15:09:25.250Z",
                  "state": "RETURNED",
                  "assignedGrade": 50.0
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "updateTime": "2023-04-05T15:09:25.250Z",
                  "state": "TURNED_IN"
                },
                {
                  "id": "student-submission-4",
                  "courseWorkId": "deleted-course-work-item",
                  "updateTime": "2023-03-25T15:09:25.250Z",
                  "state": "TURNED_IN"
                },
                {
                  "id": "student-submission-5",
                  "courseWorkId": "course-work-item-4",
                  "updateTime": "2023-03-25T15:09:25.250Z",
                  "state": "TURNED_IN"
                }
              ]
            })"))));

  AssignmentListFuture future;
  client()->GetCompletedStudentAssignments(future.GetCallback());

  const auto [success, assignments] = future.Take();
  EXPECT_TRUE(success);
  ASSERT_EQ(assignments.size(), 3u);

  EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(0)->course_work_title,
            "Math assignment - submission turned in");
  EXPECT_EQ(assignments.at(0)->link,
            "https://classroom.google.com/test-link-3");
  EXPECT_FALSE(assignments.at(0)->due);
  EXPECT_FALSE(assignments.at(0)->submissions_state);

  EXPECT_EQ(assignments.at(1)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(1)->course_work_title,
            "Math assignment - submission graded two");
  EXPECT_EQ(assignments.at(1)->link,
            "https://classroom.google.com/test-link-4");
  EXPECT_FALSE(assignments.at(1)->due);
  EXPECT_FALSE(assignments.at(1)->submissions_state);

  EXPECT_EQ(assignments.at(2)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(2)->course_work_title,
            "Math assignment - submission graded");
  EXPECT_EQ(assignments.at(2)->link,
            "https://classroom.google.com/test-link-2");
  EXPECT_FALSE(assignments.at(2)->due);
  EXPECT_FALSE(assignments.at(2)->submissions_state);

  histogram_tester()->ExpectTotalCount(
      "Ash.Glanceables.Api.Classroom.StudentDataFetchTime",
      /*expected_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.CourseWorkItemsPerStudentCourseCount",
      /*sample=*/4,
      /*expected_bucket_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.StudentSubmissionsPerStudentCourseCount",
      /*sample=*/4,
      /*expected_bucket_count=*/1);
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReturnsStudentAssignmentsWithApproachingDueDate) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment - missed due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-2",
                  "title": "Math assignment - approaching due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-2",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-3",
                  "title": "Math assignment - approaching due date, completed",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-4",
                  "title": "Math assignment - approaching due date two",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-4",
                  "dueDate": {"year": 2023, "month": 6, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-5",
                  "title": "Math assignment - approaching due date three",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-5",
                  "dueDate": {"year": 2023, "month": 5, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "RETURNED",
                  "assignedGrade": 50.0
                },
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-4",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-5",
                  "courseWorkId": "course-work-item-5",
                  "state": "NEW"
                }
              ]
            })"))));

  AssignmentListFuture future;
  client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

  const auto [success, assignments] = future.Take();
  EXPECT_TRUE(success);
  ASSERT_EQ(assignments.size(), 3u);

  EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(0)->course_work_title,
            "Math assignment - approaching due date");
  EXPECT_EQ(assignments.at(0)->link,
            "https://classroom.google.com/test-link-2");
  EXPECT_EQ(FormatTimeAsString(assignments.at(0)->due.value()),
            "2023-04-25T15:09:25.250Z");
  EXPECT_FALSE(assignments.at(0)->submissions_state);

  EXPECT_EQ(assignments.at(1)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(1)->course_work_title,
            "Math assignment - approaching due date three");
  EXPECT_EQ(assignments.at(1)->link,
            "https://classroom.google.com/test-link-5");
  EXPECT_EQ(FormatTimeAsString(assignments.at(1)->due.value()),
            "2023-05-25T15:09:25.250Z");
  EXPECT_FALSE(assignments.at(1)->submissions_state);

  EXPECT_EQ(assignments.at(2)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(2)->course_work_title,
            "Math assignment - approaching due date two");
  EXPECT_EQ(assignments.at(2)->link,
            "https://classroom.google.com/test-link-4");
  EXPECT_EQ(FormatTimeAsString(assignments.at(2)->due.value()),
            "2023-06-25T15:09:25.250Z");
  EXPECT_FALSE(assignments.at(2)->submissions_state);

  histogram_tester()->ExpectTotalCount(
      "Ash.Glanceables.Api.Classroom.StudentDataFetchTime",
      /*expected_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.CourseWorkItemsPerStudentCourseCount",
      /*sample=*/5,
      /*expected_bucket_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.StudentSubmissionsPerStudentCourseCount",
      /*sample=*/5,
      /*expected_bucket_count=*/1);
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReturnsStudentAssignmentsWithMissedDueDate) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment - missed due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 3, "day": 20},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-2",
                  "title": "Math assignment - approaching due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-2",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-3",
                  "title": "Math assignment - missed due date, completed",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-4",
                  "title": "Math assignment - missed due date, turned in",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-4",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-5",
                  "title": "Math assignment - missed due date two",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-5",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-6",
                  "title": "Math assignment - missed due date three",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-6",
                  "dueDate": {"year": 2023, "month": 3, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "RETURNED",
                  "assignedGrade": 50.0
                },
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-4",
                  "state": "TURNED_IN"
                },
                {
                  "id": "student-submission-5",
                  "courseWorkId": "course-work-item-5",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-6",
                  "courseWorkId": "course-work-item-6",
                  "state": "NEW"
                }
              ]
            })"))));

  AssignmentListFuture future;
  client()->GetStudentAssignmentsWithMissedDueDate(future.GetCallback());

  const auto [success, assignments] = future.Take();
  EXPECT_TRUE(success);
  ASSERT_EQ(assignments.size(), 3u);

  EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(0)->course_work_title,
            "Math assignment - missed due date two");
  EXPECT_EQ(assignments.at(0)->link,
            "https://classroom.google.com/test-link-5");
  EXPECT_EQ(FormatTimeAsString(assignments.at(0)->due.value()),
            "2023-04-05T15:09:25.250Z");
  EXPECT_FALSE(assignments.at(0)->submissions_state);

  EXPECT_EQ(assignments.at(1)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(1)->course_work_title,
            "Math assignment - missed due date three");
  EXPECT_EQ(assignments.at(1)->link,
            "https://classroom.google.com/test-link-6");
  EXPECT_EQ(FormatTimeAsString(assignments.at(1)->due.value()),
            "2023-03-25T15:09:25.250Z");
  EXPECT_FALSE(assignments.at(1)->submissions_state);

  EXPECT_EQ(assignments.at(2)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(2)->course_work_title,
            "Math assignment - missed due date");
  EXPECT_EQ(assignments.at(2)->link,
            "https://classroom.google.com/test-link-1");
  EXPECT_EQ(FormatTimeAsString(assignments.at(2)->due.value()),
            "2023-03-20T15:09:25.250Z");
  EXPECT_FALSE(assignments.at(2)->submissions_state);

  histogram_tester()->ExpectTotalCount(
      "Ash.Glanceables.Api.Classroom.StudentDataFetchTime",
      /*expected_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.CourseWorkItemsPerStudentCourseCount",
      /*sample=*/6,
      /*expected_bucket_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.StudentSubmissionsPerStudentCourseCount",
      /*sample=*/6,
      /*expected_bucket_count=*/1);
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReturnsStudentAssignmentsWithoutDueDate) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "creationTime": "2023-03-10T15:09:25.250Z"
                },
                {
                  "id": "course-work-item-2",
                  "title": "Math assignment - with due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-2",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-3",
                  "title": "Math assignment - submission graded",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3"
                },
                {
                  "id": "course-work-item-4",
                  "title": "Math assignment one",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-4",
                  "creationTime": "2023-03-20T15:09:25.250Z"
                },
                {
                  "id": "course-work-item-5",
                  "title": "Math assignment two",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-5",
                  "creationTime": "2023-03-15T15:09:25.250Z"
                }
              ]
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "RETURNED",
                  "assignedGrade": 50.0
                },
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-4",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-5",
                  "courseWorkId": "course-work-item-5",
                  "state": "NEW"
                }
              ]
            })"))));

  AssignmentListFuture future;
  client()->GetStudentAssignmentsWithoutDueDate(future.GetCallback());

  const auto [success, assignments] = future.Take();
  EXPECT_TRUE(success);
  ASSERT_EQ(assignments.size(), 3u);

  EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(0)->course_work_title, "Math assignment one");
  EXPECT_EQ(assignments.at(0)->link,
            "https://classroom.google.com/test-link-4");
  EXPECT_FALSE(assignments.at(0)->due);
  EXPECT_FALSE(assignments.at(0)->submissions_state);

  EXPECT_EQ(assignments.at(1)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(1)->course_work_title, "Math assignment two");
  EXPECT_EQ(assignments.at(1)->link,
            "https://classroom.google.com/test-link-5");
  EXPECT_FALSE(assignments.at(1)->due);
  EXPECT_FALSE(assignments.at(1)->submissions_state);

  EXPECT_EQ(assignments.at(2)->course_title, "Active Course 1");
  EXPECT_EQ(assignments.at(2)->course_work_title, "Math assignment");
  EXPECT_EQ(assignments.at(2)->link,
            "https://classroom.google.com/test-link-1");
  EXPECT_FALSE(assignments.at(2)->due);
  EXPECT_FALSE(assignments.at(2)->submissions_state);

  histogram_tester()->ExpectTotalCount(
      "Ash.Glanceables.Api.Classroom.StudentDataFetchTime",
      /*expected_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.CourseWorkItemsPerStudentCourseCount",
      /*sample=*/5,
      /*expected_bucket_count=*/1);
  histogram_tester()->ExpectUniqueSample(
      "Ash.Glanceables.Api.Classroom.StudentSubmissionsPerStudentCourseCount",
      /*sample=*/5,
      /*expected_bucket_count=*/1);
}

TEST_F(GlanceablesClassroomClientImplTest,
       CourseWorkFailureWhenFetchingStudentAssignments) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "RETURNED",
                  "assignedGrade": 50.0
                }
              ]
            })"))));

  AssignmentListFuture future;
  client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

  const auto [success, assignments] = future.Take();
  EXPECT_FALSE(success);
  EXPECT_TRUE(assignments.empty());
}

TEST_F(GlanceablesClassroomClientImplTest,
       SubmissionsFailureWhenFetchingStudentAssignments) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment - missed due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-2",
                  "title": "Math assignment - approaching due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-2",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-3",
                  "title": "Math assignment - approaching due date, completed",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  AssignmentListFuture future;
  client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

  const auto [success, assignments] = future.Take();
  EXPECT_FALSE(success);
  EXPECT_TRUE(assignments.empty());
}

TEST_F(GlanceablesClassroomClientImplTest,
       RefetchStudentAssignmentsAfterReshowingBubble) {
  ExpectActiveCourse();

  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("/courses/course-id-1/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "student-course-work-item-1",
                  "title": "Math assignment - missed due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "student-course-work-item-1",
                  "title": "Math assignment - missed due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "student-course-work-item-2",
                  "title": "Math assignment - approaching due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-2",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("courseWork/-/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "student-course-work-item-1",
                  "state": "NEW"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "student-course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "student-course-work-item-2",
                  "state": "NEW"
                }
              ]
            })"))));

  // The student has one assignment with missed due date.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithMissedDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);
    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - missed due date");
  }

  // Initially, there are no assignments with approaching due date.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 0u);
  }

  // Simulate glanceables bubble closure.
  client()->OnGlanceablesBubbleClosed();

  // The response from requests sent after the bubble was closed contains an
  // assignment with approaching due date.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);
    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - approaching due date");
  }

  // No change in assignments with missed due date.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithMissedDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);
    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - missed due date");
  }

  // Simulate another request, to verify that coursework is not refetched if the
  // bubble does not close.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);
    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - approaching due date");
  }
}

TEST_F(GlanceablesClassroomClientImplTest,
       FetchStudentCoursesAfterIsActiveCheck) {
  ExpectActiveCourse();

  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("/courses/course-id-1/courseWork?"))))
      .Times(2)
      .WillRepeatedly(Invoke([](const HttpRequest&) {
        return TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "student-course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })");
      }));
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("courseWork/-/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
          CreateSubmissionsListResponse("student-course-work-item-1", 1, 0,
                                        0)))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
          CreateSubmissionsListResponse("student-course-work-item-1", 1, 1,
                                        0)))));

  TestFuture<bool> is_active_future;
  client()->IsStudentRoleActive(is_active_future.GetCallback());

  const bool active = is_active_future.Get();
  EXPECT_TRUE(active);

  // The student has one assignment with missed due date.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithMissedDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);
    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
  }

  // Simulate glanceables bubble closure.
  client()->OnGlanceablesBubbleClosed();

  {
    AssignmentListFuture future;
    client()->GetCompletedStudentAssignments(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);
    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
  }
}

TEST_F(GlanceablesClassroomClientImplTest,
       FetchStudentCoursesConcurrentlyWithIsActiveCheck) {
  ExpectActiveCourse();

  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("/courses/course-id-1/courseWork?"))))
      .Times(2)
      .WillRepeatedly(Invoke([](const HttpRequest&) {
        return TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "student-course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })");
      }));
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("courseWork/-/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
          CreateSubmissionsListResponse("student-course-work-item-1", 1, 0,
                                        0)))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
          CreateSubmissionsListResponse("student-course-work-item-1", 1, 1,
                                        0)))));

  TestFuture<bool> is_active_future;
  client()->IsStudentRoleActive(is_active_future.GetCallback());

  AssignmentListFuture assignments_future;
  client()->GetStudentAssignmentsWithMissedDueDate(
      assignments_future.GetCallback());

  const bool active = is_active_future.Get();
  EXPECT_TRUE(active);

  const auto [success, assignments] = assignments_future.Take();
  EXPECT_TRUE(success);
  ASSERT_EQ(assignments.size(), 1u);
  EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");

  // Simulate glanceables bubble closure.
  client()->OnGlanceablesBubbleClosed();

  client()->GetCompletedStudentAssignments(assignments_future.GetCallback());

  const auto [refetch_success, refetched_assignments] =
      assignments_future.Take();
  ASSERT_EQ(refetched_assignments.size(), 1u);
  EXPECT_EQ(refetched_assignments.at(0)->course_work_title, "Assignment 1");
}

TEST_F(GlanceablesClassroomClientImplTest,
       DontRefetchStudentAssignmentsIfBubbleReshownWhileStillFetching) {
  ExpectActiveCourse();

  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("/courses/course-id-1/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "student-course-work-item-1",
                  "title": "Math assignment - missed due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 5},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));
  EXPECT_CALL(
      request_handler(),
      HandleRequest(Field(&HttpRequest::relative_url,
                          HasSubstr("courseWork/-/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "student-course-work-item-1",
                  "state": "NEW"
                }
              ]
            })"))));

  AssignmentListFuture initial_future;
  client()->GetStudentAssignmentsWithMissedDueDate(
      initial_future.GetCallback());

  // Simulate glanceables bubble closure, and then another assignments request
  // before the first request completes.
  client()->OnGlanceablesBubbleClosed();

  AssignmentListFuture second_future;
  client()->GetStudentAssignmentsWithMissedDueDate(second_future.GetCallback());

  // Verify that both requests return the same result.
  const auto [initial_success, initial_assignments] = initial_future.Take();
  EXPECT_TRUE(initial_success);
  ASSERT_EQ(initial_assignments.size(), 1u);
  EXPECT_EQ(initial_assignments.at(0)->course_title, "Active Course 1");
  EXPECT_EQ(initial_assignments.at(0)->course_work_title,
            "Math assignment - missed due date");

  const auto [second_success, second_assignments] = second_future.Take();
  EXPECT_TRUE(second_success);
  ASSERT_EQ(second_assignments.size(), 1u);
  EXPECT_EQ(second_assignments.at(0)->course_title, "Active Course 1");
  EXPECT_EQ(second_assignments.at(0)->course_work_title,
            "Math assignment - missed due date");

  // Getting assignments after initial results have been received does not
  // repeat course work data fetch.
  AssignmentListFuture third_future;
  client()->GetStudentAssignmentsWithMissedDueDate(third_future.GetCallback());

  const auto [third_success, third_assignments] = third_future.Take();
  EXPECT_TRUE(third_success);
  ASSERT_EQ(third_assignments.size(), 1u);
  EXPECT_EQ(third_assignments.at(0)->course_title, "Active Course 1");
  EXPECT_EQ(third_assignments.at(0)->course_work_title,
            "Math assignment - missed due date");
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReusePreviousStudentDataOnCourseFetchError) {
  EXPECT_CALL(request_handler(), HandleRequest(Field(&HttpRequest::relative_url,
                                                     HasSubstr("/courses?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {
                  "id": "course-id-1",
                  "name": "Active Course 1",
                  "courseState": "ACTIVE"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {
                  "id": "course-id-2",
                  "name": "Active Course 2",
                  "courseState": "ACTIVE"
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("course-id-1/courseWork?"))))
      .Times(2)
      .WillRepeatedly(Invoke([](const HttpRequest&) {
        return TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment - approaching due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })");
      }));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("course-id-2/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Final assignment",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .Times(3)
      .WillRepeatedly(Invoke([](const HttpRequest&) {
        return TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                }
              ]
            })");
      }));

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - approaching due date");
  }

  client()->OnGlanceablesBubbleClosed();

  {
    clock()->Advance(base::Hours(4));

    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_FALSE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - approaching due date");
  }

  // Make sure assignments can be refetched after a failure.
  {
    clock()->Advance(base::Hours(4));

    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 2");
    EXPECT_EQ(assignments.at(0)->course_work_title, "Final assignment");
  }
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReusePreviousStudentDataOnCourseSecondPageFetchError) {
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(
                  &HttpRequest::relative_url,
                  AllOf(HasSubstr("/courses?"), Not(HasSubstr("pageToken="))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {
                  "id": "course-id-1",
                  "name": "Active Course 1",
                  "courseState": "ACTIVE"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {
                  "id": "course-id-2",
                  "name": "Active Course 2",
                  "courseState": "ACTIVE"
                }
              ],
              "nextPageToken": "page-2-token"
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courses": [
                {
                  "id": "course-id-2",
                  "name": "Active Course 2",
                  "courseState": "ACTIVE"
                }
              ]
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courses?"),
                                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("course-id-1/courseWork?"))))
      .Times(2)
      .WillRepeatedly(Invoke([](const HttpRequest&) {
        return TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Math assignment - approaching due date",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })");
      }));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("course-id-2/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Final assignment",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));
  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("studentSubmissions?"))))
      .Times(3)
      .WillRepeatedly(Invoke([](const HttpRequest&) {
        return TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                }
              ]
            })");
      }));

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - approaching due date");
  }

  client()->OnGlanceablesBubbleClosed();

  {
    clock()->Advance(base::Hours(4));

    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_FALSE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 1");
    EXPECT_EQ(assignments.at(0)->course_work_title,
              "Math assignment - approaching due date");
  }

  // Make sure assignments can be refetched after a failure.
  {
    clock()->Advance(base::Hours(4));

    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_title, "Active Course 2");
    EXPECT_EQ(assignments.at(0)->course_work_title, "Final assignment");
  }
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReturnCachedDataIfCourseWorkFetchFailsForStudents) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-2",
                  "title": "Assignment 2",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 16,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-4",
                  "title": "Assignment 4",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 16,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "TURNED_IN"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "NEW"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-4",
                  "state": "NEW"
                }
              ]
            })"))));

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 2u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
    EXPECT_EQ(assignments.at(1)->course_work_title, "Assignment 2");
  }

  client()->OnGlanceablesBubbleClosed();

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_FALSE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
  }

  // Make sure assignments can be refetched after a failure.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 4");
  }
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReturnCachedDataIfCourseWorkSecondPageFetchFailsForStudents) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        Not(HasSubstr("pageToken="))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-2",
                  "title": "Assignment 2",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 16,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-3",
                  "title": "Assignment 3",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 17,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ],
              "nextPageToken": "page-2-token"
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-4",
                  "title": "Assignment 4",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/courseWork?"),
                                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "NEW"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-4",
                  "state": "NEW"
                }
              ]
            })"))));

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 2u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
    EXPECT_EQ(assignments.at(1)->course_work_title, "Assignment 2");
  }

  client()->OnGlanceablesBubbleClosed();

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_FALSE(success);
    ASSERT_EQ(assignments.size(), 2u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 2");
    EXPECT_EQ(assignments.at(1)->course_work_title, "Assignment 3");
  }

  // Make sure assignments can be refetched after a failure.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 4");
  }
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReturnCachedDataIfSubmissionsFetchFailsForStudents) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-2",
                  "title": "Assignment 2",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 16,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-3",
                  "title": "Assignment 3",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 17,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-4",
                  "title": "Assignment 4",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  HasSubstr("/studentSubmissions?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-4",
                  "state": "NEW"
                }
              ]
            })"))));

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 2u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
    EXPECT_EQ(assignments.at(1)->course_work_title, "Assignment 2");
  }

  client()->OnGlanceablesBubbleClosed();

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_FALSE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
  }

  // Make sure assignments can be refetched after a failure.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 1u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 4");
  }
}

TEST_F(GlanceablesClassroomClientImplTest,
       ReturnCachedDataIfSubmissionsSecondPageFetchFailsForStudents) {
  ExpectActiveCourse();

  EXPECT_CALL(request_handler(),
              HandleRequest(
                  Field(&HttpRequest::relative_url, HasSubstr("/courseWork?"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-2",
                  "title": "Assignment 2",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 16,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-1",
                  "title": "Assignment 1",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-3",
                  "title": "Assignment 3",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 17,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "courseWork": [
                {
                  "id": "course-work-item-3",
                  "title": "Assignment 3",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-1",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 15,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                },
                {
                  "id": "course-work-item-4",
                  "title": "Assignment 4",
                  "state": "PUBLISHED",
                  "alternateLink": "https://classroom.google.com/test-link-3",
                  "dueDate": {"year": 2023, "month": 4, "day": 25},
                  "dueTime": {
                    "hours": 17,
                    "minutes": 9,
                    "seconds": 25,
                    "nanos": 250000000
                  }
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/studentSubmissions?"),
                                        Not(HasSubstr("pageToken="))))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-1",
                  "courseWorkId": "course-work-item-1",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                }
              ]
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-2",
                  "courseWorkId": "course-work-item-2",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "NEW"
                }
              ],
              "nextPageToken": "page-2-token"
            })"))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
            {
              "studentSubmissions": [
                {
                  "id": "student-submission-3",
                  "courseWorkId": "course-work-item-3",
                  "state": "NEW"
                },
                {
                  "id": "student-submission-4",
                  "courseWorkId": "course-work-item-4",
                  "state": "NEW"
                }
              ]
            })"))));

  EXPECT_CALL(request_handler(),
              HandleRequest(Field(&HttpRequest::relative_url,
                                  AllOf(HasSubstr("/studentSubmissions?"),
                                        HasSubstr("pageToken=page-2-token")))))
      .WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 2u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
    EXPECT_EQ(assignments.at(1)->course_work_title, "Assignment 2");
  }

  client()->OnGlanceablesBubbleClosed();

  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_FALSE(success);
    ASSERT_EQ(assignments.size(), 2u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 1");
    EXPECT_EQ(assignments.at(1)->course_work_title, "Assignment 3");
  }

  // Make sure assignments can be refetched after a failure.
  {
    AssignmentListFuture future;
    client()->GetStudentAssignmentsWithApproachingDueDate(future.GetCallback());

    const auto [success, assignments] = future.Take();
    EXPECT_TRUE(success);
    ASSERT_EQ(assignments.size(), 2u);

    EXPECT_EQ(assignments.at(0)->course_work_title, "Assignment 3");
    EXPECT_EQ(assignments.at(1)->course_work_title, "Assignment 4");
  }
}

}  // namespace ash