// 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/test/nacl/nacl_browsertest_util.h"

#include <stdlib.h>
#include "base/command_line.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/values.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/nacl/common/nacl_switches.h"
#include "content/public/browser/plugin_service.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/webplugininfo.h"
#include "net/base/net_util.h"

typedef content::TestMessageHandler::MessageResponse MessageResponse;

MessageResponse StructuredMessageHandler::HandleMessage(
    const std::string& json) {
  scoped_ptr<base::Value> value;
  base::JSONReader reader(base::JSON_ALLOW_TRAILING_COMMAS);
  // Automation messages are stringified before they are sent because the
  // automation channel cannot handle arbitrary objects.  This means we
  // need to decode the json twice to get the original message.
  value.reset(reader.ReadToValue(json));
  if (!value.get())
    return InternalError("Could parse automation JSON: " + json +
                         " because " + reader.GetErrorMessage());

  std::string temp;
  if (!value->GetAsString(&temp))
    return InternalError("Message was not a string: " + json);

  value.reset(reader.ReadToValue(temp));
  if (!value.get())
    return InternalError("Could not parse message JSON: " + temp +
                         " because " + reader.GetErrorMessage());

  base::DictionaryValue* msg;
  if (!value->GetAsDictionary(&msg))
    return InternalError("Message was not an object: " + temp);

  std::string type;
  if (!msg->GetString("type", &type))
    return MissingField("unknown", "type");

  return HandleStructuredMessage(type, msg);
}

MessageResponse StructuredMessageHandler::MissingField(
    const std::string& type,
    const std::string& field) {
  return InternalError(type + " message did not have field: " + field);
}

MessageResponse StructuredMessageHandler::InternalError(
    const std::string& reason) {
  SetError(reason);
  return DONE;
}

LoadTestMessageHandler::LoadTestMessageHandler()
    : test_passed_(false) {
}

void LoadTestMessageHandler::Log(const std::string& type,
                                 const std::string& message) {
  // TODO(ncbray) better logging.
  LOG(INFO) << type << " " << message;
}

MessageResponse LoadTestMessageHandler::HandleStructuredMessage(
   const std::string& type,
   base::DictionaryValue* msg) {
  if (type == "Log") {
    std::string message;
    if (!msg->GetString("message", &message))
      return MissingField(type, "message");
    Log("LOG", message);
    return CONTINUE;
  } else if (type == "Shutdown") {
    std::string message;
    if (!msg->GetString("message", &message))
      return MissingField(type, "message");
    if (!msg->GetBoolean("passed", &test_passed_))
      return MissingField(type, "passed");
    Log("SHUTDOWN", message);
    return DONE;
  } else {
    return InternalError("Unknown message type: " + type);
  }
}

// A message handler for nacl_integration tests ported to be browser_tests.
// nacl_integration tests report to their test jig using a series of RPC calls
// that are encoded as URL requests. When these tests run as browser_tests,
// they make the same RPC requests, but use the automation channel instead of
// URL requests. This message handler decodes and responds to these requests.
class NaClIntegrationMessageHandler : public StructuredMessageHandler {
 public:
  NaClIntegrationMessageHandler();

  void Log(const std::string& message);

  virtual MessageResponse HandleStructuredMessage(
      const std::string& type,
      base::DictionaryValue* msg) OVERRIDE;

  bool test_passed() const {
    return test_passed_;
  }

 private:
  bool test_passed_;

  DISALLOW_COPY_AND_ASSIGN(NaClIntegrationMessageHandler);
};

NaClIntegrationMessageHandler::NaClIntegrationMessageHandler()
    : test_passed_(false) {
}

void NaClIntegrationMessageHandler::Log(const std::string& message) {
  // TODO(ncbray) better logging.
  LOG(INFO) << "|||| " << message;
}

MessageResponse NaClIntegrationMessageHandler::HandleStructuredMessage(
    const std::string& type,
    base::DictionaryValue* msg) {
  if (type == "TestLog") {
    std::string message;
    if (!msg->GetString("message", &message))
      return MissingField(type, "message");
    Log(message);
    return CONTINUE;
  } else if (type == "Shutdown") {
    std::string message;
    if (!msg->GetString("message", &message))
      return MissingField(type, "message");
    if (!msg->GetBoolean("passed", &test_passed_))
      return MissingField(type, "passed");
    Log(message);
    return DONE;
  } else if (type == "Ping") {
    return CONTINUE;
  } else if (type == "JavaScriptIsAlive") {
    return CONTINUE;
  } else {
    return InternalError("Unknown message type: " + type);
  }
}

// NaCl browser tests serve files out of the build directory because nexes and
// pexes are artifacts of the build.  To keep things tidy, all test data is kept
// in a subdirectory.  Several variants of a test may be run, for example when
// linked against newlib and when linked against glibc.  These variants are kept
// in different subdirectories.  For example, the build directory will look
// something like this on Linux:
// out/
//     Release/
//             nacl_test_data/
//                            newlib/
//                            glibc/
//                            pnacl/
static bool GetNaClVariantRoot(const base::FilePath::StringType& variant,
                               base::FilePath* document_root) {
  if (!ui_test_utils::GetRelativeBuildDirectory(document_root))
    return false;
  *document_root = document_root->Append(FILE_PATH_LITERAL("nacl_test_data"));
  *document_root = document_root->Append(variant);
  return true;
}

static void AddPnaclParm(const base::FilePath::StringType& url,
                         base::FilePath::StringType* url_with_parm) {
  if (url.find(FILE_PATH_LITERAL("?")) == base::FilePath::StringType::npos) {
    *url_with_parm = url + FILE_PATH_LITERAL("?pnacl=1");
  } else {
    *url_with_parm = url + FILE_PATH_LITERAL("&pnacl=1");
  }
}

static void AddPnaclDisabledParm(const base::FilePath::StringType& url,
                                 base::FilePath::StringType* url_with_parm) {
  if (url.find(FILE_PATH_LITERAL("?")) == base::FilePath::StringType::npos) {
    *url_with_parm = url + FILE_PATH_LITERAL("?pnacl_disabled=1");
  } else {
    *url_with_parm = url + FILE_PATH_LITERAL("&pnacl_disabled=1");
  }
}

NaClBrowserTestBase::NaClBrowserTestBase() {
}

NaClBrowserTestBase::~NaClBrowserTestBase() {
}

void NaClBrowserTestBase::SetUpCommandLine(base::CommandLine* command_line) {
  command_line->AppendSwitch(switches::kEnableNaCl);
}

void NaClBrowserTestBase::SetUpOnMainThread() {
  // Sanity check.
  base::FilePath plugin_lib;
  ASSERT_TRUE(PathService::Get(chrome::FILE_NACL_PLUGIN, &plugin_lib));
  ASSERT_TRUE(base::PathExists(plugin_lib)) << plugin_lib.value();

  ASSERT_TRUE(StartTestServer()) << "Cannot start test server.";
}

bool NaClBrowserTestBase::GetDocumentRoot(base::FilePath* document_root) {
  return GetNaClVariantRoot(Variant(), document_root);
}

bool NaClBrowserTestBase::IsAPnaclTest() {
  return false;
}

bool NaClBrowserTestBase::IsPnaclDisabled() {
  return false;
}

GURL NaClBrowserTestBase::TestURL(
    const base::FilePath::StringType& url_fragment) {
  base::FilePath expanded_url = base::FilePath(FILE_PATH_LITERAL("files"));
  expanded_url = expanded_url.Append(url_fragment);
  return test_server_->GetURL(expanded_url.MaybeAsASCII());
}

bool NaClBrowserTestBase::RunJavascriptTest(
    const GURL& url,
    content::TestMessageHandler* handler) {
  content::JavascriptTestObserver observer(
      browser()->tab_strip_model()->GetActiveWebContents(),
      handler);
  ui_test_utils::NavigateToURL(browser(), url);
  return observer.Run();
}

void NaClBrowserTestBase::RunLoadTest(
    const base::FilePath::StringType& test_file) {
  LoadTestMessageHandler handler;
  base::FilePath::StringType test_file_with_pnacl = test_file;
  if (IsAPnaclTest()) {
    AddPnaclParm(test_file, &test_file_with_pnacl);
  }
  base::FilePath::StringType test_file_with_both = test_file_with_pnacl;
  if (IsPnaclDisabled()) {
    AddPnaclDisabledParm(test_file_with_pnacl, &test_file_with_both);
  }
  bool ok = RunJavascriptTest(TestURL(test_file_with_both), &handler);
  ASSERT_TRUE(ok) << handler.error_message();
  ASSERT_TRUE(handler.test_passed()) << "Test failed.";
}

void NaClBrowserTestBase::RunNaClIntegrationTest(
    const base::FilePath::StringType& url_fragment, bool full_url) {
  NaClIntegrationMessageHandler handler;
  base::FilePath::StringType url_fragment_with_pnacl = url_fragment;
  if (IsAPnaclTest()) {
    AddPnaclParm(url_fragment, &url_fragment_with_pnacl);
  }
  base::FilePath::StringType url_fragment_with_both = url_fragment_with_pnacl;
  if (IsPnaclDisabled()) {
    AddPnaclDisabledParm(url_fragment_with_pnacl, &url_fragment_with_both);
  }
  bool ok = RunJavascriptTest(full_url
                              ? GURL(url_fragment_with_both)
                              : TestURL(url_fragment_with_both),
                              &handler);
  ASSERT_TRUE(ok) << handler.error_message();
  ASSERT_TRUE(handler.test_passed()) << "Test failed.";
}

bool NaClBrowserTestBase::StartTestServer() {
  // Launch the web server.
  base::FilePath document_root;
  if (!GetDocumentRoot(&document_root))
    return false;
  test_server_.reset(new net::SpawnedTestServer(
                         net::SpawnedTestServer::TYPE_HTTP,
                         net::SpawnedTestServer::kLocalhost,
                         document_root));
  return test_server_->Start();
}

base::FilePath::StringType NaClBrowserTestNewlib::Variant() {
  return FILE_PATH_LITERAL("newlib");
}

base::FilePath::StringType NaClBrowserTestGLibc::Variant() {
  return FILE_PATH_LITERAL("glibc");
}

base::FilePath::StringType NaClBrowserTestPnacl::Variant() {
  return FILE_PATH_LITERAL("pnacl");
}

bool NaClBrowserTestPnacl::IsAPnaclTest() {
  return true;
}

base::FilePath::StringType NaClBrowserTestPnaclDisabled::Variant() {
  return FILE_PATH_LITERAL("pnacl");
}

bool NaClBrowserTestPnaclDisabled::IsAPnaclTest() {
  return true;
}

bool NaClBrowserTestPnaclDisabled::IsPnaclDisabled() {
  return true;
}
void NaClBrowserTestPnaclDisabled::SetUpCommandLine(
    base::CommandLine* command_line) {
  NaClBrowserTestBase::SetUpCommandLine(command_line);
  command_line->AppendSwitch(switches::kDisablePnacl);
}

base::FilePath::StringType NaClBrowserTestNonSfiMode::Variant() {
  return FILE_PATH_LITERAL("libc-free");
}

void NaClBrowserTestNonSfiMode::SetUpCommandLine(
    base::CommandLine* command_line) {
  NaClBrowserTestBase::SetUpCommandLine(command_line);
  command_line->AppendSwitch(switches::kEnableNaClNonSfiMode);
}

base::FilePath::StringType NaClBrowserTestStatic::Variant() {
  return FILE_PATH_LITERAL("static");
}

bool NaClBrowserTestStatic::GetDocumentRoot(base::FilePath* document_root) {
  *document_root = base::FilePath(FILE_PATH_LITERAL("chrome/test/data/nacl"));
  return true;
}

base::FilePath::StringType NaClBrowserTestPnaclNonSfi::Variant() {
  return FILE_PATH_LITERAL("nonsfi");
}

void NaClBrowserTestPnaclNonSfi::SetUpCommandLine(
    base::CommandLine* command_line) {
  NaClBrowserTestBase::SetUpCommandLine(command_line);
  command_line->AppendSwitch(switches::kEnableNaClNonSfiMode);
}

void NaClBrowserTestNewlibExtension::SetUpCommandLine(
    CommandLine* command_line) {
  NaClBrowserTestBase::SetUpCommandLine(command_line);
  base::FilePath src_root;
  ASSERT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &src_root));

  // Extension-based tests should specialize the GetDocumentRoot() / Variant()
  // to point at the isolated the test extension directory.
  // Otherwise, multiple NaCl extensions tests will end up sharing the
  // same directory when loading the extension files.
  base::FilePath document_root;
  ASSERT_TRUE(GetDocumentRoot(&document_root));

  // Document root is relative to source root, and source root may not be CWD.
  command_line->AppendSwitchPath(switches::kLoadExtension,
                                 src_root.Append(document_root));
}