// 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.

#import "ios/chrome/browser/sessions/model/session_restoration_web_state_observer.h"

#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"

// A Wrapper around a id<CRWWebViewScrollViewObserver>.
@interface SessionRestorationScrollObserverWrapper : NSObject
@property(nonatomic, strong) id<CRWWebViewScrollViewObserver> value;
@end

@implementation SessionRestorationScrollObserverWrapper
@end

namespace {

// Increments count when called.
void IncrementCounter(size_t* counter) {
  ++*counter;
}

// Creates a CRWWebViewProxy that captures the first observer registered
// to its CRWWebViewScrollViewProxy and store it into `wrapper`.
id MockWebViewProxy(SessionRestorationScrollObserverWrapper* wrapper) {
  // Create a mock CRWWebViewScrollViewProxy that captures the observer
  // and store it into `wrapper.value`. It also check that the observer
  // is correctly unregistered.
  id scroll_view_proxy = OCMClassMock([CRWWebViewScrollViewProxy class]);
  OCMStub([scroll_view_proxy addObserver:[OCMArg any]])
      .andDo(^(NSInvocation* invocation) {
        __unsafe_unretained id observer = nil;
        [invocation getArgument:&observer atIndex:2];
        wrapper.value = observer;
      });
  OCMStub([scroll_view_proxy
      removeObserver:[OCMArg checkWithBlock:^BOOL(id observer) {
        if (observer != wrapper.value) {
          return NO;
        }
        wrapper.value = nil;
        return YES;
      }]]);

  // Register the fake CRWWebViewScrollViewProxy.
  id proxy = OCMProtocolMock(@protocol(CRWWebViewProxy));
  OCMStub([proxy scrollViewProxy]).andReturn(scroll_view_proxy);
  return proxy;
}

}  // anonymous namespace

class SessionRestorationWebStateObserverTest : public PlatformTest {
 public:
  void SetUp() override {
    web_state_ = std::make_unique<web::FakeWebState>();
    web_state_->SetIsRealized(true);
    web_state_->SetWebFramesManager(
        std::make_unique<web::FakeWebFramesManager>());
    web_state_->SetNavigationManager(
        std::make_unique<web::FakeNavigationManager>());

    // Create a SessionRestorationScrollObserverWrapper to hold the captured
    // scroll observer. This allows capturing the observer while mocking in
    // a safe way as the block does not need to reference `this`.
    wrapper_ = [[SessionRestorationScrollObserverWrapper alloc] init];
    web_state_->SetWebViewProxy(MockWebViewProxy(wrapper_));
  }

  void TearDown() override {
    // The observer needs to be destroyed before the WebState.
    SessionRestorationWebStateObserver::RemoveFromWebState(web_state_.get());

    // The observer needs to be unregistered on destruction.
    ASSERT_FALSE(wrapper_.value);
    wrapper_ = nil;

    PlatformTest::TearDown();
  }

  // Creates and returns the SessionRestorationWebStateObserver for `web_state`.
  SessionRestorationWebStateObserver* CreateSessionRestorationWebStateObserver(
      web::WebState* web_state,
      base::RepeatingClosure closure) {
    SessionRestorationWebStateObserver::CreateForWebState(
        web_state, base::IgnoreArgs<web::WebState*>(closure));

    return SessionRestorationWebStateObserver::FromWebState(web_state);
  }

  // Returns the fake WebState.
  web::FakeWebState* web_state() { return web_state_.get(); }

  // Returns the captured observer. Can be used to simulate scroll events.
  id<CRWWebViewScrollViewObserver> scroll_observer() { return wrapper_.value; }

  // Returns the fake WebFramesManager.
  web::FakeWebFramesManager* web_frames_manager() {
    return static_cast<web::FakeWebFramesManager*>(
        web_state_->GetPageWorldWebFramesManager());
  }

 private:
  // The fake WebState used by the tests.
  std::unique_ptr<web::FakeWebState> web_state_;

  // Captured observer.
  __strong SessionRestorationScrollObserverWrapper* wrapper_ = nil;
};

// Tests that SessionRestorationWebStateObserver consider the WebState as
// clean on creation.
TEST_F(SessionRestorationWebStateObserverTest, Creation) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  EXPECT_FALSE(observer->is_dirty());
  EXPECT_EQ(call_count, 0u);
}

// Tests that SessionRestorationWebStateObserver mark the WebState as
// dirty upon scroll events.
TEST_F(SessionRestorationWebStateObserverTest, ScrollEvent) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  [scroll_observer() webViewScrollViewDidEndDragging:nil willDecelerate:NO];
  [scroll_observer() webViewScrollViewDidEndDragging:nil willDecelerate:NO];

  EXPECT_TRUE(observer->is_dirty());
  EXPECT_EQ(call_count, 1u);
}

// Tests that SessionRestorationWebStateObserver mark the WebState as
// dirty upon navigation committed.
TEST_F(SessionRestorationWebStateObserverTest, NavigationCommitted) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  web::FakeNavigationContext context;
  context.SetIsDownload(false);
  web_state()->OnNavigationFinished(&context);
  web_state()->OnNavigationFinished(&context);

  EXPECT_TRUE(observer->is_dirty());
  EXPECT_EQ(call_count, 1u);
}

// Tests that SessionRestorationWebStateObserver consider the WebState as
// clean on navigation that are download.
TEST_F(SessionRestorationWebStateObserverTest, NavigationCommitted_Download) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  web::FakeNavigationContext context;
  context.SetIsDownload(true);
  web_state()->OnNavigationFinished(&context);
  web_state()->OnNavigationFinished(&context);

  EXPECT_FALSE(observer->is_dirty());
  EXPECT_EQ(call_count, 0u);
}

// Tests that SessionRestorationWebStateObserver mark the WebState as
// dirty when the WebState is shown (which updates the last active time).
TEST_F(SessionRestorationWebStateObserverTest, WasShown) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  web_state()->WasShown();
  web_state()->WasShown();

  EXPECT_TRUE(observer->is_dirty());
  EXPECT_EQ(call_count, 1u);
}

// Tests that SessionRestorationWebStateObserver mark the WebState as
// dirty when a WebFrame becomes available.
TEST_F(SessionRestorationWebStateObserverTest, WebFrameAvailable) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  web_frames_manager()->AddWebFrame(
      web::FakeWebFrame::CreateChildWebFrame(GURL("https://example.com/c")));

  EXPECT_TRUE(observer->is_dirty());
  EXPECT_EQ(call_count, 1u);
}

// Tests that SessionRestorationWebStateObserver consider the WebState as
// clean when main frame become available if this is the main frame.
TEST_F(SessionRestorationWebStateObserverTest, WebFrameAvailable_MainFrame) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  web_frames_manager()->AddWebFrame(
      web::FakeWebFrame::CreateMainWebFrame(GURL("https://example.com")));

  EXPECT_FALSE(observer->is_dirty());
  EXPECT_EQ(call_count, 0u);
}

// Tests that SessionRestorationWebStateObserver consider the WebState as
// clean when WebFrame become available but no navigation occurred.
TEST_F(SessionRestorationWebStateObserverTest, WebFrameAvailable_NoNavigation) {
  web_state()->SetIsRealized(true);

  size_t call_count = 0;
  SessionRestorationWebStateObserver* observer =
      CreateSessionRestorationWebStateObserver(
          web_state(), base::BindRepeating(&IncrementCounter, &call_count));

  web::FakeNavigationContext context;
  context.SetIsDownload(false);
  web_state()->OnNavigationFinished(&context);
  web_state()->OnNavigationFinished(&context);

  EXPECT_TRUE(observer->is_dirty());
  EXPECT_EQ(call_count, 1u);

  // Reset the dirty flag.
  observer->clear_dirty();
  EXPECT_FALSE(observer->is_dirty());

  web_frames_manager()->AddWebFrame(
      web::FakeWebFrame::CreateChildWebFrame(GURL("https://example.com/c")));

  EXPECT_FALSE(observer->is_dirty());
  EXPECT_EQ(call_count, 1u);
}
