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

#include "chrome/browser/ui/panels/base_panel_browser_test.h"

#include "base/bind.h"
#include "base/command_line.h"
#include "base/memory/weak_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/extension_prefs.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/panels/detached_panel_collection.h"
#include "chrome/browser/ui/panels/native_panel.h"
#include "chrome/browser/ui/panels/panel_collection.h"
#include "chrome/browser/ui/panels/panel_mouse_watcher.h"
#include "chrome/browser/ui/panels/stacked_panel_collection.h"
#include "chrome/browser/ui/panels/test_panel_active_state_observer.h"
#include "chrome/browser/ui/panels/test_panel_mouse_watcher.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/notification_service.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/common/manifest_constants.h"
#include "sync/api/string_ordinal.h"

#if defined(OS_LINUX)
#include "chrome/browser/ui/browser_window.h"
#include "ui/base/x/x11_util.h"
#endif

#if defined(OS_LINUX) && !defined(USE_AURA)
#include "ui/base/x/active_window_watcher_x.h"
#endif

#if defined(OS_MACOSX)
#include "base/mac/scoped_nsautorelease_pool.h"
#include "chrome/browser/ui/cocoa/run_loop_testing.h"
#endif

using content::WebContentsTester;
using extensions::Extension;

namespace {

const gfx::Rect kTestingPrimaryDisplayArea = gfx::Rect(0, 0, 800, 600);
const gfx::Rect kTestingPrimaryWorkArea = gfx::Rect(0, 0, 800, 580);

struct MockDesktopBar {
  bool auto_hiding_enabled;
  DisplaySettingsProvider::DesktopBarVisibility visibility;
  int thickness;
};

class MockDisplaySettingsProviderImpl :
    public BasePanelBrowserTest::MockDisplaySettingsProvider {
 public:
  explicit MockDisplaySettingsProviderImpl();
  virtual ~MockDisplaySettingsProviderImpl() { }

  // Overridden from DisplaySettingsProvider:
  virtual gfx::Rect GetPrimaryDisplayArea() const OVERRIDE;
  virtual gfx::Rect GetPrimaryWorkArea() const OVERRIDE;
  virtual gfx::Rect GetDisplayAreaMatching(
      const gfx::Rect& bounds) const OVERRIDE;
  virtual gfx::Rect GetWorkAreaMatching(
      const gfx::Rect& bounds) const OVERRIDE;
  virtual bool IsAutoHidingDesktopBarEnabled(
      DesktopBarAlignment alignment) OVERRIDE;
  virtual int GetDesktopBarThickness(
      DesktopBarAlignment alignment) const OVERRIDE;
  virtual DesktopBarVisibility GetDesktopBarVisibility(
      DesktopBarAlignment alignment) const OVERRIDE;
  virtual bool IsFullScreen() OVERRIDE;

  // Overridden from MockDisplaySettingsProvider:
  virtual void SetPrimaryDisplay(
      const gfx::Rect& display_area, const gfx::Rect& work_area) OVERRIDE;
  virtual void SetSecondaryDisplay(
      const gfx::Rect& display_area, const gfx::Rect& work_area) OVERRIDE;
  virtual void EnableAutoHidingDesktopBar(DesktopBarAlignment alignment,
                                          bool enabled,
                                          int thickness) OVERRIDE;
  virtual void SetDesktopBarVisibility(
      DesktopBarAlignment alignment, DesktopBarVisibility visibility) OVERRIDE;
  virtual void SetDesktopBarThickness(DesktopBarAlignment alignment,
                                      int thickness) OVERRIDE;
  virtual void EnableFullScreenMode(bool enabled) OVERRIDE;

 private:
  gfx::Rect primary_display_area_;
  gfx::Rect primary_work_area_;
  gfx::Rect secondary_display_area_;
  gfx::Rect secondary_work_area_;
  MockDesktopBar mock_desktop_bars[3];
  bool full_screen_enabled_;

  DISALLOW_COPY_AND_ASSIGN(MockDisplaySettingsProviderImpl);
};


MockDisplaySettingsProviderImpl::MockDisplaySettingsProviderImpl()
    : full_screen_enabled_(false) {
  memset(mock_desktop_bars, 0, sizeof(mock_desktop_bars));
}

gfx::Rect MockDisplaySettingsProviderImpl::GetPrimaryDisplayArea() const {
  return primary_display_area_;
}

gfx::Rect MockDisplaySettingsProviderImpl::GetPrimaryWorkArea() const {
  return primary_work_area_;
}

gfx::Rect MockDisplaySettingsProviderImpl::GetDisplayAreaMatching(
    const gfx::Rect& bounds) const {
  if (secondary_display_area_.IsEmpty())
    return primary_display_area_;

  gfx::Rect primary_intersection =
      gfx::IntersectRects(bounds, primary_display_area_);
  int primary_intersection_size =
      primary_intersection.width() * primary_intersection.height();

  gfx::Rect secondary_intersection =
      gfx::IntersectRects(bounds, secondary_display_area_);
  int secondary_intersection_size =
      secondary_intersection.width() * secondary_intersection.height();

  return primary_intersection_size >= secondary_intersection_size ?
      primary_display_area_ : secondary_display_area_;
}

gfx::Rect MockDisplaySettingsProviderImpl::GetWorkAreaMatching(
    const gfx::Rect& bounds) const {
  if (secondary_work_area_.IsEmpty())
    return primary_work_area_;

  gfx::Rect primary_intersection =
      gfx::IntersectRects(bounds, primary_work_area_);
  int primary_intersection_size =
      primary_intersection.width() * primary_intersection.height();

  gfx::Rect secondary_intersection =
      gfx::IntersectRects(bounds, secondary_work_area_);
  int secondary_intersection_size =
      secondary_intersection.width() * secondary_intersection.height();

  return primary_intersection_size >= secondary_intersection_size ?
      primary_work_area_ : secondary_work_area_;
}

bool MockDisplaySettingsProviderImpl::IsAutoHidingDesktopBarEnabled(
    DesktopBarAlignment alignment) {
  return mock_desktop_bars[static_cast<int>(alignment)].auto_hiding_enabled;
}

int MockDisplaySettingsProviderImpl::GetDesktopBarThickness(
    DesktopBarAlignment alignment) const {
  return mock_desktop_bars[static_cast<int>(alignment)].thickness;
}

DisplaySettingsProvider::DesktopBarVisibility
MockDisplaySettingsProviderImpl::GetDesktopBarVisibility(
    DesktopBarAlignment alignment) const {
  return mock_desktop_bars[static_cast<int>(alignment)].visibility;
}

bool MockDisplaySettingsProviderImpl::IsFullScreen() {
  return full_screen_enabled_;
}

void MockDisplaySettingsProviderImpl::EnableAutoHidingDesktopBar(
    DesktopBarAlignment alignment, bool enabled, int thickness) {
  MockDesktopBar* bar = &(mock_desktop_bars[static_cast<int>(alignment)]);
  bar->auto_hiding_enabled = enabled;
  bar->thickness = thickness;
}

void MockDisplaySettingsProviderImpl::SetPrimaryDisplay(
    const gfx::Rect& display_area, const gfx::Rect& work_area) {
  DCHECK(display_area.Contains(work_area));
  primary_display_area_ = display_area;
  primary_work_area_ = work_area;
  OnDisplaySettingsChanged();
}

void MockDisplaySettingsProviderImpl::SetSecondaryDisplay(
    const gfx::Rect& display_area, const gfx::Rect& work_area) {
  DCHECK(display_area.Contains(work_area));
  secondary_display_area_ = display_area;
  secondary_work_area_ = work_area;
  OnDisplaySettingsChanged();
}

void MockDisplaySettingsProviderImpl::SetDesktopBarVisibility(
    DesktopBarAlignment alignment, DesktopBarVisibility visibility) {
  MockDesktopBar* bar = &(mock_desktop_bars[static_cast<int>(alignment)]);
  if (!bar->auto_hiding_enabled)
    return;
  if (visibility == bar->visibility)
    return;
  bar->visibility = visibility;
  FOR_EACH_OBSERVER(
      DesktopBarObserver,
      desktop_bar_observers(),
      OnAutoHidingDesktopBarVisibilityChanged(alignment, visibility));
}

void MockDisplaySettingsProviderImpl::SetDesktopBarThickness(
    DesktopBarAlignment alignment, int thickness) {
  MockDesktopBar* bar = &(mock_desktop_bars[static_cast<int>(alignment)]);
  if (!bar->auto_hiding_enabled)
    return;
  if (thickness == bar->thickness)
    return;
  bar->thickness = thickness;
  FOR_EACH_OBSERVER(
      DesktopBarObserver,
      desktop_bar_observers(),
      OnAutoHidingDesktopBarThicknessChanged(alignment, thickness));
}

void MockDisplaySettingsProviderImpl::EnableFullScreenMode(bool enabled) {
  full_screen_enabled_ = enabled;
  CheckFullScreenMode(PERFORM_FULLSCREEN_CHECK);
}

}  // namespace

const base::FilePath::CharType* BasePanelBrowserTest::kTestDir =
    FILE_PATH_LITERAL("panels");

BasePanelBrowserTest::BasePanelBrowserTest()
    : InProcessBrowserTest(),
      mock_display_settings_enabled_(true) {
}

BasePanelBrowserTest::~BasePanelBrowserTest() {
}

bool BasePanelBrowserTest::SkipTestIfIceWM() {
#if defined(OS_LINUX)
  return ui::GuessWindowManager() == ui::WM_ICE_WM;
#else
  return false;
#endif
}

bool BasePanelBrowserTest::SkipTestIfCompizWM() {
#if defined(OS_LINUX)
  return ui::GuessWindowManager() == ui::WM_COMPIZ;
#else
  return false;
#endif
}

void BasePanelBrowserTest::SetUpCommandLine(CommandLine* command_line) {
  command_line->AppendSwitch(switches::kEnablePanels);
}

void BasePanelBrowserTest::SetUpOnMainThread() {
  InProcessBrowserTest::SetUpOnMainThread();

  // Setup the work area and desktop bar so that we have consistent testing
  // environment for all panel related tests.
  if (mock_display_settings_enabled_) {
    mock_display_settings_provider_ = new MockDisplaySettingsProviderImpl();
    mock_display_settings_provider_->SetPrimaryDisplay(
        kTestingPrimaryDisplayArea, kTestingPrimaryWorkArea);
    PanelManager::SetDisplaySettingsProviderForTesting(
        mock_display_settings_provider_);
  }

  PanelManager* panel_manager = PanelManager::GetInstance();
  panel_manager->enable_auto_sizing(false);

  PanelManager::shorten_time_intervals_for_testing();

  // Simulate the mouse movement so that tests are not affected by actual mouse
  // events.
  PanelMouseWatcher* mouse_watcher = new TestPanelMouseWatcher();
  panel_manager->SetMouseWatcherForTesting(mouse_watcher);

  // This is needed so the subsequently created panels can be activated.
  // On a Mac, it transforms background-only test process into foreground one.
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
}

void BasePanelBrowserTest::WaitForPanelActiveState(
    Panel* panel, ActiveState expected_state) {
  DCHECK(expected_state == SHOW_AS_ACTIVE ||
         expected_state == SHOW_AS_INACTIVE);

#if defined(OS_MACOSX)
  scoped_ptr<NativePanelTesting> panel_testing(
      CreateNativePanelTesting(panel));
  ASSERT_TRUE(panel_testing->EnsureApplicationRunOnForeground()) <<
      "Failed to bring application to foreground. Bail out.";
#endif

  PanelActiveStateObserver signal(panel, expected_state == SHOW_AS_ACTIVE);
  signal.Wait();
}

void BasePanelBrowserTest::WaitForWindowSizeAvailable(Panel* panel) {
  scoped_ptr<NativePanelTesting> panel_testing(
      CreateNativePanelTesting(panel));
  content::WindowedNotificationObserver signal(
      chrome::NOTIFICATION_PANEL_WINDOW_SIZE_KNOWN,
      content::Source<Panel>(panel));
  if (panel_testing->IsWindowSizeKnown())
    return;
  signal.Wait();
  EXPECT_TRUE(panel_testing->IsWindowSizeKnown());
}

void BasePanelBrowserTest::WaitForBoundsAnimationFinished(Panel* panel) {
  scoped_ptr<NativePanelTesting> panel_testing(
      CreateNativePanelTesting(panel));
  // Sometimes there are several animations in sequence due to content
  // auto resizing. Wait for all animations to finish.
  while (panel_testing->IsAnimatingBounds()) {
    content::WindowedNotificationObserver signal(
        chrome::NOTIFICATION_PANEL_BOUNDS_ANIMATIONS_FINISHED,
        content::Source<Panel>(panel));
    if (!panel_testing->IsAnimatingBounds())
      return;
    signal.Wait();
  }
}

BasePanelBrowserTest::CreatePanelParams::CreatePanelParams(
    const std::string& name,
    const gfx::Rect& bounds,
    ActiveState show_flag)
    : name(name),
      bounds(bounds),
      show_flag(show_flag),
      wait_for_fully_created(true),
      expected_active_state(show_flag),
      create_mode(PanelManager::CREATE_AS_DOCKED),
      profile(NULL) {
}

Panel* BasePanelBrowserTest::CreatePanelWithParams(
    const CreatePanelParams& params) {
#if defined(OS_MACOSX)
  // Opening panels on a Mac causes NSWindowController of the Panel window
  // to be autoreleased. We need a pool drained after it's done so the test
  // can close correctly. The NSWindowController of the Panel window controls
  // lifetime of the Panel object so we want to release it as soon as
  // possible. In real Chrome, this is done by message pump.
  // On non-Mac platform, this is an empty class.
  base::mac::ScopedNSAutoreleasePool autorelease_pool;
#endif

  content::WindowedNotificationObserver observer(
      content::NOTIFICATION_LOAD_STOP,
      content::NotificationService::AllSources());

  PanelManager* manager = PanelManager::GetInstance();
  Panel* panel = manager->CreatePanel(
      params.name,
      params.profile ? params.profile : browser()->profile(),
      params.url,
      params.bounds,
      params.create_mode);

  if (!params.url.is_empty())
    observer.Wait();

  if (!manager->auto_sizing_enabled() ||
      params.bounds.width() || params.bounds.height()) {
    EXPECT_FALSE(panel->auto_resizable());
  } else {
    EXPECT_TRUE(panel->auto_resizable());
  }

  if (params.show_flag == SHOW_AS_ACTIVE) {
    panel->Show();
  } else {
    panel->ShowInactive();
  }

  if (params.wait_for_fully_created) {
    base::MessageLoopForUI::current()->RunUntilIdle();

#if defined(OS_LINUX)
    // On bots, we might have a simple window manager which always activates new
    // windows, and can't always deactivate them. Re-activate the main tabbed
    // browser to "deactivate" the newly created panel.
    if (params.expected_active_state == SHOW_AS_INACTIVE &&
        ui::GuessWindowManager() == ui::WM_ICE_WM) {
      // Wait for new panel to become active before deactivating to ensure
      // the activated notification is consumed before we wait for the panel
      // to become inactive.
      WaitForPanelActiveState(panel, SHOW_AS_ACTIVE);
      browser()->window()->Activate();
    }
#endif
    // More waiting, because gaining or losing focus may require inter-process
    // asynchronous communication, and it is not enough to just run the local
    // message loop to make sure this activity has completed.
    WaitForPanelActiveState(panel, params.expected_active_state);

    // On Linux, window size is not available right away and we should wait
    // before moving forward with the test.
    WaitForWindowSizeAvailable(panel);

    // Wait for the bounds animations on creation to finish.
    WaitForBoundsAnimationFinished(panel);
  }

  return panel;
}

Panel* BasePanelBrowserTest::CreatePanelWithBounds(
    const std::string& panel_name, const gfx::Rect& bounds) {
  CreatePanelParams params(panel_name, bounds, SHOW_AS_ACTIVE);
  return CreatePanelWithParams(params);
}

Panel* BasePanelBrowserTest::CreatePanel(const std::string& panel_name) {
  CreatePanelParams params(panel_name, gfx::Rect(), SHOW_AS_ACTIVE);
  return CreatePanelWithParams(params);
}

Panel* BasePanelBrowserTest::CreateDockedPanel(const std::string& name,
                                               const gfx::Rect& bounds) {
  Panel* panel = CreatePanelWithBounds(name, bounds);
  EXPECT_EQ(PanelCollection::DOCKED, panel->collection()->type());
  return panel;
}

Panel* BasePanelBrowserTest::CreateDetachedPanel(const std::string& name,
                                                 const gfx::Rect& bounds) {
  Panel* panel = CreatePanelWithBounds(name, bounds);
  PanelManager* panel_manager = panel->manager();
  panel_manager->MovePanelToCollection(panel,
                                       panel_manager->detached_collection(),
                                       PanelCollection::DEFAULT_POSITION);
  EXPECT_EQ(PanelCollection::DETACHED, panel->collection()->type());
  // The panel is first created as docked panel, which ignores the specified
  // origin in |bounds|. We need to reposition the panel after it becomes
  // detached.
  panel->SetPanelBounds(bounds);
  WaitForBoundsAnimationFinished(panel);
  return panel;
}

Panel* BasePanelBrowserTest::CreateStackedPanel(const std::string& name,
                                                const gfx::Rect& bounds,
                                                StackedPanelCollection* stack) {
  Panel* panel = CreateDetachedPanel(name, bounds);
  panel->manager()->MovePanelToCollection(
      panel,
      stack,
      static_cast<PanelCollection::PositioningMask>(
          PanelCollection::DEFAULT_POSITION |
          PanelCollection::COLLAPSE_TO_FIT));
  EXPECT_EQ(PanelCollection::STACKED, panel->collection()->type());
  WaitForBoundsAnimationFinished(panel);
  return panel;
}

Panel* BasePanelBrowserTest::CreateInactivePanel(const std::string& name) {
  // Create an active panel first, instead of inactive panel. This is because
  // certain window managers on Linux, like icewm, will always activate the
  // new window.
  Panel* panel = CreatePanel(name);

  DeactivatePanel(panel);
  WaitForPanelActiveState(panel, SHOW_AS_INACTIVE);

  return panel;
}

Panel* BasePanelBrowserTest::CreateInactiveDockedPanel(
    const std::string& name, const gfx::Rect& bounds) {
  // Create an active panel first, instead of inactive panel. This is because
  // certain window managers on Linux, like icewm, will always activate the
  // new window.
  Panel* panel = CreateDockedPanel(name, bounds);

  DeactivatePanel(panel);
  WaitForPanelActiveState(panel, SHOW_AS_INACTIVE);

  return panel;
}

Panel* BasePanelBrowserTest::CreateInactiveDetachedPanel(
    const std::string& name, const gfx::Rect& bounds) {
  // Create an active panel first, instead of inactive panel. This is because
  // certain window managers on Linux, like icewm, will always activate the
  // new window.
  Panel* panel = CreateDetachedPanel(name, bounds);

  DeactivatePanel(panel);
  WaitForPanelActiveState(panel, SHOW_AS_INACTIVE);

  return panel;
}

void BasePanelBrowserTest::ActivatePanel(Panel* panel) {
  // For certain window managers on Linux, the window activation/deactivation
  // signals might not be sent. To work around this, we explicitly deactivate
  // all other panels first.
#if defined(OS_LINUX)
  std::vector<Panel*> panels = PanelManager::GetInstance()->panels();
  for (std::vector<Panel*>::const_iterator iter = panels.begin();
       iter != panels.end(); ++iter) {
    Panel* current_panel = *iter;
    if (panel != current_panel)
      current_panel->Deactivate();
  }
#endif

  panel->Activate();
}

void BasePanelBrowserTest::DeactivatePanel(Panel* panel) {
#if defined(OS_LINUX)
  // For certain window managers on Linux, like icewm, panel activation and
  // deactivation notification might not get tiggered when non-panel window is
  // activated or deactivated. So we deactivate the panel directly.
  panel->Deactivate();
#else
  // Make the panel lose focus by activating the browser window. This is
  // because:
  // 1) On Windows, deactivating the panel window might cause the application
  //    to lose the foreground status. When this occurs, trying to activate
  //    the panel window again will not be allowed by the system.
  // 2) On MacOS, deactivating a window is not supported by Cocoa.
  browser()->window()->Activate();
#endif
}

// static
NativePanelTesting* BasePanelBrowserTest::CreateNativePanelTesting(
    Panel* panel) {
  return panel->native_panel()->CreateNativePanelTesting();
}

scoped_refptr<Extension> BasePanelBrowserTest::CreateExtension(
    const base::FilePath::StringType& path,
    extensions::Manifest::Location location,
    const DictionaryValue& extra_value) {
  extensions::ExtensionPrefs* extension_prefs =
      extensions::ExtensionPrefs::Get(browser()->profile());
  base::FilePath full_path = extension_prefs->install_directory().Append(path);

  scoped_ptr<DictionaryValue> input_value(extra_value.DeepCopy());
  input_value->SetString(extensions::manifest_keys::kVersion, "1.0.0.0");
  input_value->SetString(extensions::manifest_keys::kName, "Sample Extension");

  std::string error;
  scoped_refptr<Extension> extension = Extension::Create(
      full_path,  location, *input_value, Extension::NO_FLAGS, &error);
  EXPECT_TRUE(extension.get());
  EXPECT_STREQ("", error.c_str());
  browser()->profile()->GetExtensionService()->
      OnExtensionInstalled(extension.get(),
                           syncer::StringOrdinal(),
                           false /* no requirement errors */,
                           extensions::Blacklist::NOT_BLACKLISTED,
                           false /* don't wait for idle */);
  return extension;
}

void BasePanelBrowserTest::CloseWindowAndWait(Panel* panel) {
  // Closing a panel may involve several async tasks. Need to use
  // message pump and wait for the notification.
  PanelManager* manager = PanelManager::GetInstance();
  int panel_count = manager->num_panels();
  content::WindowedNotificationObserver signal(
      chrome::NOTIFICATION_PANEL_CLOSED,
      content::Source<Panel>(panel));
  panel->Close();
  signal.Wait();
  // Now we have one less panel.
  EXPECT_EQ(panel_count - 1, manager->num_panels());

#if defined(OS_MACOSX)
  // Mac window controllers may be autoreleased, and in the non-test
  // environment, may actually depend on the autorelease pool being recycled
  // with the run loop in order to perform important work. Replicate this in
  // the test environment.
  AutoreleasePool()->Recycle();

  // Make sure that everything has a chance to run.
  chrome::testing::NSRunLoopRunAllPending();
#endif  // OS_MACOSX
}

void BasePanelBrowserTest::MoveMouseAndWaitForExpansionStateChange(
    Panel* panel,
    const gfx::Point& position) {
  content::WindowedNotificationObserver signal(
      chrome::NOTIFICATION_PANEL_CHANGED_EXPANSION_STATE,
      content::Source<Panel>(panel));
  MoveMouse(position);
  signal.Wait();
}

void BasePanelBrowserTest::MoveMouse(const gfx::Point& position) {
  PanelManager::GetInstance()->mouse_watcher()->NotifyMouseMovement(position);
}

std::string BasePanelBrowserTest::MakePanelName(int index) {
  std::string panel_name("Panel");
  return panel_name + base::IntToString(index);
}

bool BasePanelBrowserTest::WmSupportWindowActivation() {
#if defined(OS_LINUX) && !defined(USE_AURA)
  return ui::ActiveWindowWatcherX::WMSupportsActivation();
#else
  return true;
#endif
}