/*
 * BSD 2-Clause License
 *
 * Copyright (c) 2020, Agnieszka Cicha-Cisek
 * Copyright (c) 2020, Patryk Cisek
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <chrono>
#include <thread>

#include <QSignalSpy>
#include <QtTest/QtTest>

#include "authenticationexception.h"
#include "common_mocks/nitrokeymock.h"
#include "nitrokeyimpl.h"
#include "nitrokeyprovider.h"
#include "randompasswordgenerator.h"
#include "totpslot.h"

class NitrokeyProviderTests : public QObject
{
    Q_OBJECT

    static bool sleepAndReturnBool(bool returnValue)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        return returnValue;
    }

    static bool sleepAndReturnFalse()
    {
        return sleepAndReturnBool(false);
    }

    static bool sleepAndReturnTrue()
    {
        return sleepAndReturnBool(true);
    }

    RandomPasswordGenerator passGenerator;

private slots:
    void providerConnectsToNitrokey()
    {
        // Given
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        EXPECT_CALL(mockRef, connect()).Times(::testing::AtLeast(1));
        NitrokeyProvider provider(mock);

        // When
        provider.connect();

        // Then
        // GoogleMock throws exception on failure to satisfy pre-set
        // expectations.
    }

    void providerEmitsConnectedWhenConnected()
    {
        // Given
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);

        EXPECT_CALL(mockRef, connect())
                .WillOnce(::testing::Invoke(sleepAndReturnTrue));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, SIGNAL(connected()));

        // When
        provider.connect();

        // Then
        spy.wait();
        QVERIFY(spy.count() == 1);
    }

    void isConnectedReturnsValidResultWhenCalled()
    {
        // Given
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        const std::vector<bool> EXPECTED_RESPONSES = {
            true, false, true, true, false, false
        };
        EXPECT_CALL(mockRef, isConnected())
                .WillOnce(::testing::Return(EXPECTED_RESPONSES[0]))
                .WillOnce(::testing::Return(EXPECTED_RESPONSES[1]))
                .WillOnce(::testing::Return(EXPECTED_RESPONSES[2]))
                .WillOnce(::testing::Return(EXPECTED_RESPONSES[3]))
                .WillOnce(::testing::Return(EXPECTED_RESPONSES[4]))
                .WillOnce(::testing::Return(EXPECTED_RESPONSES[5]));
        std::vector<bool> responses;

        // When
        std::for_each(std::begin(EXPECTED_RESPONSES),
                      std::end(EXPECTED_RESPONSES),
                      [&responses, &mockRef](const bool response) mutable {
            responses.push_back(mockRef.isConnected());
        });

        // Then
        EXPECT_EQ(EXPECTED_RESPONSES, responses);
    }

    void providerEmitsSlotsWhenRequested()
    {
        // Given
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        const std::vector<TOTPSlot> EXPECTED_SLOTS =
        {
            { "Name1", 2 },
            { "Name2", 4 },
            { "Name3", 5 }
        };
        auto getSlotsMock = [=]()
        {
            sleepAndReturnTrue();
            return EXPECTED_SLOTS;
        };
        EXPECT_CALL(mockRef, getSlots())
                .WillOnce(::testing::Invoke(getSlotsMock));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, SIGNAL(gotSlots(QList<TOTPSlot>)));

        // When
        provider.getSlots();

        // Then
        spy.wait();
        QVERIFY(spy.count() >= 1);
        QList<QVariant> arguments = spy.takeFirst();
        QList<TOTPSlot> receivedArgs = arguments[0].value<QList<TOTPSlot>>();
        for (int i = 0; i < 3; ++i)
        {
            QCOMPARE(EXPECTED_SLOTS[i].slotName(), receivedArgs[i].slotName());
            QCOMPARE(EXPECTED_SLOTS[i].slotNumber(), receivedArgs[i].slotNumber());
        }
    }

    void providerCallsFirstAuthWhenRequested()
    {
        // Given
        const auto TEMP_PASS = passGenerator.generatePassword();
        const QString ADMIN_PIN("12345678");
        auto firstAuthMock = [=](const std::string &, const std::string &) mutable
        {
            return sleepAndReturnTrue();
        };
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        EXPECT_CALL(mockRef, firstAuth(::testing::Eq(ADMIN_PIN.toStdString()),
                                       ::testing::ElementsAreArray(TEMP_PASS)))
                .WillOnce(::testing::Invoke(firstAuthMock));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, &NitrokeyProvider::adminAuthenticated);

        // When
        provider.adminAuth(ADMIN_PIN, TEMP_PASS);

        // Then
        spy.wait();
        ASSERT_GE(spy.count(), 1);
    }

    void providerCallsUserAuthWhenRequested()
    {
        // Given
        const auto TEMP_PASS = passGenerator.generatePassword();
        const QString USER_PIN("123456");
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        auto userAuthMock = [=](const std::string &, const std::string &) mutable
        {
            sleepAndReturnTrue();
        };
        EXPECT_CALL(mockRef, userAuth(::testing::Eq(USER_PIN.toStdString()),
                                      ::testing::ElementsAreArray(TEMP_PASS)))
                .WillOnce(::testing::Invoke(userAuthMock));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, SIGNAL(userAuthenticated()));

        // When
        provider.userAuth(USER_PIN, TEMP_PASS);

        // Then
        spy.wait();
        ASSERT_GE(spy.count(), 1);
    }

    void providerEmitsUserAuthFailureWhenKeyThrowsException()
    {
        // Given
        const auto TEMP_PASS = passGenerator.generatePassword();
        const QString USER_PIN("123456");
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        EXPECT_CALL(mockRef, userAuth(::testing::Eq(USER_PIN.toStdString()),
                                      ::testing::ElementsAreArray(TEMP_PASS)))
                .WillOnce(::testing::Throw(AuthenticationException("Auth error")));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, &NitrokeyProvider::userAuthenticationFailure);

        // When
        provider.userAuth(USER_PIN, TEMP_PASS);

        // Then
        spy.wait();
        ASSERT_EQ(spy.count(), 1);
    }

    void providerEmitsAdminAuthFailureWhenKeyThrowsException()
    {
        // Given
        const auto TEMP_PASS = passGenerator.generatePassword();
        const QString ADMIN_PIN("12345678");
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        EXPECT_CALL(mockRef, firstAuth(::testing::Eq(ADMIN_PIN.toStdString()),
                                       ::testing::ElementsAreArray(TEMP_PASS)))
                .WillOnce(::testing::Throw(AuthenticationException("Auth error")));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, &NitrokeyProvider::adminAuthenticationFailure);

        // When
        provider.adminAuth(ADMIN_PIN, TEMP_PASS);

        // Then
        spy.wait();
        ASSERT_EQ(spy.count(), 1);
    }

    void providerCallsTotpWriteAndEmitsSuccessSignalWhenRequested()
    {
        // Given
        const auto TEMP_PASS = passGenerator.generatePassword();
        const QString ADMIN_PIN("12345678");
        auto firstAuthMock = [=](const std::string &, const std::string &) mutable
        {
            return sleepAndReturnTrue();
        };
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        EXPECT_CALL(mockRef, firstAuth(::testing::Eq(ADMIN_PIN.toStdString()),
                                       ::testing::ElementsAreArray(TEMP_PASS)))
                .WillOnce(::testing::Invoke(firstAuthMock));
        NitrokeyProvider provider(mock);
        QSignalSpy spyAuth(&provider, &NitrokeyProvider::adminAuthenticated);
        provider.adminAuth(ADMIN_PIN, TEMP_PASS);
        spyAuth.wait();
        const std::string SLOT_NAME = "someName";
        const std::uint16_t SLOT_NO = 5;
        TOTPSlot slot(SLOT_NAME, SLOT_NO);
        std::uint16_t TIME_WINDOW = 30;
        const QString HEX_SECRET = "1234567890";
        QSignalSpy spyWrite(&provider, &NitrokeyProvider::totpSlotWritten);
        EXPECT_CALL(mockRef, writeTotpSlot(
                        ::testing::Eq(slot.slotNumber()),
                        ::testing::Eq(slot.slotName()),
                        ::testing::Eq(HEX_SECRET.toStdString()),
                        ::testing::Eq(TIME_WINDOW),
                        ::testing::_));

        // When
        provider.writeTotpSlot(slot, TIME_WINDOW, HEX_SECRET, TEMP_PASS);

        // Then
        spyWrite.wait();
        ASSERT_GE(spyWrite.count(), 1);
    }

    void writesTOTPSlotWhenRequested()
    {
        // Given
        const auto TEMP_PASS = passGenerator.generatePassword();
        const std::uint8_t SLOT_NUMBER = 7;
        const unsigned int UNIX_TIMESTAMP = 654654654;
        const QString USER_PASS = "123456";
        const TOTPSlot aSlot("aSlotName", SLOT_NUMBER);
        const std::string TOTP_CODE("495472");

        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);

        EXPECT_CALL(mockRef, setTimeSoft(::testing::Eq(UNIX_TIMESTAMP)))
                .Times(1);
        EXPECT_CALL(mockRef, getTOTPCode(
                        ::testing::Eq(aSlot),
                        ::testing::_))
                .WillOnce(::testing::Return(TOTP_CODE));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, &NitrokeyProvider::gotTOTPCode);

        // When
        provider.getTOTPCode(aSlot, UNIX_TIMESTAMP, TEMP_PASS);

        // Then
        spy.wait();
        ASSERT_EQ(spy.count(), 1);
        QList<QVariant> signalArgs = spy.takeFirst();
        const GetTOTPCodeResponse totpCode =
                signalArgs[0].value<GetTOTPCodeResponse>();
        EXPECT_EQ(totpCode.TOTP_CODE.toStdString(), TOTP_CODE);
        EXPECT_EQ(totpCode.SLOT_NUMBER, SLOT_NUMBER);
        EXPECT_EQ(totpCode.UNIX_TIMESTAMP, UNIX_TIMESTAMP);
    }

    void eraseTOTPSlotWhenRequested()
    {
        // Given
        const auto tempPass = passGenerator.generatePassword();
        const std::string SLOT_NAME("SomeRandomSlot");
        constexpr std::uint8_t SLOT_NUMBER = 2;
        const TOTPSlot slotToBeErased(SLOT_NAME, SLOT_NUMBER);
        NitrokeyMock &mockRef = dynamic_cast<NitrokeyMock&>(*mock);
        EXPECT_CALL(mockRef, eraseTotpSlot(
                        ::testing::Eq(slotToBeErased.slotNumber()),
                        ::testing::_));
        NitrokeyProvider provider(mock);
        QSignalSpy spy(&provider, &NitrokeyProvider::totpSlotErased);

        // When
        provider.eraseTotpSlot(slotToBeErased, tempPass);

        // Then
        spy.wait();
    }

    void initTestCase()
    {
        ::testing::GTEST_FLAG(throw_on_failure) = true;
    }

    void init()
    {
        mock.reset(new NitrokeyMock);
    }

private:
    std::shared_ptr<NitrokeyBase> mock;
};

QTEST_MAIN(NitrokeyProviderTests)
#include "nitrokeyprovidertest.moc"
