/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "InputHub.h"

#include <chrono>
#include <memory>
#include <mutex>

#include <linux/input.h>

#include <gtest/gtest.h>

#include <utils/StopWatch.h>
#include <utils/Timers.h>

#include "TestHelpers.h"

// # of milliseconds to fudge stopwatch measurements
#define TIMING_TOLERANCE_MS 25
#define NO_TIMEOUT (-1)

namespace android {
namespace tests {

using namespace std::literals::chrono_literals;

using InputCbFunc = std::function<void(const std::shared_ptr<InputDeviceNode>&, InputEvent&, nsecs_t)>;
using DeviceCbFunc = std::function<void(const std::shared_ptr<InputDeviceNode>&)>;

static const InputCbFunc kNoopInputCb = [](const std::shared_ptr<InputDeviceNode>&, InputEvent&, nsecs_t){};
static const DeviceCbFunc kNoopDeviceCb = [](const std::shared_ptr<InputDeviceNode>&){};

class TestInputCallback : public InputCallbackInterface {
public:
    TestInputCallback() :
        mInputCb(kNoopInputCb), mDeviceAddedCb(kNoopDeviceCb), mDeviceRemovedCb(kNoopDeviceCb) {}
    virtual ~TestInputCallback() = default;

    void setInputCallback(InputCbFunc cb) { mInputCb = cb; }
    void setDeviceAddedCallback(DeviceCbFunc cb) { mDeviceAddedCb = cb; }
    void setDeviceRemovedCallback(DeviceCbFunc cb) { mDeviceRemovedCb = cb; }

    virtual void onInputEvent(const std::shared_ptr<InputDeviceNode>& node, InputEvent& event,
            nsecs_t event_time) override {
        mInputCb(node, event, event_time);
    }
    virtual void onDeviceAdded(const std::shared_ptr<InputDeviceNode>& node) override {
        mDeviceAddedCb(node);
    }
    virtual void onDeviceRemoved(const std::shared_ptr<InputDeviceNode>& node) override {
        mDeviceRemovedCb(node);
    }

private:
    InputCbFunc mInputCb;
    DeviceCbFunc mDeviceAddedCb;
    DeviceCbFunc mDeviceRemovedCb;
};

class InputHubTest : public ::testing::Test {
 protected:
     virtual void SetUp() {
         mCallback = std::make_shared<TestInputCallback>();
         mInputHub = std::make_shared<InputHub>(mCallback);
     }

     std::shared_ptr<TestInputCallback> mCallback;
     std::shared_ptr<InputHub> mInputHub;
};

TEST_F(InputHubTest, testWake) {
    // Call wake() after 100ms.
    auto f = delay_async(100ms, [&]() { EXPECT_EQ(OK, mInputHub->wake()); });

    StopWatch stopWatch("poll");
    EXPECT_EQ(OK, mInputHub->poll());
    int32_t elapsedMillis = ns2ms(stopWatch.elapsedTime());

    EXPECT_NEAR(100, elapsedMillis, TIMING_TOLERANCE_MS);
}

TEST_F(InputHubTest, DISABLED_testDeviceAdded) {
    auto tempDir = std::make_shared<TempDir>();
    std::string pathname;
    // Expect that this callback will run and set handle and pathname.
    mCallback->setDeviceAddedCallback(
            [&](const std::shared_ptr<InputDeviceNode>& node) {
                pathname = node->getPath();
            });

    ASSERT_EQ(OK, mInputHub->registerDevicePath(tempDir->getName()));

    // Create a new file in tempDir after 100ms.
    std::unique_ptr<TempFile> tempFile;
    std::mutex tempFileMutex;
    auto f = delay_async(100ms,
            [&]() {
                std::lock_guard<std::mutex> lock(tempFileMutex);
                tempFile.reset(tempDir->newTempFile());
            });

    StopWatch stopWatch("poll");
    EXPECT_EQ(OK, mInputHub->poll());
    int32_t elapsedMillis = ns2ms(stopWatch.elapsedTime());


    EXPECT_NEAR(100, elapsedMillis, TIMING_TOLERANCE_MS);
    std::lock_guard<std::mutex> lock(tempFileMutex);
    EXPECT_EQ(tempFile->getName(), pathname);
}

TEST_F(InputHubTest, DISABLED_testDeviceRemoved) {
    // Create a temp dir and file. Save its name and handle (to be filled in
    // once InputHub scans the dir).
    auto tempDir = std::make_unique<TempDir>();
    auto deviceFile = std::unique_ptr<TempFile>(tempDir->newTempFile());
    std::string tempFileName(deviceFile->getName());

    std::shared_ptr<InputDeviceNode> tempNode;
    // Expect that these callbacks will run for the above device file.
    mCallback->setDeviceAddedCallback(
            [&](const std::shared_ptr<InputDeviceNode>& node) {
                tempNode = node;
            });
    mCallback->setDeviceRemovedCallback(
            [&](const std::shared_ptr<InputDeviceNode>& node) {
                EXPECT_EQ(tempNode, node);
            });

    ASSERT_EQ(OK, mInputHub->registerDevicePath(tempDir->getName()));
    // Ensure that tempDir was scanned to find the device.
    ASSERT_TRUE(tempNode != nullptr);

    auto f = delay_async(100ms, [&]() { deviceFile.reset(); });

    StopWatch stopWatch("poll");
    EXPECT_EQ(OK, mInputHub->poll());
    int32_t elapsedMillis = ns2ms(stopWatch.elapsedTime());

    EXPECT_NEAR(100, elapsedMillis, TIMING_TOLERANCE_MS);
}

TEST_F(InputHubTest, DISABLED_testInputEvent) {
    // Create a temp dir and file. Save its name and handle (to be filled in
    // once InputHub scans the dir.)
    auto tempDir = std::make_unique<TempDir>();
    auto deviceFile = std::unique_ptr<TempFile>(tempDir->newTempFile());
    std::string tempFileName(deviceFile->getName());

    // Send a key event corresponding to HOME.
    struct input_event iev;
    iev.time = { 1, 0 };
    iev.type = EV_KEY;
    iev.code = KEY_HOME;
    iev.value = 0x01;

    auto inputDelayMs = 100ms;
    auto f = delay_async(inputDelayMs, [&] {
                ssize_t nWrite = TEMP_FAILURE_RETRY(write(deviceFile->getFd(), &iev, sizeof(iev)));

                ASSERT_EQ(static_cast<ssize_t>(sizeof(iev)), nWrite) << "could not write to "
                    << deviceFile->getFd() << ". errno: " << errno;
            });

    // Expect this callback to run when the input event is read.
    nsecs_t expectedWhen = systemTime(CLOCK_MONOTONIC) + ms2ns(inputDelayMs.count());
    mCallback->setInputCallback(
            [&](const std::shared_ptr<InputDeviceNode>& node, InputEvent& event,
                nsecs_t event_time) {
                EXPECT_NEAR(expectedWhen, event_time, ms2ns(TIMING_TOLERANCE_MS));
                EXPECT_EQ(s2ns(1), event.when);
                EXPECT_EQ(tempFileName, node->getPath());
                EXPECT_EQ(EV_KEY, event.type);
                EXPECT_EQ(KEY_HOME, event.code);
                EXPECT_EQ(0x01, event.value);
            });
    ASSERT_EQ(OK, mInputHub->registerDevicePath(tempDir->getName()));

    StopWatch stopWatch("poll");
    EXPECT_EQ(OK, mInputHub->poll());
    int32_t elapsedMillis = ns2ms(stopWatch.elapsedTime());

    EXPECT_NEAR(100, elapsedMillis, TIMING_TOLERANCE_MS);
}

TEST_F(InputHubTest, DISABLED_testCallbackOrder) {
    // Create two "devices": one to receive input and the other to go away.
    auto tempDir = std::make_unique<TempDir>();
    auto deviceFile1 = std::unique_ptr<TempFile>(tempDir->newTempFile());
    auto deviceFile2 = std::unique_ptr<TempFile>(tempDir->newTempFile());
    std::string tempFileName(deviceFile2->getName());

    bool inputCallbackFinished = false, deviceCallbackFinished = false;

    // Setup the callback for input events. Should run before the device
    // callback.
    mCallback->setInputCallback(
            [&](const std::shared_ptr<InputDeviceNode>&, InputEvent&, nsecs_t) {
                ASSERT_FALSE(deviceCallbackFinished);
                inputCallbackFinished = true;
            });

    // Setup the callback for device removal. Should run after the input
    // callback.
    mCallback->setDeviceRemovedCallback(
            [&](const std::shared_ptr<InputDeviceNode>& node) {
                ASSERT_TRUE(inputCallbackFinished)
                    << "input callback did not run before device changed callback";
                // Make sure the correct device was removed.
                EXPECT_EQ(tempFileName, node->getPath());
                deviceCallbackFinished = true;
            });
    ASSERT_EQ(OK, mInputHub->registerDevicePath(tempDir->getName()));

    auto f = delay_async(100ms,
            [&]() {
                // Delete the second device file first.
                deviceFile2.reset();

                // Then inject an input event into the first device.
                struct input_event iev;
                iev.time = { 1, 0 };
                iev.type = EV_KEY;
                iev.code = KEY_HOME;
                iev.value = 0x01;

                ssize_t nWrite = TEMP_FAILURE_RETRY(write(deviceFile1->getFd(), &iev, sizeof(iev)));

                ASSERT_EQ(static_cast<ssize_t>(sizeof(iev)), nWrite) << "could not write to "
                    << deviceFile1->getFd() << ". errno: " << errno;
            });

    StopWatch stopWatch("poll");
    EXPECT_EQ(OK, mInputHub->poll());
    int32_t elapsedMillis = ns2ms(stopWatch.elapsedTime());

    EXPECT_NEAR(100, elapsedMillis, TIMING_TOLERANCE_MS);
    EXPECT_TRUE(inputCallbackFinished);
    EXPECT_TRUE(deviceCallbackFinished);
}

}  // namespace tests
}  // namespace android