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

#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_model.h"

#import "base/strings/sys_string_conversions.h"
#import "base/test/scoped_feature_list.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/test/fullscreen_model_test_util.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/test/test_fullscreen_model_observer.h"
#import "ios/chrome/browser/toolbar/legacy/ui_bundled/fullscreen/toolbars_size.h"
#import "ios/web/common/features.h"
#import "testing/platform_test.h"

namespace {
// The toolbar height to use for tests.
const CGFloat kToolbarHeight = 50.0;
// The scroll view height used for tests.
const CGFloat kScrollViewHeight = 400.0;
// The content height used for tests.
const CGFloat kContentHeight = 5000.0;
// Converts `insets` to a string for debugging.
std::string GetStringFromInsets(UIEdgeInsets insets) {
  return base::SysNSStringToUTF8(NSStringFromUIEdgeInsets(insets));
}
}  // namespace

// Test fixture for FullscreenModel.
class FullscreenModelTest : public PlatformTest {
 public:
  FullscreenModelTest() : PlatformTest() {
    model_->AddObserver(&observer_);
    // Set the toolbars height to kToolbarHeight, and simulate a page load that
    // finishes with a 0.0 y content offset.
    ToolbarsSize* toolbarsSize =
        [[ToolbarsSize alloc] initWithCollapsedTopToolbarHeight:0.0
                                       expandedTopToolbarHeight:kToolbarHeight
                                    expandedBottomToolbarHeight:kToolbarHeight
                                   collapsedBottomToolbarHeight:0.0];
    model_->SetToolbarsSize(toolbarsSize);
    model_->SetScrollViewHeight(kScrollViewHeight);
    model_->SetContentHeight(kContentHeight);
    model_->ResetForNavigation();
    model_->SetYContentOffset(0.0);
  }
  ~FullscreenModelTest() override { model_->RemoveObserver(&observer_); }

  FullscreenModel* model() { return model_.get(); }
  TestFullscreenModelObserver& observer() { return observer_; }
  ToolbarsSize* GetToolbarsSize() { return model()->toolbars_size_; }

 private:
  std::unique_ptr<FullscreenModel> model_ = std::make_unique<FullscreenModel>();
  TestFullscreenModelObserver observer_;
};

// Tests that incremending and decrementing the disabled counter correctly
// disabled/enables the model, and that the model state is updated correctly
// when disabled.
TEST_F(FullscreenModelTest, EnableDisable) {
  ASSERT_TRUE(model()->enabled());
  ASSERT_TRUE(observer().enabled());
  // Scroll in order to hide the Toolbar.
  SimulateFullscreenUserScrollWithDelta(model(), kToolbarHeight * 3);
  EXPECT_EQ(observer().progress(), 0.0);
  if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
    EXPECT_TRUE(model()->has_base_offset());
  }
  // Increment the disabled counter and check that the model is disabled.
  model()->IncrementDisabledCounter();
  EXPECT_FALSE(model()->enabled());
  EXPECT_FALSE(observer().enabled());
  // Since the model has been disabled the Toolbar is shown, verify that the
  // model state reflects that.
  EXPECT_EQ(observer().progress(), 1.0);
  EXPECT_EQ(model()->base_offset(),
            GetFullscreenBaseOffsetForProgress(model(), 1.0));
  // Increment again and check that the model is still disabled.
  model()->IncrementDisabledCounter();
  EXPECT_FALSE(model()->enabled());
  EXPECT_FALSE(observer().enabled());
  // Decrement the counter and check that the model is still disabled.
  model()->DecrementDisabledCounter();
  EXPECT_FALSE(model()->enabled());
  EXPECT_FALSE(observer().enabled());
  // Decrement again and check that the model is reenabled.
  model()->DecrementDisabledCounter();
  EXPECT_TRUE(model()->enabled());
  EXPECT_TRUE(observer().enabled());
}

// Tests that calling ResetForNavigation() resets the model to a fully-visible
// pre-scroll state.
TEST_F(FullscreenModelTest, ResetForNavigation) {
  // Simulate a scroll event and check that progress has been updated.
  SimulateFullscreenUserScrollForProgress(model(), 0.5);
  ASSERT_EQ(observer().progress(), 0.5);
  // Call ResetForNavigation() and verify that the base offset is reset and that
  // the toolbar is fully visible.
  model()->ResetForNavigation();
  if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
    EXPECT_FALSE(model()->has_base_offset());
  }
  EXPECT_EQ(observer().progress(), 1.0);
}

// Tests that ResetForNavigation() correctly resets manually forced fullscreen.
TEST_F(FullscreenModelTest, ResetForNavigationWithManuallyForced) {
  // Enable the feature.
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(kHideToolbarsInOverflowMenu);

  // Manually force fullscreen.
  model()->SetForceFullscreenMode(true);
  model()->set_manually_forced(true);
  model()->SetInsetsUpdateEnabled(false);
  model()->IncrementDisabledCounter();

  ASSERT_TRUE(model()->IsForceFullscreenMode());
  ASSERT_FALSE(model()->IsInsetsUpdateEnabled());
  ASSERT_FALSE(model()->enabled());

  // Call ResetForNavigation() and verify that the forced state is reset.
  model()->ResetForNavigation();

  EXPECT_FALSE(model()->IsForceFullscreenMode());
  EXPECT_TRUE(model()->IsInsetsUpdateEnabled());
  EXPECT_TRUE(model()->enabled());
}

// Tests that the progress value is not updated if the current scroll is being
// ignored.
TEST_F(FullscreenModelTest, IgnoreRemainderOfCurrentScroll) {
  ASSERT_EQ(model()->progress(), 1.0);
  // Simulate a scroll to a 0.0 progress value in two halves.
  const CGFloat kHalfProgress = 0.5;
  const CGFloat kHalfProgressDelta =
      GetFullscreenOffsetDeltaForProgress(model(), kHalfProgress);
  model()->SetScrollViewIsDragging(true);
  model()->SetScrollViewIsScrolling(true);
  model()->SetYContentOffset(model()->GetYContentOffset() + kHalfProgressDelta);
  model()->SetScrollViewIsDragging(false);
  ASSERT_EQ(model()->progress(), kHalfProgress);
  // Begin ignoring the scroll while the decelerating.
  model()->IgnoreRemainderOfCurrentScroll();
  model()->SetYContentOffset(model()->GetYContentOffset() + kHalfProgressDelta);
  model()->SetScrollViewIsScrolling(false);
  EXPECT_EQ(model()->progress(), kHalfProgress);
  // Simulate another scroll and verify that the model is no longer ignoring
  // from the previous call to IgnoreRemainderOfCurrentScroll().
  SimulateFullscreenUserScrollForProgress(model(), 1.0);
  ASSERT_EQ(model()->progress(), 1.0);
}

// Tests that the end progress value of a scroll adjustment animation is used
// as the model's progress.
TEST_F(FullscreenModelTest, AnimationEnded) {
  const CGFloat kAnimationEndProgress = 0.5;
  ASSERT_EQ(observer().progress(), 1.0);
  model()->AnimationEndedWithProgress(kAnimationEndProgress);
  // Check that the resulting progress value was not broadcast.
  EXPECT_EQ(observer().progress(), 1.0);
  // Start dragging to to simulate a touch that occurs while the scroll end
  // animation is in progress.  This would cancel the scroll animation and call
  // AnimationEndedWithProgress().  After this occurs, the base offset should be
  // updated to a value corresponding with a 0.5 progress value.
  model()->SetScrollViewIsDragging(true);
  EXPECT_EQ(
      GetFullscreenBaseOffsetForProgress(model(), kAnimationEndProgress),
      model()->GetYContentOffset() - kAnimationEndProgress * kToolbarHeight);
}

// Tests that changing the toolbar height fully shows the new toolbar and
// responds appropriately to content offset changes.
TEST_F(FullscreenModelTest, UpdateToolbarHeight) {
  // Reset the toolbar height and verify that the base offset is reset and that
  // the toolbar is fully visible.
  ToolbarsSize* ToolbarsSize = GetToolbarsSize();
  ToolbarsSize.expandedTopToolbarHeight = 2.0 * kToolbarHeight;
  model()->ToolbarsHeightDidChange();
  if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
    EXPECT_FALSE(model()->has_base_offset());
  }
  EXPECT_EQ(observer().progress(), 1.0);
  // Simulate a page load to a 0.0 y content offset.
  model()->ResetForNavigation();
  model()->SetYContentOffset(0.0);
  // Simulate a scroll to -kToolbarHeight.  Since toolbar_height() is twice
  // that, this should produce a progress value of 0.5.
  SimulateFullscreenUserScrollWithDelta(model(), kToolbarHeight);
  ASSERT_EQ(model()->GetYContentOffset(), kToolbarHeight);
  EXPECT_EQ(observer().progress(), 0.5);
}

// Tests that updating the y content offset produces the expected progress
// value.
TEST_F(FullscreenModelTest, UserScroll) {
  const CGFloat kFinalProgress = 0.5;
  SimulateFullscreenUserScrollForProgress(model(), kFinalProgress);
  EXPECT_EQ(observer().progress(), kFinalProgress);
  EXPECT_EQ(model()->GetYContentOffset(), kFinalProgress * kToolbarHeight);
}

// Tests that updating the y content offset of a disabled model only updates its
// base offset.
TEST_F(FullscreenModelTest, DisabledScroll) {
  const CGFloat kProgress = 0.5;
  model()->IncrementDisabledCounter();
  SimulateFullscreenUserScrollForProgress(model(), kProgress);
  EXPECT_EQ(observer().progress(), 1.0);
  EXPECT_EQ(model()->base_offset(),
            GetFullscreenBaseOffsetForProgress(model(), 1.0));
}

// Tests that updating the y content offset programmatically (i.e. while the
// scroll view is not scrolling) only updates the base offset.
TEST_F(FullscreenModelTest, ProgrammaticScroll) {
  // Perform a programmatic scroll that would result in a progress of 0.5, and
  // verify that the initial progress value of 1.0 is maintained.
  const CGFloat kProgress = 0.5;
  model()->SetYContentOffset(kProgress * kToolbarHeight);
  EXPECT_EQ(observer().progress(), 1.0);
  EXPECT_EQ(model()->base_offset(),
            GetFullscreenBaseOffsetForProgress(model(), 1.0));
}

// Tests that updating the y content offset while zooming only updates the
// model's base offset.
TEST_F(FullscreenModelTest, ZoomScroll) {
  const CGFloat kProgress = 0.5;
  model()->SetScrollViewIsZooming(true);
  SimulateFullscreenUserScrollForProgress(model(), kProgress);
  EXPECT_EQ(observer().progress(), 1.0);
  EXPECT_EQ(model()->base_offset(),
            GetFullscreenBaseOffsetForProgress(model(), 1.0));
}

// Tests that updating the y content offset while the toolbar height is 0 only
// updates the model's base offset.
TEST_F(FullscreenModelTest, NoToolbarScroll) {
  ToolbarsSize* ToolbarsSize = GetToolbarsSize();
  ToolbarsSize.expandedTopToolbarHeight = 0.0;
  model()->SetYContentOffset(100);
  EXPECT_EQ(observer().progress(), 1.0);
  EXPECT_EQ(model()->base_offset(), 100);
}

// Tests that setting scrolling to false sends a scroll end signal to its
// observers.
TEST_F(FullscreenModelTest, ScrollEnded) {
  model()->SetScrollViewIsScrolling(true);
  model()->SetScrollViewIsScrolling(false);
  EXPECT_TRUE(observer().scroll_end_received());
}

// Tests that the base offset is updated when dragging begins.
TEST_F(FullscreenModelTest, DraggingStarted) {
  model()->ResetForNavigation();
  model()->SetScrollViewIsDragging(true);
  if (base::FeatureList::IsEnabled(web::features::kSmoothScrollingDefault)) {
    EXPECT_TRUE(model()->has_base_offset());
  }
}

// Tests that toolbar_insets() returns the correct values.
TEST_F(FullscreenModelTest, ToolbarInsets) {
  // Checks whether `insets` are equal to the expected insets at `progress`.
  void (^check_insets)(UIEdgeInsets insets, CGFloat progress) =
      ^void(UIEdgeInsets insets, CGFloat progress) {
        UIEdgeInsets expected_insets = UIEdgeInsetsMake(
            progress * kToolbarHeight, 0, progress * kToolbarHeight, 0);
        EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(insets, expected_insets))
            << "Insets " << GetStringFromInsets(insets)
            << " not equal to expected insets "
            << GetStringFromInsets(expected_insets);
      };

  const CGFloat kFullyVisibleProgress = 1.0;
  check_insets(model()->max_toolbar_insets(), kFullyVisibleProgress);
  check_insets(model()->current_toolbar_insets(), kFullyVisibleProgress);
  const CGFloat kHalfProgress = 0.5;
  SimulateFullscreenUserScrollForProgress(model(), kHalfProgress);
  check_insets(model()->current_toolbar_insets(), kHalfProgress);
  const CGFloat kHiddenProgress = 0.0;
  SimulateFullscreenUserScrollForProgress(model(), kHiddenProgress);
  check_insets(model()->current_toolbar_insets(), kHiddenProgress);
  check_insets(model()->min_toolbar_insets(), kHiddenProgress);
}

// Tests that the model is disabled when the content height is less than the
// scroll view height.
TEST_F(FullscreenModelTest, DisableForShortContent) {
  ASSERT_TRUE(model()->enabled());
  // The model should be disabled when the rendered content height is less than
  // the height of the scroll view.
  model()->SetContentHeight(model()->GetScrollViewHeight());
  EXPECT_FALSE(model()->enabled());
  // Reset the height to kContentHeight and verify that the model is re-enabled.
  model()->SetContentHeight(model()->GetScrollViewHeight() +
                            2 * kToolbarHeight + 1.0);
  EXPECT_TRUE(model()->enabled());
}

// Tests that scrolling past the edge of the page content is ignored when the
// scroll view is being resized.
TEST_F(FullscreenModelTest, IgnoreScrollsPastBottomWhileResizing) {
  // Instruct the model to resize the scroll view and scroll to the bottom of
  // the page.
  model()->SetResizesScrollView(true);
  model()->SetYContentOffset(kContentHeight - kScrollViewHeight);
  // Try scrolling with a user gesture such that the toolars are hidden, then
  // verify that this scroll is ignored.
  SimulateFullscreenUserScrollForProgress(model(), 0.0);
  EXPECT_EQ(observer().progress(), 1.0);
}

// Tests that updates to the content height that would normally disable the
// model are ignored during the scroll, and that the model is correctly updated
// to be disabled upon the subsequent scroll.
TEST_F(FullscreenModelTest, IgnoreContentHeightChangesWhileScrolling) {
  ASSERT_TRUE(model()->enabled());
  // Simulate a re-render to a height that would disable the model during a
  // scroll.
  model()->SetScrollViewIsScrolling(true);
  model()->SetContentHeight(kScrollViewHeight / 2.0);
  model()->SetScrollViewIsScrolling(false);
  EXPECT_TRUE(model()->enabled());
  // Simulate the start of a subsequent scroll and verify that the model becomes
  // disabled for the short content height.
  model()->SetScrollViewIsDragging(true);
  EXPECT_FALSE(model()->enabled());
}

// Tests that the model detects when the page is scrolled to the top and bottom.
TEST_F(FullscreenModelTest, ScrolledToTopAndBottom) {
  // Scroll to the top of the page and verify that only is_scrolled_to_top()
  // returns true.
  model()->SetYContentOffset(-kToolbarHeight);
  EXPECT_TRUE(model()->is_scrolled_to_top());
  EXPECT_FALSE(model()->is_scrolled_to_bottom());

  // Scroll to the middle of the page and verify that neither
  // is_scrolled_to_top() nor is_scrolled_to_bottom() returns true.
  model()->SetYContentOffset(kContentHeight / 2.0);
  EXPECT_FALSE(model()->is_scrolled_to_top());
  EXPECT_FALSE(model()->is_scrolled_to_bottom());

  // Scroll to the bottom of the page and verify that only
  // is_scrolled_to_bottom() returns true.
  model()->SetYContentOffset(kContentHeight - kScrollViewHeight);
  EXPECT_FALSE(model()->is_scrolled_to_top());
  EXPECT_TRUE(model()->is_scrolled_to_bottom());
}

// Tests that after scrolling down a lot to enter fullscreen, scrolling
// partially up as part of the same drag event exits fullscreen.
TEST_F(FullscreenModelTest, ScrollDownThenUp) {
  model()->SetScrollViewIsDragging(true);
  model()->SetScrollViewIsScrolling(true);
  EXPECT_EQ(observer().progress(), 1.0);
  model()->SetYContentOffset(model()->GetYContentOffset() + kToolbarHeight * 3);
  EXPECT_EQ(observer().progress(), 0);
  // Simulate a direction change in the swipe.
  model()->SetYContentOffset(model()->GetYContentOffset() - 5);
  model()->SetYContentOffset(model()->GetYContentOffset() - kToolbarHeight);
  EXPECT_EQ(observer().progress(), 1.0);
  model()->SetScrollViewIsDragging(false);
  model()->SetScrollViewIsScrolling(false);
}
