// Copyright 2013 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 "ash/display/resolution_notification_controller.h"

#include "ash/display/display_manager.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "grit/ash_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/size.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/notification.h"
#include "ui/message_center/notification_list.h"

namespace ash {
namespace {

base::string16 ExpectedNotificationMessage(int64 display_id,
                                           const gfx::Size& new_resolution) {
  return l10n_util::GetStringFUTF16(
      IDS_ASH_STATUS_TRAY_DISPLAY_RESOLUTION_CHANGED,
      base::UTF8ToUTF16(
          Shell::GetInstance()->display_manager()->GetDisplayNameForId(
              display_id)),
      base::UTF8ToUTF16(new_resolution.ToString()));
}

base::string16 ExpectedFallbackNotificationMessage(
    int64 display_id,
    const gfx::Size& specified_resolution,
    const gfx::Size& fallback_resolution) {
  return l10n_util::GetStringFUTF16(
      IDS_ASH_STATUS_TRAY_DISPLAY_RESOLUTION_CHANGED_TO_UNSUPPORTED,
      base::UTF8ToUTF16(
          Shell::GetInstance()->display_manager()->GetDisplayNameForId(
              display_id)),
      base::UTF8ToUTF16(specified_resolution.ToString()),
      base::UTF8ToUTF16(fallback_resolution.ToString()));
}

}  // namespace

class ResolutionNotificationControllerTest : public ash::test::AshTestBase {
 public:
  ResolutionNotificationControllerTest()
      : accept_count_(0) {
  }

  virtual ~ResolutionNotificationControllerTest() {}

 protected:
  virtual void SetUp() OVERRIDE {
    ash::test::AshTestBase::SetUp();
    ResolutionNotificationController::SuppressTimerForTest();
  }

  void SetDisplayResolutionAndNotifyWithResolution(
      const gfx::Display& display,
      const gfx::Size& new_resolution,
      const gfx::Size& actual_new_resolution) {
    DisplayManager* display_manager = Shell::GetInstance()->display_manager();

    const DisplayInfo& info = display_manager->GetDisplayInfo(display.id());
    DisplayMode old_mode(info.size_in_pixel(),
                         60 /* refresh_rate */,
                         false /* interlaced */,
                         false /* native */);
    DisplayMode new_mode = old_mode;
    new_mode.size = new_resolution;

    if (display_manager->SetDisplayMode(display.id(), new_mode)) {
      controller()->PrepareNotification(
          display.id(),
          old_mode,
          new_mode,
          base::Bind(&ResolutionNotificationControllerTest::OnAccepted,
                     base::Unretained(this)));
    }

    // OnConfigurationChanged event won't be emitted in the test environment,
    // so invoke UpdateDisplay() to emit that event explicitly.
    std::vector<DisplayInfo> info_list;
    for (size_t i = 0; i < display_manager->GetNumDisplays(); ++i) {
      int64 id = display_manager->GetDisplayAt(i).id();
      DisplayInfo info = display_manager->GetDisplayInfo(id);
      if (display.id() == id) {
        gfx::Rect bounds = info.bounds_in_native();
        bounds.set_size(actual_new_resolution);
        info.SetBounds(bounds);
      }
      info_list.push_back(info);
    }
    display_manager->OnNativeDisplaysChanged(info_list);
    RunAllPendingInMessageLoop();
  }

  void SetDisplayResolutionAndNotify(const gfx::Display& display,
                                     const gfx::Size& new_resolution) {
    SetDisplayResolutionAndNotifyWithResolution(
        display, new_resolution, new_resolution);
  }

  static base::string16 GetNotificationMessage() {
    const message_center::NotificationList::Notifications& notifications =
        message_center::MessageCenter::Get()->GetVisibleNotifications();
    for (message_center::NotificationList::Notifications::const_iterator iter =
             notifications.begin(); iter != notifications.end(); ++iter) {
      if ((*iter)->id() == ResolutionNotificationController::kNotificationId)
        return (*iter)->title();
    }

    return base::string16();
  }

  static void ClickOnNotification() {
    message_center::MessageCenter::Get()->ClickOnNotification(
        ResolutionNotificationController::kNotificationId);
  }

  static void ClickOnNotificationButton(int index) {
    message_center::MessageCenter::Get()->ClickOnNotificationButton(
        ResolutionNotificationController::kNotificationId, index);
  }

  static void CloseNotification() {
    message_center::MessageCenter::Get()->RemoveNotification(
        ResolutionNotificationController::kNotificationId, true /* by_user */);
  }

  static bool IsNotificationVisible() {
    return message_center::MessageCenter::Get()->FindVisibleNotificationById(
        ResolutionNotificationController::kNotificationId);
  }

  static void TickTimer() {
    controller()->OnTimerTick();
  }

  static ResolutionNotificationController* controller() {
    return Shell::GetInstance()->resolution_notification_controller();
  }

  int accept_count() const {
    return accept_count_;
  }

 private:
  void OnAccepted() {
    EXPECT_FALSE(controller()->DoesNotificationTimeout());
    accept_count_++;
  }

  int accept_count_;

  DISALLOW_COPY_AND_ASSIGN(ResolutionNotificationControllerTest);
};

// Basic behaviors and verifies it doesn't cause crashes.
TEST_F(ResolutionNotificationControllerTest, Basic) {
  if (!SupportsMultipleDisplays())
    return;

  UpdateDisplay("300x300#300x300%57|200x200%58,250x250#250x250%59|200x200%60");
  int64 id2 = ash::ScreenUtil::GetSecondaryDisplay().id();
  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();
  ASSERT_EQ(0, accept_count());
  EXPECT_FALSE(IsNotificationVisible());

  // Changes the resolution and apply the result.
  SetDisplayResolutionAndNotify(
      ScreenUtil::GetSecondaryDisplay(), gfx::Size(200, 200));
  EXPECT_TRUE(IsNotificationVisible());
  EXPECT_FALSE(controller()->DoesNotificationTimeout());
  EXPECT_EQ(ExpectedNotificationMessage(id2, gfx::Size(200, 200)),
            GetNotificationMessage());
  DisplayMode mode;
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(60.0, mode.refresh_rate);

  // Click the revert button, which reverts to the best resolution.
  ClickOnNotificationButton(0);
  RunAllPendingInMessageLoop();
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(0, accept_count());
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("250x250", mode.size.ToString());
  EXPECT_EQ(59.0, mode.refresh_rate);
}

TEST_F(ResolutionNotificationControllerTest, ClickMeansAccept) {
  if (!SupportsMultipleDisplays())
    return;

  UpdateDisplay("300x300#300x300%57|200x200%58,250x250#250x250%59|200x200%60");
  int64 id2 = ash::ScreenUtil::GetSecondaryDisplay().id();
  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();
  ASSERT_EQ(0, accept_count());
  EXPECT_FALSE(IsNotificationVisible());

  // Changes the resolution and apply the result.
  SetDisplayResolutionAndNotify(
      ScreenUtil::GetSecondaryDisplay(), gfx::Size(200, 200));
  EXPECT_TRUE(IsNotificationVisible());
  EXPECT_FALSE(controller()->DoesNotificationTimeout());
  DisplayMode mode;
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(60.0, mode.refresh_rate);

  // Click the revert button, which reverts the resolution.
  ClickOnNotification();
  RunAllPendingInMessageLoop();
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(1, accept_count());
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(60.0, mode.refresh_rate);
}

TEST_F(ResolutionNotificationControllerTest, AcceptButton) {
  if (!SupportsMultipleDisplays())
    return;

  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();

  UpdateDisplay("300x300#300x300%59|200x200%60");
  const gfx::Display& display = ash::Shell::GetScreen()->GetPrimaryDisplay();
  SetDisplayResolutionAndNotify(display, gfx::Size(200, 200));
  EXPECT_TRUE(IsNotificationVisible());

  // If there's a single display only, it will have timeout and the first button
  // becomes accept.
  EXPECT_TRUE(controller()->DoesNotificationTimeout());
  ClickOnNotificationButton(0);
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(1, accept_count());
  DisplayMode mode;
  EXPECT_TRUE(
      display_manager->GetSelectedModeForDisplayId(display.id(), &mode));
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(60.0f, mode.refresh_rate);

  // In that case the second button is revert.
  UpdateDisplay("300x300#300x300%59|200x200%60");
  SetDisplayResolutionAndNotify(display, gfx::Size(200, 200));
  EXPECT_TRUE(IsNotificationVisible());

  EXPECT_TRUE(controller()->DoesNotificationTimeout());
  ClickOnNotificationButton(1);
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(1, accept_count());
  EXPECT_TRUE(
      display_manager->GetSelectedModeForDisplayId(display.id(), &mode));
  EXPECT_EQ("300x300", mode.size.ToString());
  EXPECT_EQ(59.0f, mode.refresh_rate);
}

TEST_F(ResolutionNotificationControllerTest, Close) {
  if (!SupportsMultipleDisplays())
    return;

  UpdateDisplay("100x100,150x150#150x150%59|200x200%60");
  int64 id2 = ash::ScreenUtil::GetSecondaryDisplay().id();
  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();
  ASSERT_EQ(0, accept_count());
  EXPECT_FALSE(IsNotificationVisible());

  // Changes the resolution and apply the result.
  SetDisplayResolutionAndNotify(
      ScreenUtil::GetSecondaryDisplay(), gfx::Size(200, 200));
  EXPECT_TRUE(IsNotificationVisible());
  EXPECT_FALSE(controller()->DoesNotificationTimeout());
  DisplayMode mode;
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(60.0f, mode.refresh_rate);

  // Close the notification (imitates clicking [x] button). Also verifies if
  // this does not cause a crash.  See crbug.com/271784
  CloseNotification();
  RunAllPendingInMessageLoop();
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(1, accept_count());
}

TEST_F(ResolutionNotificationControllerTest, Timeout) {
  if (!SupportsMultipleDisplays())
    return;

  UpdateDisplay("300x300#300x300%59|200x200%60");
  const gfx::Display& display = ash::Shell::GetScreen()->GetPrimaryDisplay();
  SetDisplayResolutionAndNotify(display, gfx::Size(200, 200));

  for (int i = 0; i < ResolutionNotificationController::kTimeoutInSec; ++i) {
    EXPECT_TRUE(IsNotificationVisible()) << "notification is closed after "
                                         << i << "-th timer tick";
    TickTimer();
    RunAllPendingInMessageLoop();
  }
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(0, accept_count());
  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();
  DisplayMode mode;
  EXPECT_TRUE(
      display_manager->GetSelectedModeForDisplayId(display.id(), &mode));
  EXPECT_EQ("300x300", mode.size.ToString());
  EXPECT_EQ(59.0f, mode.refresh_rate);
}

TEST_F(ResolutionNotificationControllerTest, DisplayDisconnected) {
  if (!SupportsMultipleDisplays())
    return;

  UpdateDisplay("300x300#300x300%56|200x200%57,"
                "200x200#250x250%58|200x200%59|100x100%60");
  int64 id2 = ash::ScreenUtil::GetSecondaryDisplay().id();
  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();
  SetDisplayResolutionAndNotify(
      ScreenUtil::GetSecondaryDisplay(), gfx::Size(100, 100));
  ASSERT_TRUE(IsNotificationVisible());

  // Disconnects the secondary display and verifies it doesn't cause crashes.
  UpdateDisplay("300x300#300x300%56|200x200%57");
  RunAllPendingInMessageLoop();
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(0, accept_count());
  DisplayMode mode;
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  gfx::Size resolution;
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(59.0f, mode.refresh_rate);
}

TEST_F(ResolutionNotificationControllerTest, MultipleResolutionChange) {
  if (!SupportsMultipleDisplays())
    return;

  UpdateDisplay("300x300#300x300%56|200x200%57,"
                "250x250#250x250%58|200x200%59");
  int64 id2 = ash::ScreenUtil::GetSecondaryDisplay().id();
  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();

  SetDisplayResolutionAndNotify(
      ScreenUtil::GetSecondaryDisplay(), gfx::Size(200, 200));
  EXPECT_TRUE(IsNotificationVisible());
  EXPECT_FALSE(controller()->DoesNotificationTimeout());
  DisplayMode mode;
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(59.0f, mode.refresh_rate);

  // Invokes SetDisplayResolutionAndNotify during the previous notification is
  // visible.
  SetDisplayResolutionAndNotify(
      ScreenUtil::GetSecondaryDisplay(), gfx::Size(250, 250));
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("250x250", mode.size.ToString());
  EXPECT_EQ(58.0f, mode.refresh_rate);

  // Then, click the revert button. Although |old_resolution| for the second
  // SetDisplayResolutionAndNotify is 200x200, it should revert to the original
  // size 250x250.
  ClickOnNotificationButton(0);
  RunAllPendingInMessageLoop();
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(0, accept_count());
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("250x250", mode.size.ToString());
  EXPECT_EQ(58.0f, mode.refresh_rate);
}

TEST_F(ResolutionNotificationControllerTest, Fallback) {
  if (!SupportsMultipleDisplays())
    return;

  UpdateDisplay("300x300#300x300%56|200x200%57,"
                "250x250#250x250%58|220x220%59|200x200%60");
  int64 id2 = ash::ScreenUtil::GetSecondaryDisplay().id();
  ash::DisplayManager* display_manager =
      ash::Shell::GetInstance()->display_manager();
  ASSERT_EQ(0, accept_count());
  EXPECT_FALSE(IsNotificationVisible());

  // Changes the resolution and apply the result.
  SetDisplayResolutionAndNotifyWithResolution(
      ScreenUtil::GetSecondaryDisplay(),
      gfx::Size(220, 220),
      gfx::Size(200, 200));
  EXPECT_TRUE(IsNotificationVisible());
  EXPECT_FALSE(controller()->DoesNotificationTimeout());
  EXPECT_EQ(
      ExpectedFallbackNotificationMessage(
          id2, gfx::Size(220, 220), gfx::Size(200, 200)),
      GetNotificationMessage());
  DisplayMode mode;
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("200x200", mode.size.ToString());
  EXPECT_EQ(60.0f, mode.refresh_rate);

  // Click the revert button, which reverts to the best resolution.
  ClickOnNotificationButton(0);
  RunAllPendingInMessageLoop();
  EXPECT_FALSE(IsNotificationVisible());
  EXPECT_EQ(0, accept_count());
  EXPECT_TRUE(display_manager->GetSelectedModeForDisplayId(id2, &mode));
  EXPECT_EQ("250x250", mode.size.ToString());
  EXPECT_EQ(58.0f, mode.refresh_rate);
}

}  // namespace ash
