// Copyright (c) 2011 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 "remoting/protocol/jingle_messages.h"

#include "base/logging.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/libjingle/source/talk/xmllite/xmlelement.h"
#include "third_party/libjingle/source/talk/xmpp/constants.h"

using buzz::QName;
using buzz::XmlAttr;
using buzz::XmlElement;

namespace remoting {
namespace protocol {

namespace {

const char kXmlNsNs[] = "http://www.w3.org/2000/xmlns/";
const char kXmlNs[] = "xmlns";

// Compares two XML blobs and returns true if they are
// equivalent. Otherwise |error| is set to error message that
// specifies the first test.
bool VerifyXml(const XmlElement* exp,
               const XmlElement* val,
               std::string* error) {
  if (exp->Name() != val->Name()) {
    *error = "<" + exp->Name().Merged() + ">" + " is expected, but " +
        "<" + val->Name().Merged() + ">"  + " found";
    return false;
  }
  if (exp->BodyText() != val->BodyText()) {
    *error = "<" + exp->Name().LocalPart() + ">" + exp->BodyText() +
        "</" + exp->Name().LocalPart() + ">" " is expected, but found " +
        "<" + exp->Name().LocalPart() + ">" + val->BodyText() +
        "</" + exp->Name().LocalPart() + ">";
    return false;
  }

  for (const XmlAttr* exp_attr = exp->FirstAttr(); exp_attr != NULL;
       exp_attr = exp_attr->NextAttr()) {
    if (exp_attr->Name().Namespace() == kXmlNsNs ||
        exp_attr->Name() == QName(kXmlNs)) {
      continue; // Skip NS attributes.
    }
    if (val->Attr(exp_attr->Name()) != exp_attr->Value()) {
      *error = "In <" + exp->Name().LocalPart() + "> attribute " +
          exp_attr->Name().LocalPart() + " is expected to be set to " +
          exp_attr->Value();
      return false;
    }
  }

  for (const XmlAttr* val_attr = val->FirstAttr(); val_attr;
       val_attr = val_attr->NextAttr()) {
    if (val_attr->Name().Namespace() == kXmlNsNs ||
        val_attr->Name() == QName(kXmlNs)) {
      continue; // Skip NS attributes.
    }
    if (exp->Attr(val_attr->Name()) != val_attr->Value()) {
      *error = "In <" + exp->Name().LocalPart() + "> unexpected attribute " +
          val_attr->Name().LocalPart();
      return false;
    }
  }

  const XmlElement* exp_child = exp->FirstElement();
  const XmlElement* val_child = val->FirstElement();
  while (exp_child && val_child) {
    if (!VerifyXml(exp_child, val_child, error))
      return false;
    exp_child = exp_child->NextElement();
    val_child = val_child->NextElement();
  }
  if (exp_child) {
    *error = "<" + exp_child->Name().Merged() + "> is expected, but not found";
    return false;
  }

  if (val_child) {
    *error = "Unexpected <" + val_child->Name().Merged() + "> found";
    return false;
  }

  return true;
}

}  // namespace

// Each of the tests below try to parse a message, format it again,
// and then verify that the formatted message is the same as the
// original stanza. The sample messages were generated by libjingle.

TEST(JingleMessageTest, SessionInitiate) {
  const char* kTestSessionInitiateMessage =
      "<iq to='user@gmail.com/chromoting016DBB07' type='set' "
      "from='user@gmail.com/chromiumsy5C6A652D' "
      "xmlns='jabber:client'><jingle xmlns='urn:xmpp:jingle:1' "
      "action='session-initiate' sid='2227053353' "
      "initiator='user@gmail.com/chromiumsy5C6A652D'><content "
      "name='chromoting' creator='initiator'><description "
      "xmlns='google:remoting'><control transport='stream' version='2'/><event "
      "transport='stream' version='2'/><video transport='stream' version='2' "
      "codec='vp8'/><initial-resolution width='800' height='600'/>"
      "<authentication><auth-token>j7whCMii0Z0AAPwj7whCM/j7whCMii0Z0AAPw="
      "</auth-token></authentication></description><transport "
      "xmlns='http://www.google.com/transport/p2p'/></content></jingle>"
      "</iq>";
  scoped_ptr<XmlElement> source_message(
      XmlElement::ForStr(kTestSessionInitiateMessage));
  ASSERT_TRUE(source_message.get());

  EXPECT_TRUE(JingleMessage::IsJingleMessage(source_message.get()));

  JingleMessage message;
  std::string error;
  EXPECT_TRUE(message.ParseXml(source_message.get(), &error)) << error;

  EXPECT_EQ(message.action, JingleMessage::SESSION_INITIATE);

  scoped_ptr<XmlElement> formatted_message(message.ToXml());
  ASSERT_TRUE(formatted_message.get());
  EXPECT_TRUE(VerifyXml(formatted_message.get(), source_message.get(), &error))
      << error;
}

TEST(JingleMessageTest, SessionAccept) {
  const char* kTestSessionAcceptMessage =
      "<cli:iq from='user@gmail.com/chromoting016DBB07' "
      "to='user@gmail.com/chromiumsy5C6A652D' type='set' "
      "xmlns:cli='jabber:client'><jingle action='session-accept' "
      "sid='2227053353' xmlns='urn:xmpp:jingle:1'><content creator='initiator' "
      "name='chromoting'><description xmlns='google:remoting'><control "
      "transport='stream' version='2'/><event transport='stream' version='2'/>"
      "<video codec='vp8' transport='stream' version='2'/><initial-resolution "
      "height='2048' width='2048'/><authentication><certificate>"
      "MIICpjCCAY6gW0Cert0TANBgkqhkiG9w0BAQUFA=</certificate>"
      "</authentication></description><transport xmlns="
      "'http://www.google.com/transport/p2p'/></content></jingle></cli:iq>";

  scoped_ptr<XmlElement> source_message(
      XmlElement::ForStr(kTestSessionAcceptMessage));
  ASSERT_TRUE(source_message.get());

  EXPECT_TRUE(JingleMessage::IsJingleMessage(source_message.get()));

  JingleMessage message;
  std::string error;
  EXPECT_TRUE(message.ParseXml(source_message.get(), &error)) << error;

  EXPECT_EQ(message.action, JingleMessage::SESSION_ACCEPT);

  scoped_ptr<XmlElement> formatted_message(message.ToXml());
  ASSERT_TRUE(formatted_message.get());
  EXPECT_TRUE(VerifyXml(source_message.get(), formatted_message.get(), &error))
      << error;
}

TEST(JingleMessageTest, TransportInfo) {
  const char* kTestTransportInfoMessage =
      "<cli:iq to='user@gmail.com/chromoting016DBB07' type='set' "
      "xmlns:cli='jabber:client'><jingle xmlns='urn:xmpp:jingle:1' "
      "action='transport-info' sid='2227053353'><content name='chromoting' "
      "creator='initiator'><transport "
      "xmlns='http://www.google.com/transport/p2p'><candidate name='event' "
      "address='172.23.164.186' port='57040' preference='1' "
      "username='tPUyEAmQrEw3y7hi' protocol='udp' generation='0' "
      "password='2iRdhLfawKZC5ydJ' type='local'/><candidate name='video' "
      "address='172.23.164.186' port='42171' preference='1' "
      "username='EPK3CXo5sTLJSez0' protocol='udp' generation='0' "
      "password='eM0VUfUkZ+1Pyi0M' type='local'/></transport></content>"
      "</jingle></cli:iq>";

  scoped_ptr<XmlElement> source_message(
      XmlElement::ForStr(kTestTransportInfoMessage));
  ASSERT_TRUE(source_message.get());

  EXPECT_TRUE(JingleMessage::IsJingleMessage(source_message.get()));

  JingleMessage message;
  std::string error;
  EXPECT_TRUE(message.ParseXml(source_message.get(), &error)) << error;

  EXPECT_EQ(message.action, JingleMessage::TRANSPORT_INFO);
  EXPECT_EQ(message.candidates.size(), 2U);

  scoped_ptr<XmlElement> formatted_message(message.ToXml());
  ASSERT_TRUE(formatted_message.get());
  EXPECT_TRUE(VerifyXml(source_message.get(), formatted_message.get(), &error))
      << error;
}

TEST(JingleMessageTest, SessionTerminate) {
  const char* kTestSessionTerminateMessage =
      "<cli:iq from='user@gmail.com/chromoting016DBB07' "
      "to='user@gmail.com/chromiumsy5C6A652D' type='set' "
      "xmlns:cli='jabber:client'><jingle action='session-terminate' "
      "sid='2227053353' xmlns='urn:xmpp:jingle:1'><reason><success/>"
      "</reason></jingle></cli:iq>";

  scoped_ptr<XmlElement> source_message(
      XmlElement::ForStr(kTestSessionTerminateMessage));
  ASSERT_TRUE(source_message.get());

  EXPECT_TRUE(JingleMessage::IsJingleMessage(source_message.get()));

  JingleMessage message;
  std::string error;
  EXPECT_TRUE(message.ParseXml(source_message.get(), &error)) << error;

  EXPECT_EQ(message.action, JingleMessage::SESSION_TERMINATE);

  scoped_ptr<XmlElement> formatted_message(message.ToXml());
  ASSERT_TRUE(formatted_message.get());
  EXPECT_TRUE(VerifyXml(source_message.get(), formatted_message.get(), &error))
      << error;
}

TEST(JingleMessageReplyTest, ToXml) {
  const char* kTestIncomingMessage =
      "<cli:iq from='user@gmail.com/chromoting016DBB07' id='4' "
      "to='user@gmail.com/chromiumsy5C6A652D' type='set' "
      "xmlns:cli='jabber:client'><jingle action='session-terminate' "
      "sid='2227053353' xmlns='urn:xmpp:jingle:1'><reason><success/>"
      "</reason></jingle></cli:iq>";
  scoped_ptr<XmlElement> incoming_message(
      XmlElement::ForStr(kTestIncomingMessage));
  ASSERT_TRUE(incoming_message.get());

  struct TestCase {
    const JingleMessageReply::ErrorType error;
    std::string error_text;
    std::string expected_text;
  } tests[] = {
    { JingleMessageReply::BAD_REQUEST, "", "<iq xmlns='jabber:client' "
      "to='user@gmail.com/chromoting016DBB07' id='4' type='error'><jingle "
      "action='session-terminate' sid='2227053353' xmlns='urn:xmpp:jingle:1'>"
      "<reason><success/></reason></jingle><error type='modify'><bad-request/>"
      "</error></iq>" },
    { JingleMessageReply::BAD_REQUEST, "ErrorText", "<iq xmlns='jabber:client' "
      "to='user@gmail.com/chromoting016DBB07' id='4' type='error'><jingle "
      "action='session-terminate' sid='2227053353' xmlns='urn:xmpp:jingle:1'>"
      "<reason><success/></reason></jingle><error type='modify'><bad-request/>"
      "<text xml:lang='en'>ErrorText</text></error></iq>" },
    { JingleMessageReply::NOT_IMPLEMENTED, "", "<iq xmlns='jabber:client' "
      "to='user@gmail.com/chromoting016DBB07' id='4' type='error'><jingle "
      "action='session-terminate' sid='2227053353' xmlns='urn:xmpp:jingle:1'>"
      "<reason><success/></reason></jingle><error type='cancel'>"
      "<feature-bad-request/></error></iq>" },
    { JingleMessageReply::INVALID_SID, "",  "<iq xmlns='jabber:client' "
      "to='user@gmail.com/chromoting016DBB07' id='4' type='error'><jingle "
      "action='session-terminate' sid='2227053353' xmlns='urn:xmpp:jingle:1'>"
      "<reason><success/></reason></jingle><error type='modify'>"
      "<item-not-found/><text xml:lang='en'>Invalid SID</text></error></iq>" },
    { JingleMessageReply::INVALID_SID, "ErrorText", "<iq xmlns='jabber:client' "
      "to='user@gmail.com/chromoting016DBB07' id='4' type='error'><jingle "
      "action='session-terminate' sid='2227053353' xmlns='urn:xmpp:jingle:1'>"
      "<reason><success/></reason></jingle><error type='modify'>"
      "<item-not-found/><text xml:lang='en'>ErrorText</text></error></iq>" },
    { JingleMessageReply::UNEXPECTED_REQUEST, "", "<iq xmlns='jabber:client' "
      "to='user@gmail.com/chromoting016DBB07' id='4' type='error'><jingle "
      "action='session-terminate' sid='2227053353' xmlns='urn:xmpp:jingle:1'>"
      "<reason><success/></reason></jingle><error type='modify'>"
      "<unexpected-request/></error></iq>" },
  };

  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) {
    JingleMessageReply reply_msg;
    if (tests[i].error_text.empty()) {
      reply_msg = JingleMessageReply(tests[i].error);
    } else {
      reply_msg = JingleMessageReply(tests[i].error, tests[i].error_text);
    }
    scoped_ptr<XmlElement> reply(reply_msg.ToXml(incoming_message.get()));

    scoped_ptr<XmlElement> expected(XmlElement::ForStr(tests[i].expected_text));
    ASSERT_TRUE(expected.get());

    std::string error;
    EXPECT_TRUE(VerifyXml(expected.get(), reply.get(), &error)) << error;
  }
}

}  // namespace protocol
}  // namespace remoting