#pragma once
#ifndef MESSMER_CPPUTILS_TEST_CRYPTO_SYMMETRIC_TESTUTILS_FAKEAUTHENTICATEDCIPHER_H_
#define MESSMER_CPPUTILS_TEST_CRYPTO_SYMMETRIC_TESTUTILS_FAKEAUTHENTICATEDCIPHER_H_

#include "cpp-utils/crypto/symmetric/Cipher.h"
#include "cpp-utils/data/FixedSizeData.h"
#include "cpp-utils/data/Data.h"
#include "cpp-utils/random/RandomGenerator.h"
#include <random>
#include <cpp-utils/data/SerializationHelper.h>

namespace cpputils {

    struct FakeKey {
        static FakeKey FromString(const std::string& keyData) {
          return FakeKey{static_cast<uint64_t>(std::strtol(keyData.c_str(), nullptr, 10))};
        }

        size_t binaryLength() const {
          return sizeof(uint64_t);
        }

        static FakeKey CreateKey(RandomGenerator *randomGenerator, size_t keySize) {
            ASSERT(keySize == sizeof(uint64_t), "Wrong key size");
            auto data = randomGenerator->getFixedSize<sizeof(uint64_t)>();
            return FakeKey {*reinterpret_cast<uint64_t*>(data.data())};
        }

        uint64_t value;
    };

    // This is a fake cipher that uses an indeterministic xor chiffre and a 8-byte checksum for a simple authentication mechanism
    class FakeAuthenticatedCipher {
    public:
        BOOST_CONCEPT_ASSERT((CipherConcept<FakeAuthenticatedCipher>));

        using EncryptionKey = FakeKey;

        static constexpr unsigned int KEYSIZE = sizeof(uint64_t);
        static constexpr unsigned int STRING_KEYSIZE = 2 * KEYSIZE;

        static EncryptionKey Key1() {
          return FakeKey{5};
        }

        static EncryptionKey Key2() {
          return FakeKey{63};
        }

        static constexpr unsigned int ciphertextSize(unsigned int plaintextBlockSize) {
          return plaintextBlockSize + sizeof(uint64_t) + sizeof(uint64_t);
        }

        static constexpr unsigned int plaintextSize(unsigned int ciphertextBlockSize) {
          return ciphertextBlockSize - sizeof(uint64_t) - sizeof(uint64_t);
        }

        static Data encrypt(const CryptoPP::byte *plaintext, unsigned int plaintextSize, const EncryptionKey &encKey) {
          Data result(ciphertextSize(plaintextSize));

          //Add a random IV
          const uint64_t iv = std::uniform_int_distribution<uint64_t>()(random_);
          serialize<uint64_t>(result.data(), iv);

          //Use xor chiffre on plaintext
          _xor(static_cast<CryptoPP::byte*>(result.dataOffset(sizeof(uint64_t))), plaintext, plaintextSize, encKey.value ^ iv);

          //Add checksum information
          const uint64_t checksum = _checksum(static_cast<const CryptoPP::byte*>(result.data()), encKey, plaintextSize + sizeof(uint64_t));
          serialize<uint64_t>(result.dataOffset(plaintextSize + sizeof(uint64_t)), checksum);

          return result;
        }

        static boost::optional <Data> decrypt(const CryptoPP::byte *ciphertext, unsigned int ciphertextSize,
                                              const EncryptionKey &encKey) {
          //We need at least 16 bytes (iv + checksum)
          if (ciphertextSize < 16) {
            return boost::none;
          }

          //Check checksum
          const uint64_t expectedParity = _checksum(ciphertext, encKey, plaintextSize(ciphertextSize) + sizeof(uint64_t));
          const uint64_t actualParity = deserialize<uint64_t>(ciphertext + plaintextSize(ciphertextSize) + sizeof(uint64_t));
          if (expectedParity != actualParity) {
            return boost::none;
          }

          //Decrypt xor chiffre from ciphertext
          const uint64_t iv = deserialize<uint64_t>(ciphertext);
          Data result(plaintextSize(ciphertextSize));
          _xor(static_cast<CryptoPP::byte *>(result.data()), ciphertext + sizeof(uint64_t), plaintextSize(ciphertextSize), encKey.value ^ iv);

          return result;
        }

        static constexpr const char *NAME = "FakeAuthenticatedCipher";

    private:
        static uint64_t _checksum(const CryptoPP::byte *data, FakeKey encKey, std::size_t size) {
          uint64_t checksum = 34343435 * encKey.value; // some init value

          for (size_t i = 0; i < size; ++i) {
            checksum ^= (static_cast<uint64_t>(data[i]) << (56 - 8 * (i%8)));
          }

          return checksum;
        }

        static void _xor(CryptoPP::byte *dst, const CryptoPP::byte *src, unsigned int size, uint64_t key) {
          for (unsigned int i = 0; i < size; ++i) {
            dst[i] = src[i] ^ ((key >> (56 - 8*(i%8))) & 0xFF);
          }
        }

        static std::random_device random_;
    };

}

#endif
