#include "token_authorization_middleware.hpp"
#include "webserver_common.hpp"

#include <condition_variable>
#include <future>
#include <mutex>

#include "gmock/gmock.h"
#include "gtest/gtest.h"

using namespace crow;

class TokenAuth : public ::testing::Test
{
  public:
    TokenAuth() :
        lk(std::unique_lock<std::mutex>(m)),
        io(std::make_shared<boost::asio::io_context>())
    {
    }

    std::mutex m;
    std::condition_variable cv;
    std::unique_lock<std::mutex> lk;
    std::shared_ptr<boost::asio::io_context> io;
    int testPort = 45451;
};

TEST_F(TokenAuth, SpecialResourcesAreAcceptedWithoutAuth)
{
    CrowApp app(io);
    crow::token_authorization::requestRoutes(app);
    BMCWEB_ROUTE(app, "/redfish/v1")
    ([]() { return boost::beast::http::status::ok; });
    auto _ = std::async(std::launch::async, [&] {
        app.port(testPort).run();
        cv.notify_one();
        io->run();
    });

    asio::io_context is;
    std::string sendmsg;

    static char buf[2048];

    // Homepage should be passed with no credentials
    sendmsg = "GET /\r\n\r\n";
    {
        asio::ip::tcp::socket c(is);
        c.connect(asio::ip::tcp::endpoint(
            asio::ip::address::from_string("127.0.0.1"), 45451));
        c.send(asio::buffer(sendmsg));
        c.receive(asio::buffer(buf, 2048));
        c.close();
        EXPECT_EQ("200", std::string(buf + 9, buf + 12));
    }

    // static should be passed with no credentials
    sendmsg = "GET /static/index.html\r\n\r\n";
    {
        asio::ip::tcp::socket c(is);
        c.connect(asio::ip::tcp::endpoint(
            asio::ip::address::from_string("127.0.0.1"), 45451));
        c.send(asio::buffer(sendmsg));
        c.receive(asio::buffer(buf, 2048));
        c.close();
        EXPECT_EQ("404", std::string(buf + 9, buf + 12));
    }

    app.stop();
}

// Tests that Base64 basic strings work
TEST(TokenAuthentication, TestRejectedResource)
{
    App<crow::persistent_data::Middleware,
        crow::token_authorization::Middleware>
        app;
    app.bindaddr("127.0.0.1").port(45451);
    BMCWEB_ROUTE(app, "/")([]() { return boost::beast::http::status::ok; });
    auto _ = async(std::launch::async, [&] { app.run(); });

    asio::io_context is;
    static char buf[2048];

    // Other resources should not be passed
    std::string sendmsg = "GET /foo\r\n\r\n";
    asio::ip::tcp::socket c(is);
    for (int i = 0; i < 200; i++)
    {
        try
        {
            c.connect(asio::ip::tcp::endpoint(
                asio::ip::address::from_string("127.0.0.1"), 45451));
        }
        catch (std::exception e)
        {
            // do nothing
        }
    }
    c.send(asio::buffer(sendmsg));
    c.receive(asio::buffer(buf, 2048));
    c.close();
    EXPECT_EQ("401", std::string(buf + 9, buf + 12));

    app.stop();
}

// Tests that Base64 basic strings work
TEST(TokenAuthentication, TestGetLoginUrl)
{
    App<crow::persistent_data::Middleware,
        crow::token_authorization::Middleware>
        app;
    app.bindaddr("127.0.0.1").port(45451);
    BMCWEB_ROUTE(app, "/")([]() { return boost::beast::http::status::ok; });
    auto _ = async(std::launch::async, [&] { app.run(); });

    asio::io_context is;
    static char buf[2048];

    // Other resources should not be passed
    std::string sendmsg = "GET /login\r\n\r\n";
    asio::ip::tcp::socket c(is);
    for (int i = 0; i < 200; i++)
    {
        try
        {
            c.connect(asio::ip::tcp::endpoint(
                asio::ip::address::from_string("127.0.0.1"), 45451));
        }
        catch (std::exception e)
        {
            // do nothing
        }
    }
    c.send(asio::buffer(sendmsg));
    c.receive(asio::buffer(buf, 2048));
    c.close();
    EXPECT_EQ("401", std::string(buf + 9, buf + 12));

    app.stop();
}

// Tests boundary conditions on login
TEST(TokenAuthentication, TestPostBadLoginUrl)
{
    App<crow::persistent_data::Middleware,
        crow::token_authorization::Middleware>
        app;
    app.bindaddr("127.0.0.1").port(45451);
    BMCWEB_ROUTE(app, "/")([]() { return boost::beast::http::status::ok; });
    auto _ = async(std::launch::async, [&] { app.run(); });

    asio::io_context is;
    std::array<char, 2048> buf;
    std::string sendmsg;

    auto send_to_localhost = [&is, &buf](std::string sendmsg) {
        asio::ip::tcp::socket c(is);
        c.connect(asio::ip::tcp::endpoint(
            asio::ip::address::from_string("127.0.0.1"), 45451));
        c.send(asio::buffer(sendmsg));
        c.receive(asio::buffer(buf));
        c.close();
    };

    {
        // Retry a couple of times waiting for the server to come up
        asio::ip::tcp::socket c(is);
        for (int i = 0; i < 200; i++)
        {
            try
            {
                c.connect(asio::ip::tcp::endpoint(
                    asio::ip::address::from_string("127.0.0.1"), 45451));
                c.close();
                break;
            }
            catch (std::exception e)
            {
                // do nothing.  We expect this to fail while the server is
                // starting up
            }
        }
    }

    // Test blank login credentials
    sendmsg = "POST /login\r\nContent-Length:0\r\n\r\n\r\n";
    {
        send_to_localhost(sendmsg);
        auto return_code = std::string(&buf[9], &buf[12]);
        EXPECT_EQ("400", return_code);
    }

    // Test wrong login credentials
    sendmsg = "POST /login\r\nContent-Length:38\r\n\r\n{\"username\": \"foo\", "
              "\"password\": \"bar\"}\r\n";
    {
        send_to_localhost(sendmsg);
        auto return_code = std::string(&buf[9], &buf[12]);
        EXPECT_EQ("401", return_code);
        // TODO(ed) need to test more here.  Response string?
    }

    // Test only sending a username
    sendmsg =
        "POST /login\r\nContent-Length:19\r\n\r\n{\"username\": \"foo\"}\r\n";
    {
        send_to_localhost(sendmsg);
        auto return_code = std::string(&buf[9], &buf[12]);
        EXPECT_EQ("400", return_code);
    }

    // Test only sending a password
    sendmsg =
        "POST /login\r\nContent-Length:19\r\n\r\n{\"password\": \"foo\"}\r\n";
    {
        send_to_localhost(sendmsg);
        auto return_code = std::string(&buf[9], &buf[12]);
        EXPECT_EQ("400", return_code);
    }

    app.stop();
}

// Test class that allows login for a fixed password.
class KnownLoginAuthenticator
{
  public:
    inline bool authenticate(const std::string& username,
                             const std::string& password)
    {
        return (username == "dude") && (password == "foo");
    }
};

TEST(TokenAuthentication, TestSuccessfulLogin)
{
    App<crow::persistent_data::Middleware,
        crow::token_authorization::Middleware>
        app;
    app.bindaddr("127.0.0.1").port(45451);
    BMCWEB_ROUTE(app, "/")([]() { return boost::beast::http::status::ok; });
    auto _ = async(std::launch::async, [&] { app.run(); });

    asio::io_context is;
    std::array<char, 2048> buf;
    std::string sendmsg;

    auto send_to_localhost = [&is, &buf](std::string sendmsg) {
        asio::ip::tcp::socket c(is);
        c.connect(asio::ip::tcp::endpoint(
            asio::ip::address::from_string("127.0.0.1"), 45451));
        c.send(asio::buffer(sendmsg));
        c.receive(asio::buffer(buf));
        c.close();
    };

    {
        // Retry a couple of times waiting for the server to come up
        asio::ip::tcp::socket c(is);
        for (int i = 0; i < 200; i++)
        {
            try
            {
                c.connect(asio::ip::tcp::endpoint(
                    asio::ip::address::from_string("127.0.0.1"), 45451));
                c.close();
                break;
            }
            catch (std::exception e)
            {
                // do nothing.  We expect this to fail while the server is
                // starting up
            }
        }
    }

    // Test correct login credentials
    sendmsg =
        "POST /login\r\nContent-Length:40\r\n\r\n{\"username\": \"dude\", "
        "\"password\": \"foo\"}\r\n";
    {
        send_to_localhost(sendmsg);
        std::string response(std::begin(buf), std::end(buf));
        // This is a routine to split strings until a newline is hit
        // TODO(ed) this should really use the HTTP parser
        std::vector<std::string> headers;
        std::string::size_type pos = 0;
        std::string::size_type prev = 0;
        int content_length = 0;
        std::string content_encoding("");
        while ((pos = response.find("\r\n", prev)) != std::string::npos)
        {
            auto this_string = response.substr(prev, pos - prev);
            if (this_string == "")
            {
                prev = pos + 2;
                break;
            }

            headers.push_back(this_string);
            prev = pos + 2;
        }
        EXPECT_EQ(headers[0], "HTTP/1.1 200 OK");
        EXPECT_THAT(headers,
                    testing::Contains("Content-Type: application/json"));
        auto http_content = response.substr(prev);
    }

    // Try to use those login credentials to access a resource
    sendmsg = "GET /\r\nAuthorization: token\r\n\r\n{\"username\": \"dude\", "
              "\"password\": \"dude\"}\r\n";
    {
        send_to_localhost(sendmsg);
        auto return_code = std::string(&buf[9], &buf[12]);
        EXPECT_EQ("200", return_code);
    }

    app.stop();
}