// Copyright (c) 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.

#import "ui/message_center/cocoa/popup_collection.h"

#include <memory>
#include <utility>

#include "base/mac/scoped_nsobject.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "ui/gfx/test/ui_cocoa_test_helper.h"
#import "ui/message_center/cocoa/notification_controller.h"
#import "ui/message_center/cocoa/popup_controller.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/notification.h"

using base::ASCIIToUTF16;

namespace message_center {

class PopupCollectionTest : public ui::CocoaTest {
 public:
  PopupCollectionTest() {
    message_center::MessageCenter::Initialize();
    center_ = message_center::MessageCenter::Get();
    collection_.reset(
        [[MCPopupCollection alloc] initWithMessageCenter:center_]);
    [collection_ setAnimationDuration:0.001];
    [collection_ setAnimationEndedCallback:^{
        if (nested_run_loop_.get())
          nested_run_loop_->Quit();
    }];
  }

  void TearDown() override {
    collection_.reset();  // Close all popups.
    ui::CocoaTest::TearDown();
  }

  ~PopupCollectionTest() override { message_center::MessageCenter::Shutdown(); }

  message_center::NotifierId DummyNotifierId() {
    return message_center::NotifierId();
  }

  void AddThreeNotifications() {
    std::unique_ptr<message_center::Notification> notification;
    notification.reset(new message_center::Notification(
        message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
        ASCIIToUTF16("This is the first notification to"
                     " be displayed"),
        gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
        message_center::RichNotificationData(), NULL));
    center_->AddNotification(std::move(notification));

    notification.reset(new message_center::Notification(
        message_center::NOTIFICATION_TYPE_SIMPLE, "2", ASCIIToUTF16("Two"),
        ASCIIToUTF16("This is the second notification."), gfx::Image(),
        base::string16(), GURL(), DummyNotifierId(),
        message_center::RichNotificationData(), NULL));
    center_->AddNotification(std::move(notification));

    notification.reset(new message_center::Notification(
        message_center::NOTIFICATION_TYPE_SIMPLE, "3", ASCIIToUTF16("Three"),
        ASCIIToUTF16("This is the third notification "
                     "that has a much longer body "
                     "than the other notifications. It "
                     "may not fit on the screen if we "
                     "set the screen size too small or "
                     "if the notification is way too big"),
        gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
        message_center::RichNotificationData(), NULL));
    center_->AddNotification(std::move(notification));
    WaitForAnimationEnded();
  }

  bool CheckSpacingBetween(MCPopupController* upper, MCPopupController* lower) {
    CGFloat minY = NSMinY([[upper window] frame]);
    CGFloat maxY = NSMaxY([[lower window] frame]);
    CGFloat delta = minY - maxY;
    EXPECT_EQ(message_center::kMarginBetweenItems, delta);
    return delta == message_center::kMarginBetweenItems;
  }

  void WaitForAnimationEnded() {
    if (![collection_ isAnimating])
      return;
    nested_run_loop_.reset(new base::RunLoop());
    nested_run_loop_->Run();
    nested_run_loop_.reset();
  }

  base::MessageLoopForUI message_loop_;
  std::unique_ptr<base::RunLoop> nested_run_loop_;
  message_center::MessageCenter* center_;
  base::scoped_nsobject<MCPopupCollection> collection_;
};

TEST_F(PopupCollectionTest, AddThreeCloseOne) {
  EXPECT_EQ(0u, [[collection_ popups] count]);
  AddThreeNotifications();
  EXPECT_EQ(3u, [[collection_ popups] count]);

  center_->RemoveNotification("2", true);
  WaitForAnimationEnded();
  EXPECT_EQ(2u, [[collection_ popups] count]);
}

TEST_F(PopupCollectionTest, AttemptFourOneOffscreen) {
  [collection_ setScreenFrame:NSMakeRect(0, 0, 800, 300)];

  EXPECT_EQ(0u, [[collection_ popups] count]);
  AddThreeNotifications();
  EXPECT_EQ(2u, [[collection_ popups] count]);  // "3" does not fit on screen.

  std::unique_ptr<message_center::Notification> notification;

  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "4", ASCIIToUTF16("Four"),
      ASCIIToUTF16("This is the fourth notification."), gfx::Image(),
      base::string16(), GURL(), DummyNotifierId(),
      message_center::RichNotificationData(), NULL));
  center_->AddNotification(std::move(notification));
  WaitForAnimationEnded();

  // Remove "1" and "3" should fit on screen.
  center_->RemoveNotification("1", true);
  WaitForAnimationEnded();
  ASSERT_EQ(2u, [[collection_ popups] count]);

  EXPECT_EQ("2", [[[collection_ popups] objectAtIndex:0] notificationID]);
  EXPECT_EQ("3", [[[collection_ popups] objectAtIndex:1] notificationID]);

  // Remove "2" and "4" should fit on screen.
  center_->RemoveNotification("2", true);
  WaitForAnimationEnded();
  ASSERT_EQ(2u, [[collection_ popups] count]);

  EXPECT_EQ("3", [[[collection_ popups] objectAtIndex:0] notificationID]);
  EXPECT_EQ("4", [[[collection_ popups] objectAtIndex:1] notificationID]);
}

TEST_F(PopupCollectionTest, LayoutSpacing) {
  const CGFloat kScreenSize = 500;
  [collection_ setScreenFrame:NSMakeRect(0, 0, kScreenSize, kScreenSize)];

  AddThreeNotifications();
  NSArray* popups = [collection_ popups];

  EXPECT_EQ(message_center::kMarginBetweenItems,
            kScreenSize - NSMaxY([[[popups objectAtIndex:0] window] frame]));

  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
                                  [popups objectAtIndex:1]));
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
                                  [popups objectAtIndex:2]));

  // Set priority so that kMaxVisiblePopupNotifications does not hide it.
  message_center::RichNotificationData optional;
  optional.priority = message_center::HIGH_PRIORITY;
  std::unique_ptr<message_center::Notification> notification;
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "4", ASCIIToUTF16("Four"),
      ASCIIToUTF16("This is the fourth notification."), gfx::Image(),
      base::string16(), GURL(), DummyNotifierId(), optional, NULL));
  center_->AddNotification(std::move(notification));
  WaitForAnimationEnded();
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:2],
                                  [popups objectAtIndex:3]));

  // Remove "2".
  center_->RemoveNotification("2", true);
  WaitForAnimationEnded();
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
                                  [popups objectAtIndex:1]));
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
                                  [popups objectAtIndex:2]));

  // Remove "1".
  center_->RemoveNotification("2", true);
  WaitForAnimationEnded();
  EXPECT_EQ(message_center::kMarginBetweenItems,
            kScreenSize - NSMaxY([[[popups objectAtIndex:0] window] frame]));
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
                                  [popups objectAtIndex:1]));
}

TEST_F(PopupCollectionTest, TinyScreen) {
  [collection_ setScreenFrame:NSMakeRect(0, 0, 800, 100)];

  EXPECT_EQ(0u, [[collection_ popups] count]);
  std::unique_ptr<message_center::Notification> notification;
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
      ASCIIToUTF16("This is the first notification to"
                   " be displayed"),
      gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
      message_center::RichNotificationData(), NULL));
  center_->AddNotification(std::move(notification));
  WaitForAnimationEnded();
  EXPECT_EQ(1u, [[collection_ popups] count]);

  // Now give the notification a longer message so that it no longer fits.
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
      ASCIIToUTF16("This is now a very very very very "
                   "very very very very very very very "
                   "very very very very very very very "
                   "very very very very very very very "
                   "very very very very very very very "
                   "very very very very very very very "
                   "very very very very very very very "
                   "long notification."),
      gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
      message_center::RichNotificationData(), NULL));
  center_->UpdateNotification("1", std::move(notification));
  WaitForAnimationEnded();
  EXPECT_EQ(0u, [[collection_ popups] count]);
}

TEST_F(PopupCollectionTest, UpdateIconAndBody) {
  AddThreeNotifications();
  NSArray* popups = [collection_ popups];

  EXPECT_EQ(3u, [popups count]);

  // Update "2" icon.
  MCNotificationController* controller =
      [[popups objectAtIndex:1] notificationController];
  EXPECT_FALSE([[controller iconView] image]);
  center_->SetNotificationIcon("2",
      gfx::Image([[NSImage imageNamed:NSImageNameUser] retain]));
  WaitForAnimationEnded();
  EXPECT_TRUE([[controller iconView] image]);

  EXPECT_EQ(3u, [popups count]);
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
                                  [popups objectAtIndex:1]));
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
                                  [popups objectAtIndex:2]));

  // Replace "1".
  controller = [[popups objectAtIndex:0] notificationController];
  NSRect old_frame = [[controller view] frame];
  std::unique_ptr<message_center::Notification> notification;
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "1",
      ASCIIToUTF16("One is going to get a much longer "
                   "title than it previously had."),
      ASCIIToUTF16("This is the first notification to "
                   "be displayed, but it will also be "
                   "updated to have a significantly "
                   "longer body"),
      gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
      message_center::RichNotificationData(), NULL));
  center_->AddNotification(std::move(notification));
  WaitForAnimationEnded();
  EXPECT_GT(NSHeight([[controller view] frame]), NSHeight(old_frame));

  // Test updated spacing.
  EXPECT_EQ(3u, [popups count]);
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
                                  [popups objectAtIndex:1]));
  EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
                                  [popups objectAtIndex:2]));
  EXPECT_EQ("1", [[popups objectAtIndex:0] notificationID]);
  EXPECT_EQ("2", [[popups objectAtIndex:1] notificationID]);
  EXPECT_EQ("3", [[popups objectAtIndex:2] notificationID]);
}

TEST_F(PopupCollectionTest, UpdatePriority) {
  std::unique_ptr<message_center::Notification> notification;
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
      ASCIIToUTF16("This notification should not yet toast."), gfx::Image(),
      base::string16(), GURL(), DummyNotifierId(),
      message_center::RichNotificationData(), NULL));
  notification->set_priority(-1);

  center_->AddNotification(std::move(notification));
  WaitForAnimationEnded();
  NSArray* popups = [collection_ popups];
  EXPECT_EQ(0u, [popups count]);

  // Raise priority -1 to 1. Notification should display.
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
      ASCIIToUTF16("This notification should now toast"), gfx::Image(),
      base::string16(), GURL(), DummyNotifierId(),
      message_center::RichNotificationData(), NULL));
  notification->set_priority(1);

  center_->UpdateNotification("1", std::move(notification));
  WaitForAnimationEnded();
  EXPECT_EQ(1u, [popups count]);
}

TEST_F(PopupCollectionTest, CloseCollectionBeforeNewPopupAnimationEnds) {
  // Add a notification and don't wait for the animation to finish.
  std::unique_ptr<message_center::Notification> notification;
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
      ASCIIToUTF16("This is the first notification to"
                   " be displayed"),
      gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
      message_center::RichNotificationData(), NULL));
  center_->AddNotification(std::move(notification));

  // Release the popup collection before the animation ends. No crash should
  // be expected.
  collection_.reset();
}

TEST_F(PopupCollectionTest, CloseCollectionBeforeClosePopupAnimationEnds) {
  AddThreeNotifications();

  // Remove a notification and don't wait for the animation to finish.
  center_->RemoveNotification("1", true);

  // Release the popup collection before the animation ends. No crash should
  // be expected.
  collection_.reset();
}

TEST_F(PopupCollectionTest, CloseCollectionBeforeUpdatePopupAnimationEnds) {
  AddThreeNotifications();

  // Update a notification and don't wait for the animation to finish.
  std::unique_ptr<message_center::Notification> notification;
  notification.reset(new message_center::Notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
      ASCIIToUTF16("New message."), gfx::Image(), base::string16(), GURL(),
      DummyNotifierId(), message_center::RichNotificationData(), NULL));
  center_->UpdateNotification("1", std::move(notification));

  // Release the popup collection before the animation ends. No crash should
  // be expected.
  collection_.reset();
}

}  // namespace message_center
