/*
 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#include "precompiled.hpp"

#ifdef _WINDOWS

#include "gc/z/zAddress.inline.hpp"
#include "gc/z/zGlobals.hpp"
#include "gc/z/zList.inline.hpp"
#include "gc/z/zMemory.inline.hpp"
#include "gc/z/zSyscall_windows.hpp"
#include "gc/z/zVirtualMemory.hpp"
#include "runtime/os.hpp"
#include "unittest.hpp"

using namespace testing;

#define EXPECT_ALLOC_OK(offset) EXPECT_NE(offset, zoffset(UINTPTR_MAX))

class ZMapperTest : public Test {
private:
  static constexpr size_t ZMapperTestReservationSize = 32 * M;

  static bool            _initialized;
  static ZMemoryManager* _va;

  ZVirtualMemoryManager* _vmm;

public:
  bool reserve_for_test() {
    // Initialize platform specific parts before reserving address space
    _vmm->pd_initialize_before_reserve();

    // Reserve address space
    if (!_vmm->pd_reserve(ZOffset::address_unsafe(zoffset(0)), ZMapperTestReservationSize)) {
      return false;
    }

    // Make the address range free before setting up callbacks below
    _va->free(zoffset(0), ZMapperTestReservationSize);

    // Initialize platform specific parts after reserving address space
    _vmm->pd_initialize_after_reserve();

    return true;
  }

  virtual void SetUp() {
    // Only run test on supported Windows versions
    if (!ZSyscall::is_supported()) {
      GTEST_SKIP() << "Requires Windows version 1803 or later";
      return;
    }

    ZSyscall::initialize();
    ZGlobalsPointers::initialize();

    // Fake a ZVirtualMemoryManager
    _vmm = (ZVirtualMemoryManager*)os::malloc(sizeof(ZVirtualMemoryManager), mtTest);

    // Construct its internal ZMemoryManager
    _va = new (&_vmm->_manager) ZMemoryManager();

    // Reserve address space for the test
    if (!reserve_for_test()) {
      GTEST_SKIP() << "Failed to reserve address space";
      return;
    }

    _initialized = true;
  }

  virtual void TearDown() {
    if (!ZSyscall::is_supported()) {
      // Test skipped, nothing to cleanup
      return;
    }

    if (_initialized) {
      _vmm->pd_unreserve(ZOffset::address_unsafe(zoffset(0)), 0);
    }
    os::free(_vmm);
  }

  static void test_alloc_low_address() {
    // Verify that we get placeholder for first granule
    zoffset bottom = _va->alloc_low_address(ZGranuleSize);
    EXPECT_ALLOC_OK(bottom);

    _va->free(bottom, ZGranuleSize);

    // Alloc something larger than a granule and free it
    bottom = _va->alloc_low_address(ZGranuleSize * 3);
    EXPECT_ALLOC_OK(bottom);

    _va->free(bottom, ZGranuleSize * 3);

    // Free with more memory allocated
    bottom = _va->alloc_low_address(ZGranuleSize);
    EXPECT_ALLOC_OK(bottom);

    zoffset next = _va->alloc_low_address(ZGranuleSize);
    EXPECT_ALLOC_OK(next);

    _va->free(bottom, ZGranuleSize);
    _va->free(next, ZGranuleSize);
  }

  static void test_alloc_high_address() {
    // Verify that we get placeholder for last granule
    zoffset high = _va->alloc_high_address(ZGranuleSize);
    EXPECT_ALLOC_OK(high);

    zoffset prev = _va->alloc_high_address(ZGranuleSize);
    EXPECT_ALLOC_OK(prev);

    _va->free(high, ZGranuleSize);
    _va->free(prev, ZGranuleSize);

    // Alloc something larger than a granule and return it
    high = _va->alloc_high_address(ZGranuleSize * 2);
    EXPECT_ALLOC_OK(high);

    _va->free(high, ZGranuleSize * 2);
  }

  static void test_alloc_whole_area() {
    // Alloc the whole reservation
    zoffset bottom = _va->alloc_low_address(ZMapperTestReservationSize);
    EXPECT_ALLOC_OK(bottom);

    // Free two chunks and then allocate them again
    _va->free(bottom, ZGranuleSize * 4);
    _va->free(bottom + ZGranuleSize * 6, ZGranuleSize * 6);

    zoffset offset = _va->alloc_low_address(ZGranuleSize * 4);
    EXPECT_ALLOC_OK(offset);

    offset = _va->alloc_low_address(ZGranuleSize * 6);
    EXPECT_ALLOC_OK(offset);

    // Now free it all, and verify it can be re-allocated
    _va->free(bottom, ZMapperTestReservationSize);

    bottom = _va->alloc_low_address(ZMapperTestReservationSize);
    EXPECT_ALLOC_OK(bottom);

    _va->free(bottom, ZMapperTestReservationSize);
  }
};

bool ZMapperTest::_initialized   = false;
ZMemoryManager* ZMapperTest::_va = nullptr;

TEST_VM_F(ZMapperTest, test_alloc_low_address) {
  test_alloc_low_address();
}

TEST_VM_F(ZMapperTest, test_alloc_high_address) {
  test_alloc_high_address();
}

TEST_VM_F(ZMapperTest, test_alloc_whole_area) {
  test_alloc_whole_area();
}

#endif // _WINDOWS