/*
 * 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 <memory>
#include <thread>

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

#include "gtest/gtest.h"

#include "authentication.h"
#include "authenticationexception.h"
#include "nitrokeyprovider.h"
#include "common_mocks/authenticatedialogmock.h"
#include "common_mocks/nitrokeymock.h"
#include "randompasswordgenerator.h"
#include "unauthenticatedexception.h"

class AuthenticationTests : public QObject
{
    Q_OBJECT

    std::shared_ptr<NitrokeyMock> keyMock;
    std::shared_ptr<NitrokeyProvider> keyProvider;
    std::shared_ptr<AuthenticateDialogMock> dialog;
    RandomPasswordGenerator passwordGenerator;

    std::string passedPin;
    std::string passedPassword;

    static constexpr unsigned int TIMEOUT = 1000;
    std::string adminPin;

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

    bool saveTmpPassAndSleep(const std::string &pin, const std::string &tempPassword)
    {
        passedPin = pin;
        passedPassword = tempPassword;
        return sleepAndReturnBool(true);
    }

    bool sleepAndreturnFalse()
    {
        return sleepAndReturnBool(false);
    }

    void commonDialogExpectations()
    {
        EXPECT_CALL(*dialog, exec()).WillOnce(::testing::Return(QDialog::Accepted));
        EXPECT_CALL(*dialog, pin()).WillOnce(::testing::Return(QString(adminPin.c_str())));

    }

    void commonExpectations()
    {
        commonDialogExpectations();
        EXPECT_CALL(*keyMock, firstAuth(::testing::_,
                                        ::testing::_))
                .WillOnce(::testing::Invoke(this, &AuthenticationTests::saveTmpPassAndSleep));

    }

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

    void init()
    {
        adminPin = "12345678";
        keyMock.reset(new NitrokeyMock);
        keyProvider.reset(new NitrokeyProvider(keyMock));
        dialog.reset(new AuthenticateDialogMock(AuthenticateDialog::Type::ADMIN));
    }

    void authenticationDialogAskedForPin()
    {
        // Given
        commonExpectations();
        Authentication auth(dialog, keyProvider, TIMEOUT);
        QSignalSpy spy(&auth, &Authentication::authenticated);

        // When
        auth.authenticate();

        // Then
        spy.wait();
        EXPECT_GE(spy.count(), 1);
        // EXPECT_CALL won't throw exception
    }

    void authenticationDialogCallsAdminAuthOnKeyWithProperPin()
    {
        // Given
        commonExpectations();
        Authentication auth(dialog, keyProvider, TIMEOUT);
        QSignalSpy spy(&auth, &Authentication::authenticated);

        // When
        auth.authenticate();

        // Then
        spy.wait();
        EXPECT_EQ(adminPin, passedPin);
    }

    void authenticationReturnsProperTempPassword()
    {
        // Given
        commonExpectations();
        Authentication auth(dialog, keyProvider, TIMEOUT);
        QSignalSpy spy(&auth, &Authentication::authenticated);
        auth.authenticate();
        spy.wait();
        RandomPasswordGenerator::KeyPassword authPass;
        std::copy(passedPassword.begin(), passedPassword.end(), authPass.begin());

        // When
        auto reReadPass = auth.password();

        // Then
        EXPECT_THAT(reReadPass, ::testing::ElementsAreArray(passedPassword));
    }

    void passwordThrowsExceptionWhenNotYetAuthenticated()
    {
        // Given
        Authentication auth(dialog, keyProvider, TIMEOUT);

        // When
        QVERIFY_EXCEPTION_THROWN(auth.password(), UnauthenticatedException);

        // Then
        // Expected exception got thrown
    }

    void passwordGetsScrubbedAfterTimeout()
    {
        // Given
        constexpr int SHORT_TIMEOUT = 100;
        commonExpectations();
        Authentication auth(dialog, keyProvider, SHORT_TIMEOUT);
        QSignalSpy spy(&auth, &Authentication::authenticated);
        auth.authenticate();
        spy.wait();
        auto doesPasswordThrow = [&auth]() mutable {
            try {
                auth.password();
                return false;
            } catch (const UnauthenticatedException &) {
                return true;
            }
        };

        // When
        QTRY_VERIFY2_WITH_TIMEOUT(doesPasswordThrow(),
                                  "Authentication should timeout and start throwking UnauthenticatedException after session expires.",
                                  SHORT_TIMEOUT * 2);

        // Then
        // Expected exception got thrown
    }

    void authenticationFailureIsEmittedWhenKeyImplThrowsException()
    {
        // Given
        dialog.reset(new AuthenticateDialogMock(AuthenticateDialog::Type::USER));
        constexpr int SHORT_TIMEOUT = 100;
        commonDialogExpectations();
        EXPECT_CALL(*keyMock, userAuth(::testing::_,
                                       ::testing::_))
                .WillOnce(::testing::Throw(AuthenticationException("Auth error")));
        Authentication auth(dialog, keyProvider, SHORT_TIMEOUT);
        QSignalSpy spy(&auth, &Authentication::authenticationFailure);

        // When
        auth.authenticate();

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

    void authenticationGetsIntoUnauthenticatedStateAfterFailedAuthAttempt()
    {
        // Given
        dialog.reset(new AuthenticateDialogMock(AuthenticateDialog::Type::USER));
        constexpr int SHORT_TIMEOUT = 100;
        commonDialogExpectations();
        EXPECT_CALL(*keyMock, userAuth(::testing::_,
                                       ::testing::_))
                .WillOnce(::testing::Throw(AuthenticationException("Auth error")));
        Authentication auth(dialog, keyProvider, SHORT_TIMEOUT);
        QSignalSpy spy(&auth, &Authentication::authenticationFailure);
        auth.authenticate();
        spy.wait();

        // When
        QVERIFY_EXCEPTION_THROWN(auth.password(), UnauthenticatedException);

        // Then
        // Exception was thrown in password().
    }

    void authenticationEmitsAuthenticationCanceledWhenCancelingInDialog()
    {
        // Given
        constexpr int SHORT_TIMEOUT = 100;
        EXPECT_CALL(*dialog, exec()).WillOnce(::testing::Return(QDialog::Rejected));
        Authentication auth(dialog, keyProvider, SHORT_TIMEOUT);
        QSignalSpy spy(&auth, &Authentication::authenticationCanceled);

        // When
        auth.authenticate();

        // Then
        EXPECT_EQ(spy.count(), 1);
        QVERIFY_EXCEPTION_THROWN(auth.password(), UnauthenticatedException);
    }
};

QTEST_MAIN(AuthenticationTests)
#include "authenticationtest.moc"
