/*----------------------------------------------------------------------------*/
/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
/* Open Source Software - may be modified and shared by FRC teams. The code   */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project.                                                               */
/*----------------------------------------------------------------------------*/

#include "wpi/WebSocket.h"  // NOLINT(build/include_order)

#include "WebSocketTest.h"
#include "wpi/Base64.h"
#include "wpi/HttpParser.h"
#include "wpi/SmallString.h"
#include "wpi/raw_uv_ostream.h"
#include "wpi/sha1.h"

namespace wpi {

class WebSocketClientTest : public WebSocketTest {
 public:
  WebSocketClientTest() {
    // Bare bones server
    req.header.connect([this](StringRef name, StringRef value) {
      // save key (required for valid response)
      if (name.equals_lower("sec-websocket-key")) clientKey = value;
    });
    req.headersComplete.connect([this](bool) {
      // send response
      SmallVector<uv::Buffer, 4> bufs;
      raw_uv_ostream os{bufs, 4096};
      os << "HTTP/1.1 101 Switching Protocols\r\n";
      os << "Upgrade: websocket\r\n";
      os << "Connection: Upgrade\r\n";

      // accept hash
      SHA1 hash;
      hash.Update(clientKey);
      hash.Update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
      if (mockBadAccept) hash.Update("1");
      SmallString<64> hashBuf;
      SmallString<64> acceptBuf;
      os << "Sec-WebSocket-Accept: "
         << Base64Encode(hash.RawFinal(hashBuf), acceptBuf) << "\r\n";

      if (!mockProtocol.empty())
        os << "Sec-WebSocket-Protocol: " << mockProtocol << "\r\n";

      os << "\r\n";

      conn->Write(bufs, [](auto bufs, uv::Error) {
        for (auto& buf : bufs) buf.Deallocate();
      });

      serverHeadersDone = true;
      if (connected) connected();
    });

    serverPipe->Listen([this] {
      conn = serverPipe->Accept();
      conn->StartRead();
      conn->data.connect([this](uv::Buffer& buf, size_t size) {
        StringRef data{buf.base, size};
        if (!serverHeadersDone) {
          data = req.Execute(data);
          if (req.HasError()) Finish();
          ASSERT_EQ(req.GetError(), HPE_OK) << http_errno_name(req.GetError());
          if (data.empty()) return;
        }
        wireData.insert(wireData.end(), data.bytes_begin(), data.bytes_end());
      });
      conn->end.connect([this] { Finish(); });
    });
  }

  bool mockBadAccept = false;
  std::vector<uint8_t> wireData;
  std::shared_ptr<uv::Pipe> conn;
  HttpParser req{HttpParser::kRequest};
  SmallString<64> clientKey;
  std::string mockProtocol;
  bool serverHeadersDone = false;
  std::function<void()> connected;
};

TEST_F(WebSocketClientTest, Open) {
  int gotOpen = 0;

  clientPipe->Connect(pipeName, [&] {
    auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
    ws->closed.connect([&](uint16_t code, StringRef reason) {
      Finish();
      if (code != 1005 && code != 1006)
        FAIL() << "Code: " << code << " Reason: " << reason;
    });
    ws->open.connect([&](StringRef protocol) {
      ++gotOpen;
      Finish();
      ASSERT_TRUE(protocol.empty());
    });
  });

  loop->Run();

  if (HasFatalFailure()) return;
  ASSERT_EQ(gotOpen, 1);
}

TEST_F(WebSocketClientTest, BadAccept) {
  int gotClosed = 0;

  mockBadAccept = true;

  clientPipe->Connect(pipeName, [&] {
    auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
    ws->closed.connect([&](uint16_t code, StringRef msg) {
      Finish();
      ++gotClosed;
      ASSERT_EQ(code, 1002) << "Message: " << msg;
    });
    ws->open.connect([&](StringRef protocol) {
      Finish();
      FAIL() << "Got open";
    });
  });

  loop->Run();

  if (HasFatalFailure()) return;
  ASSERT_EQ(gotClosed, 1);
}

TEST_F(WebSocketClientTest, ProtocolGood) {
  int gotOpen = 0;

  mockProtocol = "myProtocol";

  clientPipe->Connect(pipeName, [&] {
    auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName,
                                      {"myProtocol", "myProtocol2"});
    ws->closed.connect([&](uint16_t code, StringRef msg) {
      Finish();
      if (code != 1005 && code != 1006)
        FAIL() << "Code: " << code << "Message: " << msg;
    });
    ws->open.connect([&](StringRef protocol) {
      ++gotOpen;
      Finish();
      ASSERT_EQ(protocol, "myProtocol");
    });
  });

  loop->Run();

  if (HasFatalFailure()) return;
  ASSERT_EQ(gotOpen, 1);
}

TEST_F(WebSocketClientTest, ProtocolRespNotReq) {
  int gotClosed = 0;

  mockProtocol = "myProtocol";

  clientPipe->Connect(pipeName, [&] {
    auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
    ws->closed.connect([&](uint16_t code, StringRef msg) {
      Finish();
      ++gotClosed;
      ASSERT_EQ(code, 1003) << "Message: " << msg;
    });
    ws->open.connect([&](StringRef protocol) {
      Finish();
      FAIL() << "Got open";
    });
  });

  loop->Run();

  if (HasFatalFailure()) return;
  ASSERT_EQ(gotClosed, 1);
}

TEST_F(WebSocketClientTest, ProtocolReqNotResp) {
  int gotClosed = 0;

  clientPipe->Connect(pipeName, [&] {
    auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName,
                                      StringRef{"myProtocol"});
    ws->closed.connect([&](uint16_t code, StringRef msg) {
      Finish();
      ++gotClosed;
      ASSERT_EQ(code, 1002) << "Message: " << msg;
    });
    ws->open.connect([&](StringRef protocol) {
      Finish();
      FAIL() << "Got open";
    });
  });

  loop->Run();

  if (HasFatalFailure()) return;
  ASSERT_EQ(gotClosed, 1);
}

//
// Send and receive data.  Most of these cases are tested in
// WebSocketServerTest, so only spot check differences like masking.
//

class WebSocketClientDataTest : public WebSocketClientTest,
                                public ::testing::WithParamInterface<size_t> {
 public:
  WebSocketClientDataTest() {
    clientPipe->Connect(pipeName, [&] {
      ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
      if (setupWebSocket) setupWebSocket();
    });
  }

  std::function<void()> setupWebSocket;
  std::shared_ptr<WebSocket> ws;
};

INSTANTIATE_TEST_SUITE_P(WebSocketClientDataTests, WebSocketClientDataTest,
                         ::testing::Values(0, 1, 125, 126, 65535, 65536));

TEST_P(WebSocketClientDataTest, SendBinary) {
  int gotCallback = 0;
  std::vector<uint8_t> data(GetParam(), 0x03u);
  setupWebSocket = [&] {
    ws->open.connect([&](StringRef) {
      ws->SendBinary(uv::Buffer(data), [&](auto bufs, uv::Error) {
        ++gotCallback;
        ws->Terminate();
        ASSERT_FALSE(bufs.empty());
        ASSERT_EQ(bufs[0].base, reinterpret_cast<const char*>(data.data()));
      });
    });
  };

  loop->Run();

  auto expectData = BuildMessage(0x02, true, true, data);
  AdjustMasking(wireData);
  ASSERT_EQ(wireData, expectData);
  ASSERT_EQ(gotCallback, 1);
}

TEST_P(WebSocketClientDataTest, ReceiveBinary) {
  int gotCallback = 0;
  std::vector<uint8_t> data(GetParam(), 0x03u);
  setupWebSocket = [&] {
    ws->binary.connect([&](ArrayRef<uint8_t> inData, bool fin) {
      ++gotCallback;
      ws->Terminate();
      ASSERT_TRUE(fin);
      std::vector<uint8_t> recvData{inData.begin(), inData.end()};
      ASSERT_EQ(data, recvData);
    });
  };
  auto message = BuildMessage(0x02, true, false, data);
  connected = [&] {
    conn->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
  };

  loop->Run();

  ASSERT_EQ(gotCallback, 1);
}

//
// The client must close the connection if a masked frame is received.
//

TEST_P(WebSocketClientDataTest, ReceiveMasked) {
  int gotCallback = 0;
  std::vector<uint8_t> data(GetParam(), ' ');
  setupWebSocket = [&] {
    ws->text.connect([&](StringRef, bool) {
      ws->Terminate();
      FAIL() << "Should not have gotten masked message";
    });
    ws->closed.connect([&](uint16_t code, StringRef reason) {
      ++gotCallback;
      ASSERT_EQ(code, 1002) << "reason: " << reason;
    });
  };
  auto message = BuildMessage(0x01, true, true, data);
  connected = [&] {
    conn->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
  };

  loop->Run();

  ASSERT_EQ(gotCallback, 1);
}

}  // namespace wpi
