/**
  Implemnet UnitTestLib log services

  Copyright (c) Microsoft Corporation.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent
**/

#include <PiDxe.h>
#include <UnitTestFrameworkTypes.h>
#include <Library/UnitTestLib.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/DebugLib.h>
#include <Library/PrintLib.h>
#include <Library/PcdLib.h>

#define UNIT_TEST_MAX_SINGLE_LOG_STRING_LENGTH  (512)
#define UNIT_TEST_MAX_LOG_BUFFER                SIZE_16KB

struct _UNIT_TEST_LOG_PREFIX_STRING {
  UNIT_TEST_STATUS    LogLevel;
  CHAR8               *String;
};

struct _UNIT_TEST_LOG_PREFIX_STRING  mLogPrefixStrings[] = {
  { UNIT_TEST_LOG_LEVEL_ERROR,   "[ERROR]       " },
  { UNIT_TEST_LOG_LEVEL_WARN,    "[WARNING]     " },
  { UNIT_TEST_LOG_LEVEL_INFO,    "[INFO]        " },
  { UNIT_TEST_LOG_LEVEL_VERBOSE, "[VERBOSE]     " }
};

//
// Unit-Test Log helper functions
//

STATIC
CONST CHAR8 *
GetStringForStatusLogPrefix (
  IN UINTN  LogLevel
  )
{
  UINTN  Index;
  CHAR8  *Result;

  Result = NULL;
  for (Index = 0; Index < ARRAY_SIZE (mLogPrefixStrings); Index++) {
    if (mLogPrefixStrings[Index].LogLevel == LogLevel) {
      Result = mLogPrefixStrings[Index].String;
      break;
    }
  }

  return Result;
}

STATIC
EFI_STATUS
AddStringToUnitTestLog (
  IN OUT UNIT_TEST    *UnitTest,
  IN     CONST CHAR8  *String
  )
{
  EFI_STATUS  Status;

  //
  // Make sure that you're cooking with gas.
  //
  if ((UnitTest == NULL) || (String == NULL)) {
    return EFI_INVALID_PARAMETER;
  }

  // If this is the first log for the test allocate log space
  if (UnitTest->Log == NULL) {
    UnitTestLogInit (UnitTest, NULL, 0);
  }

  if (UnitTest->Log == NULL) {
    DEBUG ((DEBUG_ERROR, "Failed to allocate space for unit test log\n"));
    ASSERT (UnitTest->Log != NULL);
    return EFI_OUT_OF_RESOURCES;
  }

  Status = AsciiStrnCatS (
             UnitTest->Log,
             UNIT_TEST_MAX_LOG_BUFFER / sizeof (CHAR8),
             String,
             UNIT_TEST_MAX_SINGLE_LOG_STRING_LENGTH
             );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Failed to add unit test log string.  Status = %r\n", Status));
    return Status;
  }

  return EFI_SUCCESS;
}

/**
  This function is responsible for initializing the log buffer for a single test. It can
  be used internally, but may also be consumed by the test framework to add pre-existing
  data to a log before it's used.

  @param[in,out]  TestHandle    A handle to the test being initialized.
  @param[in]      Buffer        [Optional] A pointer to pre-existing log data that should
                                be used to initialize the log. Should include a NULL terminator.
  @param[in]      BufferSize    [Optional] The size of the pre-existing log data.

**/
VOID
EFIAPI
UnitTestLogInit (
  IN OUT UNIT_TEST  *Test,
  IN     UINT8      *Buffer      OPTIONAL,
  IN     UINTN      BufferSize   OPTIONAL
  )
{
  //
  // Make sure that you're cooking with gas.
  //
  if (Test == NULL) {
    DEBUG ((DEBUG_ERROR, "%a called with invalid Test parameter\n", __func__));
    return;
  }

  //
  // If this is the first log for the test allocate log space
  //
  if (Test->Log == NULL) {
    Test->Log = AllocateZeroPool (UNIT_TEST_MAX_LOG_BUFFER);
  }

  //
  // check again to make sure allocate worked
  //
  if (Test->Log == NULL) {
    DEBUG ((DEBUG_ERROR, "Failed to allocate memory for the log\n"));
    return;
  }

  if ((Buffer != NULL) && (BufferSize > 0) && (BufferSize <= UNIT_TEST_MAX_LOG_BUFFER)) {
    CopyMem (Test->Log, Buffer, BufferSize);
  }
}

/**
  Test logging function that records a messages in the test framework log.
  Record is associated with the currently executing test case.

  @param[in]  ErrorLevel  The error level of the unit test log message.
  @param[in]  Format      Formatting string following the format defined in the
                          MdePkg/Include/Library/PrintLib.h.
  @param[in]  ...         Print args.
**/
VOID
EFIAPI
UnitTestLog (
  IN  UINTN        ErrorLevel,
  IN  CONST CHAR8  *Format,
  ...
  )
{
  UNIT_TEST_FRAMEWORK_HANDLE  FrameworkHandle;
  CHAR8                       NewFormatString[UNIT_TEST_MAX_SINGLE_LOG_STRING_LENGTH];
  CHAR8                       LogString[UNIT_TEST_MAX_SINGLE_LOG_STRING_LENGTH];
  CONST CHAR8                 *LogTypePrefix;
  VA_LIST                     Marker;

  FrameworkHandle = GetActiveFrameworkHandle ();
  if (FrameworkHandle == NULL) {
    DEBUG ((DEBUG_ERROR, "%a - FrameworkHandle not initialized\n", __func__));
    return;
  }

  LogTypePrefix = NULL;

  //
  // Make sure that this unit test log level is enabled.
  //
  if ((ErrorLevel & (UINTN)PcdGet32 (PcdUnitTestLogLevel)) == 0) {
    return;
  }

  //
  // If we need to define a new format string...
  // well... get to it.
  //
  LogTypePrefix = GetStringForStatusLogPrefix (ErrorLevel);
  if (LogTypePrefix != NULL) {
    AsciiSPrint (NewFormatString, sizeof (NewFormatString), "%a%a", LogTypePrefix, Format);
  } else {
    AsciiStrCpyS (NewFormatString, sizeof (NewFormatString), Format);
  }

  //
  // Convert the message to an ASCII String
  //
  VA_START (Marker, Format);
  AsciiVSPrint (LogString, sizeof (LogString), NewFormatString, Marker);
  VA_END (Marker);

  //
  // Finally, add the string to the log.
  //
  AddStringToUnitTestLog (((UNIT_TEST_FRAMEWORK *)FrameworkHandle)->CurrentTest, LogString);
}
