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

#import <Cocoa/Cocoa.h>

#include "base/basictypes.h"
#include "base/memory/scoped_nsobject.h"
#include "base/string16.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h"
#include "chrome/browser/ui/cocoa/browser_window_controller.h"
#include "chrome/browser/ui/cocoa/cocoa_profile_test.h"
#import "chrome/browser/ui/cocoa/info_bubble_window.h"
#include "content/public/browser/notification_service.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "testing/platform_test.h"

using content::WebContents;

// Watch for bookmark pulse notifications so we can confirm they were sent.
@interface BookmarkPulseObserver : NSObject {
  int notifications_;
}
@property (assign, nonatomic) int notifications;
@end


@implementation BookmarkPulseObserver

@synthesize notifications = notifications_;

- (id)init {
  if ((self = [super init])) {
    [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(pulseBookmarkNotification:)
             name:bookmark_button::kPulseBookmarkButtonNotification
           object:nil];
  }
  return self;
}

- (void)pulseBookmarkNotification:(NSNotificationCenter *)notification {
  notifications_++;
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [super dealloc];
}

@end


namespace {

class BookmarkBubbleControllerTest : public CocoaProfileTest {
 public:
  static int edits_;
  BookmarkBubbleController* controller_;

  BookmarkBubbleControllerTest() : controller_(nil) {
    edits_ = 0;
  }

  virtual void TearDown() {
    [controller_ close];
    CocoaProfileTest::TearDown();
  }

  // Returns a controller but ownership not transferred.
  // Only one of these will be valid at a time.
  BookmarkBubbleController* ControllerForNode(const BookmarkNode* node) {
    if (controller_ && !IsWindowClosing()) {
      [controller_ close];
      controller_ = nil;
    }
    controller_ = [[BookmarkBubbleController alloc]
                      initWithParentWindow:test_window()
                                     model:BookmarkModelFactory::GetForProfile(
                                         profile())
                                      node:node
                         alreadyBookmarked:YES];
    EXPECT_TRUE([controller_ window]);
    // The window must be gone or we'll fail a unit test with windows left open.
    [static_cast<InfoBubbleWindow*>([controller_ window])
        setAllowedAnimations:info_bubble::kAnimateNone];
    [controller_ showWindow:nil];
    return controller_;
  }

  BookmarkModel* GetBookmarkModel() {
    return BookmarkModelFactory::GetForProfile(profile());
  }

  bool IsWindowClosing() {
    return [static_cast<InfoBubbleWindow*>([controller_ window]) isClosing];
  }
};

// static
int BookmarkBubbleControllerTest::edits_;

// Confirm basics about the bubble window (e.g. that it is inside the
// parent window)
TEST_F(BookmarkBubbleControllerTest, TestBubbleWindow) {
  BookmarkModel* model = GetBookmarkModel();
  const BookmarkNode* node = model->AddURL(model->bookmark_bar_node(),
                                           0,
                                           ASCIIToUTF16("Bookie markie title"),
                                           GURL("http://www.google.com"));
  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);
  NSWindow* window = [controller window];
  EXPECT_TRUE(window);
  EXPECT_TRUE(NSContainsRect([test_window() frame],
                             [window frame]));
}

// Test that we can handle closing the parent window
TEST_F(BookmarkBubbleControllerTest, TestClosingParentWindow) {
  BookmarkModel* model = GetBookmarkModel();
  const BookmarkNode* node = model->AddURL(model->bookmark_bar_node(),
                                           0,
                                           ASCIIToUTF16("Bookie markie title"),
                                           GURL("http://www.google.com"));
  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);
  NSWindow* window = [controller window];
  EXPECT_TRUE(window);
  base::mac::ScopedNSAutoreleasePool pool;
  [test_window() performClose:NSApp];
}


// Confirm population of folder list
TEST_F(BookmarkBubbleControllerTest, TestFillInFolder) {
  // Create some folders, including a nested folder
  BookmarkModel* model = GetBookmarkModel();
  EXPECT_TRUE(model);
  const BookmarkNode* bookmarkBarNode = model->bookmark_bar_node();
  EXPECT_TRUE(bookmarkBarNode);
  const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0,
                                               ASCIIToUTF16("one"));
  EXPECT_TRUE(node1);
  const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 1,
                                               ASCIIToUTF16("two"));
  EXPECT_TRUE(node2);
  const BookmarkNode* node3 = model->AddFolder(bookmarkBarNode, 2,
                                               ASCIIToUTF16("three"));
  EXPECT_TRUE(node3);
  const BookmarkNode* node4 = model->AddFolder(node2, 0, ASCIIToUTF16("sub"));
  EXPECT_TRUE(node4);
  const BookmarkNode* node5 = model->AddURL(node1, 0, ASCIIToUTF16("title1"),
                                            GURL("http://www.google.com"));
  EXPECT_TRUE(node5);
  const BookmarkNode* node6 = model->AddURL(node3, 0, ASCIIToUTF16("title2"),
                                            GURL("http://www.google.com"));
  EXPECT_TRUE(node6);
  const BookmarkNode* node7 = model->AddURL(
      node4, 0, ASCIIToUTF16("title3"), GURL("http://www.google.com/reader"));
  EXPECT_TRUE(node7);

  BookmarkBubbleController* controller = ControllerForNode(node4);
  EXPECT_TRUE(controller);

  NSArray* titles =
      [[[controller folderPopUpButton] itemArray] valueForKey:@"title"];
  EXPECT_TRUE([titles containsObject:@"one"]);
  EXPECT_TRUE([titles containsObject:@"two"]);
  EXPECT_TRUE([titles containsObject:@"three"]);
  EXPECT_TRUE([titles containsObject:@"sub"]);
  EXPECT_FALSE([titles containsObject:@"title1"]);
  EXPECT_FALSE([titles containsObject:@"title2"]);


  // Verify that the top level folders are displayed correctly.
  EXPECT_TRUE([titles containsObject:@"Other Bookmarks"]);
  EXPECT_TRUE([titles containsObject:@"Bookmarks Bar"]);
  if (model->mobile_node()->IsVisible()) {
    EXPECT_TRUE([titles containsObject:@"Mobile Bookmarks"]);
  } else {
    EXPECT_FALSE([titles containsObject:@"Mobile Bookmarks"]);
  }
}

// Confirm ability to handle folders with blank name.
TEST_F(BookmarkBubbleControllerTest, TestFolderWithBlankName) {
  // Create some folders, including a nested folder
  BookmarkModel* model = GetBookmarkModel();
  EXPECT_TRUE(model);
  const BookmarkNode* bookmarkBarNode = model->bookmark_bar_node();
  EXPECT_TRUE(bookmarkBarNode);
  const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0,
                                               ASCIIToUTF16("one"));
  EXPECT_TRUE(node1);
  const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 1,
                                               string16());
  EXPECT_TRUE(node2);
  const BookmarkNode* node3 = model->AddFolder(bookmarkBarNode, 2,
                                               ASCIIToUTF16("three"));
  EXPECT_TRUE(node3);
  const BookmarkNode* node2_1 = model->AddURL(node2, 0, ASCIIToUTF16("title1"),
                                              GURL("http://www.google.com"));
  EXPECT_TRUE(node2_1);

  BookmarkBubbleController* controller = ControllerForNode(node1);
  EXPECT_TRUE(controller);

  // One of the items should be blank and its node should be node2.
  NSArray* items = [[controller folderPopUpButton] itemArray];
  EXPECT_GT([items count], 4U);
  BOOL blankFolderFound = NO;
  for (NSMenuItem* item in [[controller folderPopUpButton] itemArray]) {
    if ([[item title] length] == 0 &&
        static_cast<const BookmarkNode*>([[item representedObject]
                                          pointerValue]) == node2) {
      blankFolderFound = YES;
      break;
    }
  }
  EXPECT_TRUE(blankFolderFound);
}


// Click on edit; bubble gets closed.
TEST_F(BookmarkBubbleControllerTest, TestEdit) {
  BookmarkModel* model = GetBookmarkModel();
  const BookmarkNode* node = model->AddURL(model->bookmark_bar_node(),
                                           0,
                                           ASCIIToUTF16("Bookie markie title"),
                                           GURL("http://www.google.com"));
  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);

  EXPECT_EQ(edits_, 0);
  EXPECT_FALSE(IsWindowClosing());
  [controller edit:controller];
  EXPECT_EQ(edits_, 1);
  EXPECT_TRUE(IsWindowClosing());
}

// CallClose; bubble gets closed.
// Also confirm pulse notifications get sent.
TEST_F(BookmarkBubbleControllerTest, TestClose) {
    BookmarkModel* model = GetBookmarkModel();
    const BookmarkNode* node = model->AddURL(
        model->bookmark_bar_node(), 0, ASCIIToUTF16("Bookie markie title"),
        GURL("http://www.google.com"));
  EXPECT_EQ(edits_, 0);

  scoped_nsobject<BookmarkPulseObserver> observer([[BookmarkPulseObserver alloc]
                                                    init]);
  EXPECT_EQ([observer notifications], 0);
  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);
  EXPECT_FALSE(IsWindowClosing());
  EXPECT_EQ([observer notifications], 1);
  [controller ok:controller];
  EXPECT_EQ(edits_, 0);
  EXPECT_TRUE(IsWindowClosing());
  EXPECT_EQ([observer notifications], 2);
}

// User changes title and parent folder in the UI
TEST_F(BookmarkBubbleControllerTest, TestUserEdit) {
  BookmarkModel* model = GetBookmarkModel();
  EXPECT_TRUE(model);
  const BookmarkNode* bookmarkBarNode = model->bookmark_bar_node();
  EXPECT_TRUE(bookmarkBarNode);
  const BookmarkNode* node = model->AddURL(bookmarkBarNode,
                                           0,
                                           ASCIIToUTF16("short-title"),
                                           GURL("http://www.google.com"));
  const BookmarkNode* grandma = model->AddFolder(bookmarkBarNode, 0,
                                                 ASCIIToUTF16("grandma"));
  EXPECT_TRUE(grandma);
  const BookmarkNode* grandpa = model->AddFolder(bookmarkBarNode, 0,
                                                 ASCIIToUTF16("grandpa"));
  EXPECT_TRUE(grandpa);

  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);

  // simulate a user edit
  [controller setTitle:@"oops" parentFolder:grandma];
  [controller edit:controller];

  // Make sure bookmark has changed
  EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("oops"));
  EXPECT_EQ(node->parent()->GetTitle(), ASCIIToUTF16("grandma"));
}

// Confirm happiness with parent nodes that have the same name.
TEST_F(BookmarkBubbleControllerTest, TestNewParentSameName) {
  BookmarkModel* model = GetBookmarkModel();
  EXPECT_TRUE(model);
  const BookmarkNode* bookmarkBarNode = model->bookmark_bar_node();
  EXPECT_TRUE(bookmarkBarNode);
  for (int i=0; i<2; i++) {
    const BookmarkNode* node = model->AddURL(bookmarkBarNode,
                                             0,
                                             ASCIIToUTF16("short-title"),
                                             GURL("http://www.google.com"));
    EXPECT_TRUE(node);
    const BookmarkNode* folder = model->AddFolder(bookmarkBarNode, 0,
                                                 ASCIIToUTF16("NAME"));
    EXPECT_TRUE(folder);
    folder = model->AddFolder(bookmarkBarNode, 0, ASCIIToUTF16("NAME"));
    EXPECT_TRUE(folder);
    folder = model->AddFolder(bookmarkBarNode, 0, ASCIIToUTF16("NAME"));
    EXPECT_TRUE(folder);
    BookmarkBubbleController* controller = ControllerForNode(node);
    EXPECT_TRUE(controller);

    // simulate a user edit
    [controller setParentFolderSelection:bookmarkBarNode->GetChild(i)];
    [controller edit:controller];

    // Make sure bookmark has changed, and that the parent is what we
    // expect.  This proves nobody did searching based on name.
    EXPECT_EQ(node->parent(), bookmarkBarNode->GetChild(i));
  }
}

// Confirm happiness with nodes with the same Name
TEST_F(BookmarkBubbleControllerTest, TestDuplicateNodeNames) {
  BookmarkModel* model = GetBookmarkModel();
  const BookmarkNode* bookmarkBarNode = model->bookmark_bar_node();
  EXPECT_TRUE(bookmarkBarNode);
  const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0,
                                               ASCIIToUTF16("NAME"));
  EXPECT_TRUE(node1);
  const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 0,
                                               ASCIIToUTF16("NAME"));
  EXPECT_TRUE(node2);
  BookmarkBubbleController* controller = ControllerForNode(bookmarkBarNode);
  EXPECT_TRUE(controller);

  NSPopUpButton* button = [controller folderPopUpButton];
  [controller setParentFolderSelection:node1];
  NSMenuItem* item = [button selectedItem];
  id itemObject = [item representedObject];
  EXPECT_NSEQ([NSValue valueWithPointer:node1], itemObject);
  [controller setParentFolderSelection:node2];
  item = [button selectedItem];
  itemObject = [item representedObject];
  EXPECT_NSEQ([NSValue valueWithPointer:node2], itemObject);
}

// Click the "remove" button
TEST_F(BookmarkBubbleControllerTest, TestRemove) {
  BookmarkModel* model = GetBookmarkModel();
  GURL gurl("http://www.google.com");
  const BookmarkNode* node = model->AddURL(model->bookmark_bar_node(),
                                           0,
                                           ASCIIToUTF16("Bookie markie title"),
                                           gurl);
  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);
  EXPECT_TRUE(model->IsBookmarked(gurl));

  [controller remove:controller];
  EXPECT_FALSE(model->IsBookmarked(gurl));
  EXPECT_TRUE(IsWindowClosing());
}

// Confirm picking "choose another folder" caused edit: to be called.
TEST_F(BookmarkBubbleControllerTest, PopUpSelectionChanged) {
  BookmarkModel* model = GetBookmarkModel();
  GURL gurl("http://www.google.com");
  const BookmarkNode* node = model->AddURL(model->bookmark_bar_node(),
                                           0, ASCIIToUTF16("super-title"),
                                           gurl);
  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);

  NSPopUpButton* button = [controller folderPopUpButton];
  [button selectItemWithTitle:[[controller class] chooseAnotherFolderString]];
  EXPECT_EQ(edits_, 0);
  [button sendAction:[button action] to:[button target]];
  EXPECT_EQ(edits_, 1);
}

// Create a controller that simulates the bookmark just now being created by
// the user clicking the star, then sending the "cancel" command to represent
// them pressing escape. The bookmark should not be there.
TEST_F(BookmarkBubbleControllerTest, EscapeRemovesNewBookmark) {
  BookmarkModel* model = GetBookmarkModel();
  GURL gurl("http://www.google.com");
  const BookmarkNode* node = model->AddURL(model->bookmark_bar_node(),
                                           0,
                                           ASCIIToUTF16("Bookie markie title"),
                                           gurl);
  BookmarkBubbleController* controller =
      [[BookmarkBubbleController alloc]
          initWithParentWindow:test_window()
                         model:BookmarkModelFactory::GetForProfile(profile())
                          node:node
             alreadyBookmarked:NO];  // The last param is the key difference.
  EXPECT_TRUE([controller window]);
  // Calls release on controller.
  [controller cancel:nil];
  EXPECT_FALSE(model->IsBookmarked(gurl));
}

// Create a controller where the bookmark already existed prior to clicking
// the star and test that sending a cancel command doesn't change the state
// of the bookmark.
TEST_F(BookmarkBubbleControllerTest, EscapeDoesntTouchExistingBookmark) {
  BookmarkModel* model = GetBookmarkModel();
  GURL gurl("http://www.google.com");
  const BookmarkNode* node = model->AddURL(model->bookmark_bar_node(),
                                           0,
                                           ASCIIToUTF16("Bookie markie title"),
                                           gurl);
  BookmarkBubbleController* controller = ControllerForNode(node);
  EXPECT_TRUE(controller);

  [(id)controller cancel:nil];
  EXPECT_TRUE(model->IsBookmarked(gurl));
}

// Confirm indentation of items in pop-up menu
TEST_F(BookmarkBubbleControllerTest, TestMenuIndentation) {
  // Create some folders, including a nested folder
  BookmarkModel* model = GetBookmarkModel();
  EXPECT_TRUE(model);
  const BookmarkNode* bookmarkBarNode = model->bookmark_bar_node();
  EXPECT_TRUE(bookmarkBarNode);
  const BookmarkNode* node1 = model->AddFolder(bookmarkBarNode, 0,
                                               ASCIIToUTF16("one"));
  EXPECT_TRUE(node1);
  const BookmarkNode* node2 = model->AddFolder(bookmarkBarNode, 1,
                                               ASCIIToUTF16("two"));
  EXPECT_TRUE(node2);
  const BookmarkNode* node2_1 = model->AddFolder(node2, 0,
                                                 ASCIIToUTF16("two dot one"));
  EXPECT_TRUE(node2_1);
  const BookmarkNode* node3 = model->AddFolder(bookmarkBarNode, 2,
                                               ASCIIToUTF16("three"));
  EXPECT_TRUE(node3);

  BookmarkBubbleController* controller = ControllerForNode(node1);
  EXPECT_TRUE(controller);

  // Compare the menu item indents against expectations.
  static const int kExpectedIndent[] = {0, 1, 1, 2, 1, 0};
  NSArray* items = [[controller folderPopUpButton] itemArray];
  ASSERT_GE([items count], 6U);
  for(int itemNo = 0; itemNo < 6; itemNo++) {
    NSMenuItem* item = [items objectAtIndex:itemNo];
    EXPECT_EQ(kExpectedIndent[itemNo], [item indentationLevel])
        << "Unexpected indent for menu item #" << itemNo;
  }
}

}  // namespace

@implementation NSApplication (BookmarkBubbleUnitTest)
// Add handler for the editBookmarkNode: action to NSApp for testing purposes.
// Normally this would be sent up the responder tree correctly, but since
// tests run in the background, key window and main window are never set on
// NSApplication. Adding it to NSApplication directly removes the need for
// worrying about what the current window with focus is.
- (void)editBookmarkNode:(id)sender {
  EXPECT_TRUE([sender respondsToSelector:@selector(node)]);
  BookmarkBubbleControllerTest::edits_++;
}

@end
