// Copyright 2024 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

#include <cstdint>

#include "pw_cpu_exception/entry.h"
#include "pw_cpu_exception/handler.h"
#include "pw_cpu_exception/support.h"
#include "pw_cpu_exception_cortex_m/exception_entry_test_util.h"
#include "pw_cpu_exception_cortex_m_private/cortex_m_constants.h"
#include "pw_unit_test/framework.h"

namespace pw::cpu_exception::cortex_m {
namespace {

using pw::cpu_exception::RawFaultingCpuState;

// CMSIS/Cortex-M/ARMv6 related constants.
// These values are from the ARMv6-M Architecture Reference Manual DDI 0419
// https://developer.arm.com/documentation/ddi0419/latest

// Memory mapped registers. (ARMv6-M Section B3.2.2, Table B3-4)
volatile uint32_t& cortex_m_ccr =
    *reinterpret_cast<volatile uint32_t*>(0xE000ED14u);

// Counter that is incremented if the test's exception handler correctly handles
// a triggered exception.
size_t exceptions_handled = 0;

// Allow up to kMaxFaultDepth faults before determining the device is
// unrecoverable.
constexpr size_t kMaxFaultDepth = 1;

// The instruction size in bytes on the ARMv6-M architecture.
constexpr size_t kMaxInstructionSize = 4;

// The manually captured PC won't be the exact same as the faulting PC. This is
// the maximum tolerated distance between the two to allow the test to pass.
constexpr int32_t kMaxPcDistance = 6;

// Variable to prevent more than kMaxFaultDepth nested crashes.
size_t current_fault_depth = 0;

// Faulting pw_cpu_exception_State is copied here so values can be validated
// after exiting exception handler.
pw_cpu_exception_State captured_states[kMaxFaultDepth] = {};
pw_cpu_exception_State& captured_state = captured_states[0];

// Flag used to check if the contents of span matches the captured state.
bool span_matches = false;
// Forward declaration of the exception handler.
void TestingExceptionHandler(pw_cpu_exception_State*);

// Populate the device's registers with testable values, then trigger exception.
void BeginBaseFaultTest() {
  // Make sure divide by zero causes a fault.
  uint32_t magic = kMagicPattern;
  asm volatile(
      " mov r0, %[magic]                                      \n"
      " movs r1, #0                                           \n"
      " mov r2, pc                                            \n"
      " mov r3, lr                                            \n"
      // This instruction reads a bad address.
      " ldr r1, [r0]                                          \n"
      " nop                                                   \n"
      " nop                                                   \n"
      // clang-format off
      : /*output=*/
      : /*input=*/[magic]"r"(magic)
      : /*clobbers=*/"r0", "r1", "r2", "r3"
      // clang-format on
  );

  // Check that the stack align bit was not set.
  EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit, 0u);
}

// Populate the device's registers with testable values, then trigger exception.
// This version causes stack to not be 4-byte aligned initially, testing
// the fault handlers correction for psp.
void BeginBaseFaultUnalignedStackTest() {
  uint32_t magic = kMagicPattern;
  asm volatile(
      // Push one register to cause $sp to be no longer 8-byte aligned,
      // assuming it started 8-byte aligned as expected.
      " push {r0}                                             \n"
      " mov r0, %[magic]                                      \n"
      " movs r1, #0                                           \n"
      " mov r2, pc                                            \n"
      " mov r3, lr                                            \n"
      // This instruction reads a bad address. Our fault handler should
      // ultimately advance the pc to the pop instruction.
      " ldr r1, [r0]                                          \n"
      " nop                                                   \n"
      " nop                                                   \n"
      " pop {r0}                                              \n"
      // clang-format off
      : /*output=*/
      : /*input=*/[magic]"r"(magic)
      : /*clobbers=*/"r0", "r1", "r2", "r3"
      // clang-format on
  );

  // Check that the stack align bit was set.
  EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit,
            kPsrExtraStackAlignBit);
}

// Populate some of the extended set of captured registers, then trigger
// exception.
void BeginExtendedFaultTest() {
  uint32_t magic = kMagicPattern;
  volatile uint32_t local_msp = 0;
  volatile uint32_t local_psp = 0;
  asm volatile(
      " mov r4, %[magic]                                      \n"
      " movs r5, #0                                           \n"
      " mov r11, %[magic]                                     \n"
      " mrs %[local_msp], msp                                 \n"
      " mrs %[local_psp], psp                                 \n"
      // This instruction reads a bad address.
      " ldr r1, [r4]                                          \n"
      " nop                                                   \n"
      " nop                                                   \n"
      // clang-format off
      : /*output=*/[local_msp]"=r"(local_msp), [local_psp]"=r"(local_psp)
      : /*input=*/[magic]"r"(magic)
      : /*clobbers=*/"r4", "r5", "r11", "memory"
      // clang-format on
  );

  // Check that the stack align bit was not set.
  EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit, 0u);

  // Check that the captured stack pointers matched the ones in the context of
  // the fault.
  EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.msp), local_msp);
  EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.psp), local_psp);
}

// Populate some of the extended set of captured registers, then trigger
// exception.
// This version causes stack to not be 4-byte aligned initially, testing
// the fault handlers correction for psp.
void BeginExtendedFaultUnalignedStackTest() {
  uint32_t magic = kMagicPattern;
  volatile uint32_t local_msp = 0;
  volatile uint32_t local_psp = 0;
  asm volatile(
      // Push one register to cause $sp to be no longer 8-byte aligned,
      // assuming it started 8-byte aligned as expected.
      " push {r0}                                             \n"
      " mov r4, %[magic]                                      \n"
      " movs r5, #0                                           \n"
      " mov r11, %[magic]                                     \n"
      " mrs %[local_msp], msp                                 \n"
      " mrs %[local_psp], psp                                 \n"
      // This instruction reads a bad address. Our fault handler should
      // ultimately advance the pc to the pop instruction.
      " ldr r1, [r4]                                          \n"
      " nop                                                   \n"
      " nop                                                   \n"
      " pop {r0}                                              \n"
      // clang-format off
      : /*output=*/[local_msp]"=r"(local_msp), [local_psp]"=r"(local_psp)
      : /*input=*/[magic]"r"(magic)
      : /*clobbers=*/"r0", "r4", "r5", "r11", "memory"
      // clang-format on
  );

  // Check that the stack align bit was set.
  EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit,
            kPsrExtraStackAlignBit);

  // Check that the captured stack pointers matched the ones in the context of
  // the fault.
  EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.msp), local_msp);
  EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.psp), local_psp);
}

void Setup() {
  pw_cpu_exception_SetHandler(TestingExceptionHandler);
  InstallVectorTableEntries(
      reinterpret_cast<uint32_t*>(pw_cpu_exception_Entry));
  exceptions_handled = 0;
  current_fault_depth = 0;
  captured_state = {};
}

TEST(FaultEntry, BasicFault) {
  Setup();
  BeginBaseFaultTest();
  ASSERT_EQ(exceptions_handled, 1u);
  // captured_state values must be cast since they're in a packed struct.
  EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r0), kMagicPattern);
  EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r1), 0u);
  // PC is manually saved in r2 before the exception occurs (where PC is also
  // stored). Ensure these numbers are within a reasonable distance.
  int32_t captured_pc_distance =
      captured_state.base.pc - captured_state.base.r2;
  EXPECT_LT(captured_pc_distance, kMaxPcDistance);
  EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r3),
            static_cast<uint32_t>(captured_state.base.lr));
}

TEST(FaultEntry, BasicUnalignedStackFault) {
  Setup();
  BeginBaseFaultUnalignedStackTest();
  ASSERT_EQ(exceptions_handled, 1u);
  // captured_state values must be cast since they're in a packed struct.
  EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r0), kMagicPattern);
  EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r1), 0u);
  // PC is manually saved in r2 before the exception occurs (where PC is also
  // stored). Ensure these numbers are within a reasonable distance.
  int32_t captured_pc_distance =
      captured_state.base.pc - captured_state.base.r2;
  EXPECT_LT(captured_pc_distance, kMaxPcDistance);
  EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r3),
            static_cast<uint32_t>(captured_state.base.lr));
}

TEST(FaultEntry, ExtendedFault) {
  Setup();
  BeginExtendedFaultTest();
  ASSERT_EQ(exceptions_handled, 1u);
  ASSERT_TRUE(span_matches);
  const ExtraRegisters& extended_registers = captured_state.extended;
  // captured_state values must be cast since they're in a packed struct.
  EXPECT_EQ(static_cast<uint32_t>(extended_registers.r4), kMagicPattern);
  EXPECT_EQ(static_cast<uint32_t>(extended_registers.r5), 0u);
  EXPECT_EQ(static_cast<uint32_t>(extended_registers.r11), kMagicPattern);

  // Check expected values for this crash.
  EXPECT_EQ((extended_registers.icsr & 0x1FFu), kHardFaultIsrNum);
}

TEST(FaultEntry, ExtendedUnalignedStackFault) {
  Setup();
  BeginExtendedFaultUnalignedStackTest();
  ASSERT_EQ(exceptions_handled, 1u);
  ASSERT_TRUE(span_matches);
  const ExtraRegisters& extended_registers = captured_state.extended;
  // captured_state values must be cast since they're in a packed struct.
  EXPECT_EQ(static_cast<uint32_t>(extended_registers.r4), kMagicPattern);
  EXPECT_EQ(static_cast<uint32_t>(extended_registers.r5), 0u);
  EXPECT_EQ(static_cast<uint32_t>(extended_registers.r11), kMagicPattern);

  // Check expected values for this crash.
  EXPECT_EQ((extended_registers.icsr & 0x1FFu), kHardFaultIsrNum);
}

void TestingExceptionHandler(pw_cpu_exception_State* state) {
  if (++current_fault_depth > kMaxFaultDepth) {
    volatile bool loop = true;
    while (loop) {
      // Hit unexpected nested crash, prevent further nesting.
    }
  }

  // After a fault has been handled, the exception handler will
  // return to the faulting instruction.  For testing, move the
  // PC to the next instruction to allow the tests to continue
  // without entering an exception loop.
  // v6m supports variable instruction size, so always skip the
  // max instruction size, knowing there is enough nops after the
  // faulting test instruction to ensure it's safe to skip kMaxInstructionSize
  // even if it's a 2 byte instruction.
  state->base.pc += kMaxInstructionSize;

  // Disable traps. Must be disabled before EXPECT, as memcpy() can do unaligned
  // operations.
  cortex_m_ccr &= ~kUnalignedTrapEnableMask;

  // Copy captured state to check later.
  std::memcpy(&captured_states[exceptions_handled],
              state,
              sizeof(pw_cpu_exception_State));

  // Ensure span compares to be the same.
  span<const uint8_t> state_span = RawFaultingCpuState(*state);
  EXPECT_EQ(state_span.size(), sizeof(pw_cpu_exception_State));
  if (std::memcmp(state, state_span.data(), state_span.size()) == 0) {
    span_matches = true;
  } else {
    span_matches = false;
  }

  exceptions_handled++;

  EXPECT_EQ(state->extended.shcsr, cortex_m_shcsr());
}

}  // namespace
}  // namespace pw::cpu_exception::cortex_m
