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

#include "components/web_modal/web_contents_modal_dialog_manager.h"

#include <array>
#include <map>
#include <memory>

#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "components/web_modal/single_web_contents_dialog_manager.h"
#include "components/web_modal/test_web_contents_modal_dialog_manager_delegate.h"
#include "content/public/test/test_renderer_host.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_MAC)
#include "components/web_modal/web_contents_modal_dialog_manager_unittest_mac_helper.h"
#endif

namespace web_modal {

class MockCloseOnNavigationObserver
    : public WebContentsModalDialogManager::Observer {
 public:
  MockCloseOnNavigationObserver() = default;
  ~MockCloseOnNavigationObserver() override = default;

  MOCK_METHOD(void, OnWillCloseOnNavigation, (), (override));
  MOCK_METHOD(void, OnWillShow, (), (override));
};

// Tracks persistent state changes of the native WC-modal dialog manager.
class NativeManagerTracker {
 public:
  enum DialogState {
    UNKNOWN,
    NOT_SHOWN,
    SHOWN,
    HIDDEN,
    CLOSED
  };

  NativeManagerTracker() = default;

  void SetState(DialogState state) {
    state_ = state;
    if (state_ == SHOWN) {
      ++shown_times_;
    }
  }

  bool was_shown() { return shown_times_ > 0; }

  DialogState state_ = DialogState::UNKNOWN;
  int shown_times_ = 0;
};

NativeManagerTracker unused_tracker;

class TestNativeWebContentsModalDialogManager
    : public SingleWebContentsDialogManager {
 public:
  TestNativeWebContentsModalDialogManager(
      gfx::NativeWindow dialog,
      SingleWebContentsDialogManagerDelegate* delegate,
      NativeManagerTracker* tracker)
      : delegate_(delegate),
        dialog_(dialog),
        tracker_(tracker) {
    if (tracker_)
      tracker_->SetState(NativeManagerTracker::NOT_SHOWN);
  }

  TestNativeWebContentsModalDialogManager(
      const TestNativeWebContentsModalDialogManager&) = delete;
  TestNativeWebContentsModalDialogManager& operator=(
      const TestNativeWebContentsModalDialogManager&) = delete;

  void Show() override {
    if (tracker_)
      tracker_->SetState(NativeManagerTracker::SHOWN);
  }
  void Hide() override {
    if (tracker_)
      tracker_->SetState(NativeManagerTracker::HIDDEN);
  }
  void Close() override {
    if (tracker_)
      tracker_->SetState(NativeManagerTracker::CLOSED);
    delegate_->WillClose(dialog_);
  }
  void Focus() override {}
  void Pulse() override {}
  void HostChanged(WebContentsModalDialogHost* new_host) override {}
  gfx::NativeWindow dialog() override { return dialog_; }
  bool IsActive() const override { return is_active_; }

  void StopTracking() { tracker_ = nullptr; }
  void SetIsActive(bool is_active) { is_active_ = is_active; }

 private:
  raw_ptr<SingleWebContentsDialogManagerDelegate> delegate_;
  gfx::NativeWindow dialog_;
  raw_ptr<NativeManagerTracker> tracker_;
  bool is_active_;
};

class WebContentsModalDialogManagerTest
    : public content::RenderViewHostTestHarness {
 public:
  WebContentsModalDialogManagerTest() = default;

  WebContentsModalDialogManagerTest(const WebContentsModalDialogManagerTest&) =
      delete;
  WebContentsModalDialogManagerTest& operator=(
      const WebContentsModalDialogManagerTest&) = delete;

  void SetUp() override {
    content::RenderViewHostTestHarness::SetUp();

    delegate = std::make_unique<TestWebContentsModalDialogManagerDelegate>();
    WebContentsModalDialogManager::CreateForWebContents(web_contents());
    manager = WebContentsModalDialogManager::FromWebContents(web_contents());
    manager->SetDelegate(delegate.get());
    test_api =
        std::make_unique<WebContentsModalDialogManager::TestApi>(manager);
  }

  void TearDown() override {
#if BUILDFLAG(IS_MAC)
    TearDownFakeNativeWindowsForTesting();
#endif
    manager = nullptr;
    test_api.reset();
    content::RenderViewHostTestHarness::TearDown();
  }

 protected:
  gfx::NativeWindow MakeFakeDialog() {
#if BUILDFLAG(IS_MAC)
    return FakeNativeWindowForTesting();
#else
    // WebContentsModalDialogManager treats the dialog window as an opaque
    // type, so creating fake dialog windows using reinterpret_cast is valid.
    return reinterpret_cast<gfx::NativeWindow>(next_dialog_id++);
#endif
  }

#if !BUILDFLAG(IS_MAC)
  int next_dialog_id = 1;
#endif
  std::unique_ptr<TestWebContentsModalDialogManagerDelegate> delegate;
  raw_ptr<WebContentsModalDialogManager> manager = nullptr;
  std::unique_ptr<WebContentsModalDialogManager::TestApi> test_api;
};

// Test that the dialog is shown immediately when the delegate indicates the web
// contents is visible.
TEST_F(WebContentsModalDialogManagerTest, WebContentsVisible) {
  // Dialog should be shown while WebContents is visible.
  const gfx::NativeWindow dialog = MakeFakeDialog();

  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);
  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker.state_);
  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_TRUE(tracker.was_shown());

  native_manager->StopTracking();
}

// Test that setting a new delegate correctly synchronizes the blocked state
// if a dialog is currently active. This is crucial for split-view window moves.
TEST_F(WebContentsModalDialogManagerTest, SetDelegateSyncsBlockedState) {
  const gfx::NativeWindow dialog = MakeFakeDialog();

  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);
  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());

  auto new_delegate =
      std::make_unique<TestWebContentsModalDialogManagerDelegate>();
  EXPECT_FALSE(new_delegate->web_contents_blocked());

  manager->SetDelegate(new_delegate.get());

  // The new delegate should be updated with the current blocked state.
  EXPECT_TRUE(new_delegate->web_contents_blocked());

  // Restore the original delegate so new_delegate doesn't dangle after
  // destruction.
  manager->SetDelegate(delegate.get());

  native_manager->StopTracking();
}

// Test that the dialog is not shown immediately when the delegate indicates the
// web contents is not visible.
TEST_F(WebContentsModalDialogManagerTest, WebContentsNotVisible) {
  // Dialog should not be shown while WebContents is not visible.
  delegate->set_web_contents_visible(false);

  const gfx::NativeWindow dialog = MakeFakeDialog();

  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);
  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  EXPECT_EQ(NativeManagerTracker::NOT_SHOWN, tracker.state_);
  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_FALSE(tracker.was_shown());

  native_manager->StopTracking();
}

// Test that only the first of multiple dialogs is shown.
TEST_F(WebContentsModalDialogManagerTest, ShowDialogs) {
  const gfx::NativeWindow dialog1 = MakeFakeDialog();
  const gfx::NativeWindow dialog2 = MakeFakeDialog();
  const gfx::NativeWindow dialog3 = MakeFakeDialog();

  NativeManagerTracker tracker1;
  NativeManagerTracker tracker2;
  NativeManagerTracker tracker3;
  TestNativeWebContentsModalDialogManager* native_manager1 =
      new TestNativeWebContentsModalDialogManager(dialog1, manager, &tracker1);
  TestNativeWebContentsModalDialogManager* native_manager2 =
      new TestNativeWebContentsModalDialogManager(dialog2, manager, &tracker2);
  TestNativeWebContentsModalDialogManager* native_manager3 =
      new TestNativeWebContentsModalDialogManager(dialog3, manager, &tracker3);
  manager->ShowDialogWithManager(dialog1, base::WrapUnique(native_manager1));
  manager->ShowDialogWithManager(dialog2, base::WrapUnique(native_manager2));
  manager->ShowDialogWithManager(dialog3, base::WrapUnique(native_manager3));

  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker1.state_);
  EXPECT_EQ(NativeManagerTracker::NOT_SHOWN, tracker2.state_);
  EXPECT_EQ(NativeManagerTracker::NOT_SHOWN, tracker3.state_);

  native_manager1->StopTracking();
  native_manager2->StopTracking();
  native_manager3->StopTracking();
}

// Test that the dialog is shown/hidden when the WebContents is shown/hidden.
TEST_F(WebContentsModalDialogManagerTest, VisibilityObservation) {
  const gfx::NativeWindow dialog = MakeFakeDialog();

  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);
  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker.state_);

  test_api->WebContentsVisibilityChanged(content::Visibility::HIDDEN);

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::HIDDEN, tracker.state_);

  test_api->WebContentsVisibilityChanged(content::Visibility::VISIBLE);

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker.state_);

  native_manager->StopTracking();
}

// Tests that the dialog shows when switching from occluded to visible.
TEST_F(WebContentsModalDialogManagerTest, OccludedToVisible) {
  const gfx::NativeWindow dialog = MakeFakeDialog();

  delegate->set_web_contents_visible(false);
  test_api->WebContentsVisibilityChanged(content::Visibility::OCCLUDED);

  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);
  native_manager->SetIsActive(false);

  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::NOT_SHOWN, tracker.state_);

  test_api->WebContentsVisibilityChanged(content::Visibility::VISIBLE);

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker.state_);

  native_manager->StopTracking();
}

// Tests that the dialog is not shown when switching from visible to occluded.
// Regression test for crbug.com/350745485.
TEST_F(WebContentsModalDialogManagerTest, VisibleToOccluded) {
  const gfx::NativeWindow dialog = MakeFakeDialog();

  delegate->set_web_contents_visible(true);
  test_api->WebContentsVisibilityChanged(content::Visibility::VISIBLE);

  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);

  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker.state_);
  EXPECT_EQ(tracker.shown_times_, 1);

  // Simulate e.g. the user clicking on another window.
  native_manager->SetIsActive(false);
  test_api->WebContentsVisibilityChanged(content::Visibility::OCCLUDED);

  // The dialog should still be shown, but Show() should not have been called
  // again.
  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker.state_);
  EXPECT_EQ(tracker.shown_times_, 1);

  native_manager->StopTracking();
}

// Test that the first dialog is always shown, regardless of the order in which
// dialogs are closed.
TEST_F(WebContentsModalDialogManagerTest, CloseDialogs) {
  // The front dialog is always shown regardless of dialog close order.
  const gfx::NativeWindow dialog1 = MakeFakeDialog();
  const gfx::NativeWindow dialog2 = MakeFakeDialog();
  const gfx::NativeWindow dialog3 = MakeFakeDialog();
  const gfx::NativeWindow dialog4 = MakeFakeDialog();

  NativeManagerTracker tracker1;
  NativeManagerTracker tracker2;
  NativeManagerTracker tracker3;
  NativeManagerTracker tracker4;
  TestNativeWebContentsModalDialogManager* native_manager1 =
      new TestNativeWebContentsModalDialogManager(dialog1, manager, &tracker1);
  TestNativeWebContentsModalDialogManager* native_manager2 =
      new TestNativeWebContentsModalDialogManager(dialog2, manager, &tracker2);
  TestNativeWebContentsModalDialogManager* native_manager3 =
      new TestNativeWebContentsModalDialogManager(dialog3, manager, &tracker3);
  TestNativeWebContentsModalDialogManager* native_manager4 =
      new TestNativeWebContentsModalDialogManager(dialog4, manager, &tracker4);
  manager->ShowDialogWithManager(dialog1, base::WrapUnique(native_manager1));
  manager->ShowDialogWithManager(dialog2, base::WrapUnique(native_manager2));
  manager->ShowDialogWithManager(dialog3, base::WrapUnique(native_manager3));
  manager->ShowDialogWithManager(dialog4, base::WrapUnique(native_manager4));

  native_manager1->Close();

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker1.state_);
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker2.state_);
  EXPECT_EQ(NativeManagerTracker::NOT_SHOWN, tracker3.state_);
  EXPECT_EQ(NativeManagerTracker::NOT_SHOWN, tracker4.state_);

  native_manager3->Close();

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker1.state_);
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker2.state_);
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker3.state_);
  EXPECT_EQ(NativeManagerTracker::NOT_SHOWN, tracker4.state_);
  EXPECT_FALSE(tracker3.was_shown());

  native_manager2->Close();

  EXPECT_TRUE(manager->IsDialogActive());
  EXPECT_TRUE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker1.state_);
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker2.state_);
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker3.state_);
  EXPECT_EQ(NativeManagerTracker::SHOWN, tracker4.state_);
  EXPECT_FALSE(tracker3.was_shown());

  native_manager4->Close();

  EXPECT_FALSE(manager->IsDialogActive());
  EXPECT_FALSE(delegate->web_contents_blocked());
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker1.state_);
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker2.state_);
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker3.state_);
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker4.state_);
  EXPECT_TRUE(tracker1.was_shown());
  EXPECT_TRUE(tracker2.was_shown());
  EXPECT_FALSE(tracker3.was_shown());
  EXPECT_TRUE(tracker4.was_shown());
}

// Test that CloseAllDialogs does what it says.
TEST_F(WebContentsModalDialogManagerTest, CloseAllDialogs) {
  const int kWindowCount = 4;
  std::array<NativeManagerTracker, kWindowCount> trackers;
  std::array<TestNativeWebContentsModalDialogManager*, kWindowCount>
      native_managers;
  for (int i = 0; i < kWindowCount; i++) {
    const gfx::NativeWindow dialog = MakeFakeDialog();
    native_managers[i] =
        new TestNativeWebContentsModalDialogManager(
            dialog, manager, &(trackers[i]));
    manager->ShowDialogWithManager(dialog,
                                   base::WrapUnique(native_managers[i]));
  }

  for (int i = 0; i < kWindowCount; i++)
    EXPECT_NE(NativeManagerTracker::CLOSED, trackers[i].state_);

  test_api->CloseAllDialogs();

  EXPECT_FALSE(delegate->web_contents_blocked());
  EXPECT_FALSE(manager->IsDialogActive());
  for (int i = 0; i < kWindowCount; i++)
    EXPECT_EQ(NativeManagerTracker::CLOSED, trackers[i].state_);
}

// Test that dialogs are closed on WebContents navigation.
TEST_F(WebContentsModalDialogManagerTest, CloseOnNavigation) {
  MockCloseOnNavigationObserver observer;
  EXPECT_CALL(observer, OnWillCloseOnNavigation());

  const gfx::NativeWindow dialog = MakeFakeDialog();
  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);
  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  manager->AddObserver(&observer);

  NavigateAndCommit(GURL("https://example.com/"));
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker.state_);
}

// Test that the CloseOnNavigation observer is not triggered if the dialog is
// closed for another reason.
TEST_F(WebContentsModalDialogManagerTest,
       ObserverNotNotifiedOfNonNavigationClose) {
  MockCloseOnNavigationObserver observer;
  EXPECT_CALL(observer, OnWillCloseOnNavigation()).Times(0);

  const gfx::NativeWindow dialog = MakeFakeDialog();
  NativeManagerTracker tracker;
  TestNativeWebContentsModalDialogManager* native_manager =
      new TestNativeWebContentsModalDialogManager(dialog, manager, &tracker);
  manager->ShowDialogWithManager(dialog, base::WrapUnique(native_manager));

  manager->AddObserver(&observer);

  native_manager->Close();
  EXPECT_EQ(NativeManagerTracker::CLOSED, tracker.state_);
}

}  // namespace web_modal
