// Copyright (c) 2010 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 "base/command_line.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/values.h"
#include "chrome/browser/automation/extension_automation_constants.h"
#include "chrome/browser/extensions/extension_tabs_module_constants.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/test/automation/automation_messages.h"
#include "chrome/test/automation/automation_proxy_uitest.h"
#include "chrome/test/automation/tab_proxy.h"
#include "chrome/test/ui/ui_test.h"
#include "chrome/test/ui_test_utils.h"
#include "gfx/rect.h"
#include "googleurl/src/gurl.h"
#define GMOCK_MUTANT_INCLUDE_LATE_OBJECT_BINDING
#include "testing/gmock_mutant.h"

namespace {

using testing::_;
using testing::CreateFunctor;
using testing::DoAll;
using testing::Invoke;
using testing::InvokeWithoutArgs;
using testing::SaveArg;
using testing::WithArgs;

using ui_test_utils::TimedMessageLoopRunner;

static const char kTestDirectorySimpleApiCall[] =
    "extensions/uitest/simple_api_call";
static const char kTestDirectoryRoundtripApiCall[] =
    "extensions/uitest/roundtrip_api_call";
static const char kTestDirectoryBrowserEvent[] =
    "extensions/uitest/event_sink";

// TODO(port): http://crbug.com/45766
#if defined(OS_WIN)

// Base class to test extensions almost end-to-end by including browser
// startup, manifest parsing, and the actual process model in the
// equation.  This would also let you write UITests that test individual
// Chrome Extensions as running in Chrome.  Takes over implementation of
// extension API calls so that behavior can be tested deterministically
// through code, instead of having to contort the browser into a state
// suitable for testing.
//
// By default, makes Chrome forward all Chrome Extension API function calls
// via the automation interface. To override this, call set_functions_enabled()
// with a list of function names that should be forwarded,
class ExtensionUITest : public ExternalTabUITest {
 public:
   explicit ExtensionUITest(const std::string& extension_path)
      : loop_(MessageLoop::current()) {
    FilePath filename(test_data_directory_);
    filename = filename.AppendASCII(extension_path);
    launch_arguments_.AppendSwitchWithValue(switches::kLoadExtension,
                                            filename.value());
    functions_enabled_.push_back("*");
  }

  void set_functions_enabled(
      const std::vector<std::string>& functions_enabled) {
    functions_enabled_ = functions_enabled;
  }

  void SetUp() {
    ExternalTabUITest::SetUp();
    // TabProxy::NavigateInExternalTab is a sync call and can cause a deadlock
    // if host window is visible.
    mock_->host_window_style_ &= ~WS_VISIBLE;
    tab_ = mock_->CreateTabWithUrl(GURL());
    tab_->SetEnableExtensionAutomation(functions_enabled_);
  }

  void TearDown() {
    tab_->SetEnableExtensionAutomation(std::vector<std::string>());
    tab_ = NULL;
    EXPECT_TRUE(mock_->HostWindowExists()) <<
        "You shouldn't DestroyHostWindow yourself, or extension automation "
        "won't be correctly reset. Just exit your message loop.";
    mock_->DestroyHostWindow();
    ExternalTabUITest::TearDown();
  }

 protected:
  // Extension API functions that we want to take over.  Defaults to all.
  std::vector<std::string> functions_enabled_;

  // Message loop for running the async bits of your test.
  TimedMessageLoopRunner loop_;

  // The external tab.
  scoped_refptr<TabProxy> tab_;

 private:
  DISALLOW_COPY_AND_ASSIGN(ExtensionUITest);
};

// A test that loads a basic extension that makes an API call that does
// not require a response.
class ExtensionTestSimpleApiCall : public ExtensionUITest {
 public:
  ExtensionTestSimpleApiCall()
      : ExtensionUITest(kTestDirectorySimpleApiCall) {
  }

  void SetUp() {
    // Set just this one function explicitly to be forwarded, as a test of
    // the selective forwarding.  The next test will leave the default to test
    // universal forwarding.
    functions_enabled_.clear();
    functions_enabled_.push_back("tabs.remove");
    ExtensionUITest::SetUp();
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(ExtensionTestSimpleApiCall);
};

TEST_F(ExtensionTestSimpleApiCall, FLAKY_RunTest) {
  namespace keys = extension_automation_constants;

  ASSERT_THAT(mock_, testing::NotNull());
  EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1);
  EXPECT_CALL(*mock_, OnNavigationStateChanged(_, _, _))
      .Times(testing::AnyNumber());

  std::string message_received;
  EXPECT_CALL(*mock_, OnForwardMessageToExternalHost(
      _, _, keys::kAutomationOrigin, keys::kAutomationRequestTarget))
      .WillOnce(DoAll(
        SaveArg<1>(&message_received),
        InvokeWithoutArgs(
            CreateFunctor(&loop_, &TimedMessageLoopRunner::Quit))));

  EXPECT_CALL(*mock_, HandleClosed(_));

  ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, tab_->NavigateInExternalTab(
      GURL("chrome-extension://pmgpglkggjdpkpghhdmbdhababjpcohk/test.html"),
      GURL("")));

  loop_.RunFor(action_max_timeout_ms());
  ASSERT_FALSE(message_received.empty());

  scoped_ptr<Value> message_value(base::JSONReader::Read(message_received,
                                                         false));
  ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
  DictionaryValue* message_dict =
      reinterpret_cast<DictionaryValue*>(message_value.get());
  std::string result;
  ASSERT_TRUE(message_dict->GetString(keys::kAutomationNameKey, &result));
  EXPECT_EQ("tabs.remove", result);

  result.clear();
  ASSERT_TRUE(message_dict->GetString(keys::kAutomationArgsKey, &result));
  EXPECT_FALSE(result.empty());

  int callback_id = 123456789;
  message_dict->GetInteger(keys::kAutomationRequestIdKey, &callback_id);
  EXPECT_NE(callback_id, 123456789);

  bool has_callback = true;
  EXPECT_TRUE(message_dict->GetBoolean(keys::kAutomationHasCallbackKey,
                                       &has_callback));
  EXPECT_FALSE(has_callback);
}

// A test that loads a basic extension that makes an API call that does
// not require a response.
class ExtensionTestRoundtripApiCall : public ExtensionUITest {
public:
  ExtensionTestRoundtripApiCall()
    : ExtensionUITest(kTestDirectoryRoundtripApiCall),
      messages_received_(0) {
  }

  void SetUp() {
    // Set just this one function explicitly to be forwarded, as a test of
    // the selective forwarding.  The next test will leave the default to test
    // universal forwarding.
    functions_enabled_.clear();
    functions_enabled_.push_back("tabs.getSelected");
    functions_enabled_.push_back("tabs.remove");
    ExtensionUITest::SetUp();
  }

  void CheckAndSendResponse(const std::string& message) {
    namespace keys = extension_automation_constants;
    ++messages_received_;

    ASSERT_TRUE(tab_ != NULL);
    ASSERT_TRUE(messages_received_ == 1 || messages_received_ == 2);

    scoped_ptr<Value> message_value(base::JSONReader::Read(message, false));
    ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
    DictionaryValue* request_dict =
      static_cast<DictionaryValue*>(message_value.get());
    std::string function_name;
    ASSERT_TRUE(request_dict->GetString(keys::kAutomationNameKey,
      &function_name));
    int request_id = -2;
    EXPECT_TRUE(request_dict->GetInteger(keys::kAutomationRequestIdKey,
      &request_id));
    bool has_callback = false;
    EXPECT_TRUE(request_dict->GetBoolean(keys::kAutomationHasCallbackKey,
      &has_callback));

    if (messages_received_ == 1) {
      EXPECT_EQ("tabs.getSelected", function_name);
      EXPECT_GE(request_id, 0);
      EXPECT_TRUE(has_callback);

      DictionaryValue response_dict;
      response_dict.SetInteger(keys::kAutomationRequestIdKey, request_id);
      DictionaryValue tab_dict;
      tab_dict.SetInteger(extension_tabs_module_constants::kIdKey, 42);
      tab_dict.SetInteger(extension_tabs_module_constants::kIndexKey, 1);
      tab_dict.SetInteger(extension_tabs_module_constants::kWindowIdKey, 1);
      tab_dict.SetBoolean(extension_tabs_module_constants::kSelectedKey, true);
      tab_dict.SetBoolean(extension_tabs_module_constants::kIncognitoKey,
                          false);
      tab_dict.SetString(extension_tabs_module_constants::kUrlKey,
                         "http://www.google.com");

      std::string tab_json;
      base::JSONWriter::Write(&tab_dict, false, &tab_json);

      response_dict.SetString(keys::kAutomationResponseKey, tab_json);

      std::string response_json;
      base::JSONWriter::Write(&response_dict, false, &response_json);

      tab_->HandleMessageFromExternalHost(
        response_json,
        keys::kAutomationOrigin,
        keys::kAutomationResponseTarget);
    } else if (messages_received_ == 2) {
      EXPECT_EQ("tabs.remove", function_name);
      EXPECT_FALSE(has_callback);

      std::string args;
      EXPECT_TRUE(request_dict->GetString(keys::kAutomationArgsKey, &args));
      EXPECT_NE(std::string::npos, args.find("42"));
      loop_.Quit();
    } else {
      FAIL();
      loop_.Quit();
    }
  }

private:
  int messages_received_;
  DISALLOW_COPY_AND_ASSIGN(ExtensionTestRoundtripApiCall);
};

// This is flaky on XP Tests (dbg)(2) bot.
// http://code.google.com/p/chromium/issues/detail?id=44650
TEST_F(ExtensionTestRoundtripApiCall, FLAKY_RunTest) {
  namespace keys = extension_automation_constants;

  ASSERT_THAT(mock_, testing::NotNull());
  EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1);
  EXPECT_CALL(*mock_, OnNavigationStateChanged(_, _, _))
      .Times(testing::AnyNumber());
  EXPECT_CALL(*mock_, OnLoad(_, _)).Times(testing::AnyNumber());

  EXPECT_CALL(*mock_, OnForwardMessageToExternalHost(
    _, _, keys::kAutomationOrigin, keys::kAutomationRequestTarget))
    .Times(2)
    .WillRepeatedly(WithArgs<1>(Invoke(
        CreateFunctor(this,
            &ExtensionTestRoundtripApiCall::CheckAndSendResponse))));

  EXPECT_CALL(*mock_, HandleClosed(_));

  ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, tab_->NavigateInExternalTab(
    GURL("chrome-extension://ofoknjclcmghjfmbncljcnpjmfmldhno/test.html"),
    GURL("")));

  // CheckAndSendResponse (called by OnForwardMessageToExternalHost)
  // will end the loop once it has received both of our expected messages.
  loop_.RunFor(action_max_timeout_ms());
}

class ExtensionTestBrowserEvents : public ExtensionUITest {
 public:
  ExtensionTestBrowserEvents()
    : ExtensionUITest(kTestDirectoryBrowserEvent),
      response_count_(0) {
  }

  void SetUp() {
    // Set just this one function explicitly to be forwarded, as a test of
    // the selective forwarding.  The next test will leave the default to test
    // universal forwarding.
    functions_enabled_.clear();
    functions_enabled_.push_back("windows.getCurrent");
    ExtensionUITest::SetUp();
  }

  // Fire an event of the given name to the test extension.
  void FireEvent(const char* event_message) {
    // JSON doesn't allow single quotes.
    std::string event_message_strict(event_message);
    ReplaceSubstringsAfterOffset(&event_message_strict, 0, "\'", "\\\"");

    namespace keys = extension_automation_constants;

    ASSERT_TRUE(tab_ != NULL);

    tab_->HandleMessageFromExternalHost(
        event_message_strict,
        keys::kAutomationOrigin,
        keys::kAutomationBrowserEventRequestTarget);
  }

  void HandleMessageFromChrome(const std::string& message,
                               const std::string& target);

 protected:
  // Counts the number of times we got a given event.
  std::map<std::string, int> event_count_;

  // Array containing the names of the events to fire to the extension.
  static const char* events_[];

  // Number of events extension has told us it received.
  int response_count_;

  DISALLOW_COPY_AND_ASSIGN(ExtensionTestBrowserEvents);
};

const char* ExtensionTestBrowserEvents::events_[] = {
  // Window events.
  "[\"windows.onCreated\", \"[{'id':42,'focused':true,'top':0,'left':0,"
      "'width':100,'height':100,'incognito':false,'type':'normal'}]\"]",

  "[\"windows.onRemoved\", \"[42]\"]",

  "[\"windows.onFocusChanged\", \"[42]\"]",

  // Tab events.
  "[\"tabs.onCreated\", \"[{'id':42,'index':1,'windowId':1,"
      "'selected':true,'url':'http://www.google.com','incognito':false}]\"]",

  "[\"tabs.onUpdated\", \"[42, {'status': 'complete',"
      "'url':'http://www.google.com'}, {'id':42,'index':1,'windowId':1,"
      "'selected':true,'url':'http://www.google.com','incognito':false}]\"]",

  "[\"tabs.onMoved\", \"[42, {'windowId':1,'fromIndex':1,'toIndex':2}]\"]",

  "[\"tabs.onSelectionChanged\", \"[42, {'windowId':1}]\"]",

  "[\"tabs.onAttached\", \"[42, {'newWindowId':1,'newPosition':1}]\"]",

  "[\"tabs.onDetached\", \"[43, {'oldWindowId':1,'oldPosition':1}]\"]",

  "[\"tabs.onRemoved\", \"[43]\"]",

  // Bookmark events.
  "[\"bookmarks.onCreated\", \"['42', {'id':'42','title':'foo'}]\"]",

  "[\"bookmarks.onRemoved\", \"['42', {'parentId':'2','index':1}]\"]",

  "[\"bookmarks.onChanged\", \"['42', {'title':'foo'}]\"]",

  "[\"bookmarks.onMoved\", \"['42', {'parentId':'2','index':1,"
      "'oldParentId':'3','oldIndex':2}]\"]",

  "[\"bookmarks.onChildrenReordered\", \"['32', "
      "{'childIds':['1', '2', '3']}]\"]"
};

void ExtensionTestBrowserEvents::HandleMessageFromChrome(
    const std::string& message,
    const std::string& target) {
  namespace keys = extension_automation_constants;
  ASSERT_TRUE(tab_ != NULL);

  ASSERT_FALSE(message.empty());

  if (target == keys::kAutomationRequestTarget) {
    // This should be a request for the current window.  We don't need to
    // respond, as this is used only as an indication that the extension
    // page is now loaded.
    scoped_ptr<Value> message_value(base::JSONReader::Read(message, false));
    ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
    DictionaryValue* message_dict =
        reinterpret_cast<DictionaryValue*>(message_value.get());

    std::string name;
    ASSERT_TRUE(message_dict->GetString(keys::kAutomationNameKey, &name));
    ASSERT_EQ("windows.getCurrent", name);

    // Send an OpenChannelToExtension message to chrome. Note: the JSON
    // reader expects quoted property keys.  See the comment in
    // TEST_F(BrowserEventExtensionTest, RunTest) to understand where the
    // extension Id comes from.
    tab_->HandleMessageFromExternalHost(
        "{\"rqid\":0, \"extid\": \"ofoknjclcmghjfmbncljcnpjmfmldhno\","
        " \"connid\": 1}",
        keys::kAutomationOrigin,
        keys::kAutomationPortRequestTarget);
  } else if (target == keys::kAutomationPortResponseTarget) {
    // This is a response to the open channel request.  This means we know
    // that the port is ready to send us messages.  Fire all the events now.
    for (size_t i = 0; i < arraysize(events_); ++i)
      FireEvent(events_[i]);
  } else if (target == keys::kAutomationPortRequestTarget) {
    // This is the test extension calling us back.  Make sure its telling
    // us that it received an event.  We do this by checking to see if the
    // message is a simple string of one of the event names that is fired.
    //
    // There is a special message "ACK" which means that the extension
    // received the port connection.  This is not an event response and
    // should happen before all events.
    scoped_ptr<Value> message_value(base::JSONReader::Read(message, false));
    ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
    DictionaryValue* message_dict =
        reinterpret_cast<DictionaryValue*>(message_value.get());

    std::string event_name;
    message_dict->GetString(keys::kAutomationMessageDataKey, &event_name);
    if (event_name == "\"ACK\"") {
      ASSERT_EQ(0, event_count_.size());
    } else if (event_name.empty()) {
      // This must be the post disconnect.
      int request_id = -1;
      message_dict->GetInteger(keys::kAutomationRequestIdKey, &request_id);
      ASSERT_EQ(keys::CHANNEL_CLOSED, request_id);
    } else {
      ++event_count_[event_name];
    }

    // Check if we're done.
    if (event_count_.size() == arraysize(events_)) {
      loop_.Quit();
    }
  }
}

// Flaky, http://crbug.com/37554.
TEST_F(ExtensionTestBrowserEvents, FLAKY_RunTest) {
  // This test loads an HTML file that tries to add listeners to a bunch of
  // chrome.* events and upon adding a listener it posts the name of the event
  // to the automation layer, which we'll count to make sure the events work.
  namespace keys = extension_automation_constants;

  ASSERT_THAT(mock_, testing::NotNull());
  EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1);
  EXPECT_CALL(*mock_, OnNavigationStateChanged(_, _, _))
      .Times(testing::AnyNumber());
  EXPECT_CALL(*mock_, OnLoad(_, _)).Times(testing::AnyNumber());

  EXPECT_CALL(*mock_, OnForwardMessageToExternalHost(
    _, _, keys::kAutomationOrigin, _))
    .WillRepeatedly(WithArgs<1, 3>(Invoke(
        CreateFunctor(this,
            &ExtensionTestBrowserEvents::HandleMessageFromChrome))));

  EXPECT_CALL(*mock_, HandleClosed(_));

  ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, tab_->NavigateInExternalTab(
    GURL("chrome-extension://ofoknjclcmghjfmbncljcnpjmfmldhno/test.html"),
    GURL("")));

  // HandleMessageFromChrome (called by OnForwardMessageToExternalHost) ends
  // the loop when we've received the number of response messages we expect.
  loop_.RunFor(action_max_timeout_ms());

  // If this assert hits and the actual size is 0 then you need to look at:
  // src\chrome\test\data\extensions\uitest\event_sink\test.html and see if
  // all the events we are attaching to are valid. Also compare the list against
  // the event_names_ string array above.
  ASSERT_EQ(arraysize(events_), event_count_.size());
  for (std::map<std::string, int>::iterator i = event_count_.begin();
      i != event_count_.end(); ++i) {
    const std::pair<std::string, int>& value = *i;
    EXPECT_EQ(1, value.second);
  }
}
#endif  // defined(OS_WIN)

}  // namespace