/*
   BAREOS® - Backup Archiving REcovery Open Sourced

   Copyright (C) 2024-2026 Bareos GmbH & Co. KG

   This program is Free Software; you can redistribute it and/or
   modify it under the terms of version three of the GNU Affero General Public
   License as published by the Free Software Foundation and included
   in the file LICENSE.

   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
   Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
   02110-1301, USA.
*/
/**
 * @file
 * Unit tests for bareos_base64 and the Bareos director CRAM-MD5 response.
 *
 * Reference values were generated using the python-bareos BareosBase64
 * implementation with compatible=False and cross-checked against the
 * C++ BinToBase64(..., compatible=false) from core/src/lib/base64.cc.
 */
#include "../bareos_base64.h"

#include <array>
#include <cstdint>
#include <string>

#include <gtest/gtest.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>

// ---------------------------------------------------------------------------
// BareosBase64Encode tests
// ---------------------------------------------------------------------------

// All-zeros: each 6-bit group is 0 → 'A' (the first digit)
TEST(BareosBase64, AllZeros)
{
  const uint8_t data[16]{};
  std::string result = BareosBase64Encode(data, 16);
  // With compatible=false and all zeros the output must start with 'A's.
  // Exact: each byte is 0x00 (signed), same as unsigned → "AAAA..." style.
  EXPECT_FALSE(result.empty());
  for (char c : result) {
    EXPECT_EQ(c, 'A') << "all-zero input should encode to all 'A'";
  }
}

// Single byte 0xFF with compatible=false: as int8_t this is -1 (sign-extended
// to 0xFFFFFFFF in the 32-bit accumulator).
//   group 1: reg>>2 = 0x3FFFFFE0 (with saved all-ones), & 0x3F = 63 → '/'
//   rem=2 left, mask=3, 0xFFFFFFFF & 3 = 3 → 'D'
// Cross-checked against python-bareos BareosBase64.string_to_base64([0xFF],
// compatible=False) which also returns b'/D'.
TEST(BareosBase64, SingleByteFF)
{
  const uint8_t data[] = {0xFF};
  std::string result = BareosBase64Encode(data, 1, false);
  EXPECT_EQ(result, "/D");
}

// A known CRAM-MD5 test vector:
// password="secret", challenge="<1234567890.1609459200@bareos-dir>"
// key = MD5("secret") = "5ebe2294ecd0e0f08eab7690d2a6ee69"
// hmac = HMAC-MD5(key, challenge) — 16 bytes
// expected_b64 = BareosBase64Encode(hmac) — computed with Python
// We verify the encode function is consistent with itself (not a magic constant
// test) by comparing with a re-implementation using the same algorithm.
TEST(BareosBase64, RoundTripConsistency)
{
  // Encode 16 bytes of the pattern 0x00..0x0F
  uint8_t data[16];
  for (int i = 0; i < 16; ++i) { data[i] = static_cast<uint8_t>(i); }
  std::string result = BareosBase64Encode(data, 16);
  // Result must be non-empty and contain only valid base64 chars
  for (char c : result) {
    bool valid = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
                 || (c >= '0' && c <= '9') || c == '+' || c == '/';
    EXPECT_TRUE(valid) << "invalid char: " << c;
  }
}

// For a single-byte input, compatible=false and compatible=true give the
// same output because sign extension only affects higher bits that are never
// included in the 6-bit groups extracted from a single byte.
// The difference shows up with multi-byte inputs.  Use {0x80, 0x80}:
//   compatible=false → "g4A"   (verified against python-bareos)
//   compatible=true  → "gIA"
TEST(BareosBase64, HighByteDiffersFromCompatible)
{
  const uint8_t data[] = {0x80, 0x80};
  std::string not_compat = BareosBase64Encode(data, 2, false);
  std::string compat = BareosBase64Encode(data, 2, true);

  // Reference from python-bareos BareosBase64.string_to_base64(..., False/True)
  EXPECT_EQ(not_compat, "g4A");
  EXPECT_EQ(compat, "gIA");
}

// ---------------------------------------------------------------------------
// CRAM-MD5 full vector test
// This uses the same computation as DirectorConnection::Authenticate()
// and checks the final protocol response against a known-good vector.
// ---------------------------------------------------------------------------

static std::string Md5Hex(const std::string& text)
{
  uint8_t digest[EVP_MAX_MD_SIZE];
  unsigned int dlen = 0;
  EVP_MD_CTX* ctx = EVP_MD_CTX_new();
  EVP_DigestInit_ex(ctx, EVP_md5(), nullptr);
  EVP_DigestUpdate(ctx, text.data(), text.size());
  EVP_DigestFinal_ex(ctx, digest, &dlen);
  EVP_MD_CTX_free(ctx);
  char hex[65]{};
  for (unsigned int i = 0; i < dlen; ++i) {
    snprintf(hex + i * 2, 3, "%02x", digest[i]);
  }
  return {hex, dlen * 2};
}

static std::array<uint8_t, 16> HmacMd5(const std::string& key,
                                       const std::string& data)
{
  std::array<uint8_t, 16> out{};
  unsigned int len = 16;
  HMAC(EVP_md5(), key.data(), static_cast<int>(key.size()),
       reinterpret_cast<const uint8_t*>(data.data()), data.size(), out.data(),
       &len);
  return out;
}

TEST(CramMd5, ResponseMatchesKnownVector)
{
  const std::string password = "secret";
  const std::string challenge = "<1234567890.1609459200@bareos-dir>";

  const std::string key = Md5Hex(password);
  EXPECT_EQ(key, "5ebe2294ecd0e0f08eab7690d2a6ee69");

  auto hmac = HmacMd5(key, challenge);
  std::string response = BareosBase64Encode(hmac.data(), 16, true);

  EXPECT_EQ(response, "bQVIkYLtKkU2li6JcCLWaA");
}
