/*
 * 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 <QAbstractItemModelTester>
#include <QSignalSpy>
#include <QtTest/QtTest>

#include "authentication.h"
#include "common_mocks/authenticatedialogmock.h"
#include "common_mocks/nitrokeymock.h"
#include "common_mocks/newtotpslotdialogmock.h"
#include "keytotpmodel.h"
#include "nitrokeyprovider.h"
#include "totpuriparser.h"

class KeyTOTPModelTest : public QObject
{
    Q_OBJECT

    std::shared_ptr<NitrokeyMock> mockKey;
    std::shared_ptr<NitrokeyProvider> keyProvider;
    std::shared_ptr<NewTOTPSlotDialogMock> newTOTPSlotDialogMock;
    std::shared_ptr<AuthenticateDialogMock> adminAuthenticateDialog;
    std::shared_ptr<Authentication> adminAuthentication;
    std::shared_ptr<AuthenticateDialogMock> userAuthenticateDialog;
    std::shared_ptr<Authentication> userAuthentication;
    std::unique_ptr<KeyTOTPModel> model;
    std::unique_ptr<QAbstractItemModelTester> modelTester;

    std::vector<TOTPSlot> generateTestSlots()
    {
        return {
            { "slot1", 0 },
            { "slot2", 1 },
            { "slot3", 2 },
            { "slot4", 7 }
        };
    }

    const QString TEST_URI;
    const std::string TEST_SLOT_NAME;
    const std::string TEST_URI_SECRET;
    const QString ADMIN_PIN;

    void initAuthAndProvider()
    {
        constexpr int TIMEOUT_MSEC = 1000;
        keyProvider.reset(new NitrokeyProvider(mockKey));
        adminAuthenticateDialog.reset(new AuthenticateDialogMock(AuthenticateDialog::Type::ADMIN));
        adminAuthentication.reset(new Authentication(adminAuthenticateDialog, keyProvider, TIMEOUT_MSEC));
        userAuthenticateDialog.reset(new AuthenticateDialogMock(AuthenticateDialog::Type::USER));
        userAuthentication.reset(new Authentication(userAuthenticateDialog, keyProvider, TIMEOUT_MSEC));
        newTOTPSlotDialogMock.reset(new NewTOTPSlotDialogMock());
    }

public:
    KeyTOTPModelTest()
        : TEST_URI("otpauth://totp/LinkedIn:fsdu39d@protonmail.com?secret=ZQN4SYDIGT3B2WF4GHKJZINYAVLMF3IV&issuer=LinkedIn"),
          TEST_SLOT_NAME("randomName"),
          TEST_URI_SECRET("ZQN4SYDIGT3B2WF4GHKJZINYAVLMF3IV"),
          ADMIN_PIN("12345678") {}

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

    void init()
    {
        mockKey.reset(new NitrokeyMock());
        EXPECT_CALL(*mockKey, connect()).WillRepeatedly(::testing::Return(true));
    }

    void modelReadsSlotsOnCreation()
    {
        // Given
        EXPECT_CALL(*mockKey, getSlots())
                .WillRepeatedly(::testing::Return(generateTestSlots()));
        initAuthAndProvider();
        QSignalSpy spy(keyProvider.get(), SIGNAL(gotSlots(QList<TOTPSlot>)));

        // When
        model.reset(new KeyTOTPModel(keyProvider,
                                     adminAuthenticateDialog,
                                     adminAuthentication,
                                     userAuthenticateDialog,
                                     userAuthentication,
                                     newTOTPSlotDialogMock));
        modelTester.reset(new QAbstractItemModelTester(model.get()));

        // Then
        spy.wait(100);
        QTRY_VERIFY2(spy.count() > 0, "Key provider never emitted TOTP slots");
    }

    void modelReturnsProperNumberOrRows()
    {
        // Given
        const auto SLOTS = generateTestSlots();
        auto readSlotsAsync = [&SLOTS]()
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            return SLOTS;
        };
        EXPECT_CALL(*mockKey, getSlots())
                .WillRepeatedly(::testing::Invoke(readSlotsAsync));
        initAuthAndProvider();

        // When
        model.reset(new KeyTOTPModel(keyProvider,
                                     adminAuthenticateDialog,
                                     adminAuthentication,
                                     userAuthenticateDialog,
                                     userAuthentication,
                                     newTOTPSlotDialogMock));
        modelTester.reset(new QAbstractItemModelTester(model.get()));

        // Then
        QTRY_COMPARE_WITH_TIMEOUT(static_cast<std::size_t>(model->rowCount()),
                                  SLOTS.size(),
                                  200);
    }

    void modelReturnsIconsAsData()
    {
        // Given
        const auto SLOTS = generateTestSlots();
        auto readSlotsAsync = [&SLOTS]()
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            return SLOTS;
        };
        EXPECT_CALL(*mockKey, getSlots())
                .WillRepeatedly(::testing::Invoke(readSlotsAsync));
        initAuthAndProvider();

        // When
        model.reset(new KeyTOTPModel(keyProvider,
                                     adminAuthenticateDialog,
                                     adminAuthentication,
                                     userAuthenticateDialog,
                                     userAuthentication,
                                     newTOTPSlotDialogMock));
        modelTester.reset(new QAbstractItemModelTester(model.get()));

        // Then
        for (std::size_t idx = 0; idx < SLOTS.size(); ++idx)
        {
            QTRY_VERIFY2(model->data(model->index(idx), Qt::UserRole).canConvert<QIcon>(), "Returned data should be a QIcon instance");
            QTRY_VERIFY2(model->data(model->index(idx), Qt::DecorationRole).canConvert<QIcon>(), "Returned data should be a QIcon instance");
        }
    }

    void modelWritesNewEntryIntoCorrectSlot()
    {
        // Given
        const auto SLOTS = generateTestSlots();
        auto readSlotsAsync = [&SLOTS]()
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            return SLOTS;
        };

        EXPECT_CALL(*mockKey, getSlots())
                .WillOnce(::testing::Invoke(readSlotsAsync))
                .WillOnce(::testing::Invoke(readSlotsAsync));
        initAuthAndProvider();
        EXPECT_CALL(*adminAuthenticateDialog, exec()).WillOnce(::testing::Return(QDialog::Accepted));
        EXPECT_CALL(*adminAuthenticateDialog, pin()).WillOnce(::testing::Return(QString(ADMIN_PIN)));
        EXPECT_CALL(*mockKey, firstAuth(
                        ::testing::Eq(ADMIN_PIN.toStdString()),
                        ::testing::_)).Times(1);
        EXPECT_CALL(*newTOTPSlotDialogMock, exec()).WillOnce(::testing::Return(QDialog::Accepted));
        EXPECT_CALL(*newTOTPSlotDialogMock, slotName()). WillOnce(::testing::Return(QString(TEST_SLOT_NAME.c_str())));

        model.reset(new KeyTOTPModel(keyProvider,
                                     adminAuthenticateDialog,
                                     adminAuthentication,
                                     userAuthenticateDialog,
                                     userAuthentication,
                                     newTOTPSlotDialogMock));
        modelTester.reset(new QAbstractItemModelTester(model.get()));

        auto parser = std::unique_ptr<TotpUriParser>(new TotpUriParser(TEST_URI));
        QSignalSpy spy(keyProvider.get(), &NitrokeyProvider::totpSlotWritten);

        // When
        model->addNewSlot(std::move(parser));

        // Then
        EXPECT_CALL(*mockKey, writeTotpSlot(
                        ::testing::Eq(3),
                        ::testing::Eq(TEST_SLOT_NAME),
                        ::testing::_,
                        ::testing::Eq(30),
                        ::testing::_))
                .Times(1);
        spy.wait();
    }

    void modelDoesNotWriteSlotIfCanceledInSlotNameDialog()
    {
        // Given
        const auto SLOTS = generateTestSlots();
        auto readSlotsAsync = [&SLOTS]()
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            return SLOTS;
        };

        EXPECT_CALL(*mockKey, getSlots())
                .WillOnce(::testing::Invoke(readSlotsAsync));
        initAuthAndProvider();
        EXPECT_CALL(*newTOTPSlotDialogMock, exec()).WillOnce(::testing::Return(QDialog::Rejected));

        model.reset(new KeyTOTPModel(keyProvider,
                                     adminAuthenticateDialog,
                                     adminAuthentication,
                                     userAuthenticateDialog,
                                     userAuthentication,
                                     newTOTPSlotDialogMock));
        modelTester.reset(new QAbstractItemModelTester(model.get()));

        auto parser = std::unique_ptr<TotpUriParser>(new TotpUriParser(TEST_URI));
        QSignalSpy spy(keyProvider.get(), &NitrokeyProvider::totpSlotWritten);

        // When
        model->addNewSlot(std::move(parser));

        // Then
        EXPECT_CALL(*mockKey, writeTotpSlot(
                        ::testing::_,
                        ::testing::_,
                        ::testing::_,
                        ::testing::_,
                        ::testing::_))
                .Times(0);
        spy.wait(300);
        QTRY_VERIFY2(0 == spy.count(),
                     "When canceled in New TOTP Slot dialog, the slot should not have been saved.");
    }
};

QTEST_MAIN(KeyTOTPModelTest)
#include "keytotpmodeltest.moc"
