/*++

Copyright (c) Microsoft. All rights reserved.

Module Name:

    Plan9Tests.cpp

Abstract:

    This file contains test cases for the plan9 logic.

--*/

#include "precomp.h"
#include "Common.h"

#define LXSST_P9_PREFIX L"\\\\wsl.localhost\\" LXSS_DISTRO_NAME_TEST_L
#define LXSST_P9_TEST_DIR LXSST_P9_PREFIX L"\\data\\p9_test"
#define LXSST_P9_CLEANUP_COMMAND_LINE L"/bin/bash -c \"rm -rf /data/p9_test\""

#define VERIFY_LAST_ERROR(error) VERIFY_ARE_EQUAL(static_cast<DWORD>(error), GetLastError())

namespace Plan9Tests {
class Plan9Tests
{
    WSL_TEST_CLASS(Plan9Tests)

    // Initialize the tests
    TEST_CLASS_SETUP(TestClassSetup)
    {
        VERIFY_ARE_EQUAL(LxsstuInitialize(TRUE), TRUE);

        const auto result = std::filesystem::create_directories(LXSST_P9_TEST_DIR);
        auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
            if (!result)
            {
                auto [out, _] = LxsstuLaunchPowershellAndCaptureOutput(L"(Get-Service P9rdr).Status", 0);
                LogInfo("p9rdr state: %s", out.c_str());
                VERIFY_NO_THROW(LxsstuUninitialize(TRUE));
            }
        });

        VERIFY_IS_TRUE(result);
        return true;
    }

    // Uninitialize the tests.
    TEST_CLASS_CLEANUP(TestClassCleanup)
    {
        LxsstuLaunchWsl(LXSST_P9_CLEANUP_COMMAND_LINE);
        VERIFY_NO_THROW(LxsstuUninitialize(TRUE));

        return true;
    }

    TEST_METHOD_CLEANUP(MethodCleanup)
    {
        LxssLogKernelOutput();
        return true;
    }

    // Tests creating a file, writing to it, and reading from it.
    TEST_METHOD(TestReadWriteFile)
    {
        constexpr std::string_view data{"test data"};
        const auto file = CreateNewTestFile(L"\\readwritetest", data);

        VERIFY_WIN32_BOOL_SUCCEEDED(SetFilePointerEx(file.get(), {}, nullptr, FILE_BEGIN));
        char buffer[1024];
        DWORD bytes;
        VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(file.get(), buffer, sizeof(buffer), &bytes, nullptr));
        VERIFY_ARE_EQUAL(data.size(), bytes);
        VERIFY_ARE_EQUAL(data, std::string_view(buffer, bytes));
    }

    // Tests using a large buffer to read/write a file.
    TEST_METHOD(TestReadWriteFileLarge)
    {
        const auto file = CreateTestFile(L"\\readwritelargetest", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_CREATE);
        char buffer[64 * 1024];
        for (size_t i = 0; i < sizeof(buffer); ++i)
        {
            buffer[i] = i % 26 + 'a';
        }

        for (int i = 0; i < 10; ++i)
        {
            DWORD bytesWritten;
            VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(file.get(), buffer, sizeof(buffer), &bytesWritten, nullptr));
            VERIFY_ARE_EQUAL(sizeof(buffer), bytesWritten);
        }

        VERIFY_WIN32_BOOL_SUCCEEDED(SetFilePointerEx(file.get(), {}, nullptr, FILE_BEGIN));
        char buffer2[64 * 1024];
        DWORD bytesRead;
        for (int i = 0; i < 10; ++i)
        {
            VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(file.get(), buffer2, sizeof(buffer2), &bytesRead, nullptr));
            VERIFY_ARE_EQUAL(sizeof(buffer), bytesRead);
            VERIFY_IS_TRUE(memcmp(buffer, buffer2, sizeof(buffer)) == 0);
        }

        VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(file.get(), buffer2, sizeof(buffer2), &bytesRead, nullptr));
        VERIFY_ARE_EQUAL(0u, bytesRead);
    }

    // Tests querying and setting file information.
    TEST_METHOD(TestQuerySetInfo)
    {
        // Check the attributes on the test directory.
        FILE_BASIC_INFO basicInfo{};
        auto file = CreateTestFile({}, FILE_READ_ATTRIBUTES);
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileInformationByHandleEx(file.get(), FileBasicInfo, &basicInfo, sizeof(basicInfo)));
        VERIFY_IS_TRUE(WI_IsFlagSet(basicInfo.FileAttributes, FILE_ATTRIBUTE_DIRECTORY));
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.ChangeTime.QuadPart);
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.CreationTime.QuadPart);
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.LastAccessTime.QuadPart);
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.LastWriteTime.QuadPart);
        FILE_STANDARD_INFO standardInfo{};
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileInformationByHandleEx(file.get(), FileStandardInfo, &standardInfo, sizeof(basicInfo)));
        VERIFY_IS_TRUE(standardInfo.Directory);
        VERIFY_IS_FALSE(standardInfo.DeletePending);
        const auto id = GetFileId({});
        VERIFY_ARE_NOT_EQUAL(0ull, id);

        // Check attributes on a file.
        file = CreateNewTestFile(L"\\queryinfotest", "0123456789");
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileInformationByHandleEx(file.get(), FileBasicInfo, &basicInfo, sizeof(basicInfo)));
        VERIFY_IS_FALSE(WI_IsFlagSet(basicInfo.FileAttributes, FILE_ATTRIBUTE_DIRECTORY));
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.ChangeTime.QuadPart);
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.CreationTime.QuadPart);
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.LastAccessTime.QuadPart);
        VERIFY_ARE_NOT_EQUAL(0, basicInfo.LastWriteTime.QuadPart);
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileInformationByHandleEx(file.get(), FileStandardInfo, &standardInfo, sizeof(basicInfo)));
        VERIFY_IS_FALSE(standardInfo.Directory);
        VERIFY_IS_FALSE(standardInfo.DeletePending);
        VERIFY_ARE_EQUAL(1u, standardInfo.NumberOfLinks);
        VERIFY_ARE_EQUAL(10, standardInfo.EndOfFile.QuadPart);
        const auto id2 = GetFileId(L"\\queryinfotest");
        VERIFY_ARE_NOT_EQUAL(0ull, id2);
        VERIFY_ARE_NOT_EQUAL(id, id2);

        // Try truncating the file.
        LARGE_INTEGER size;
        size.QuadPart = 5;
        VERIFY_WIN32_BOOL_SUCCEEDED(SetFilePointerEx(file.get(), size, nullptr, FILE_BEGIN));
        VERIFY_WIN32_BOOL_SUCCEEDED(SetEndOfFile(file.get()));
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileInformationByHandleEx(file.get(), FileStandardInfo, &standardInfo, sizeof(basicInfo)));
        VERIFY_ARE_EQUAL(5, standardInfo.EndOfFile.QuadPart);
    }

    // Tests deleting files and directories.
    TEST_METHOD(TestDelete)
    {
        // Delete a file.
        CreateNewTestFile(L"\\deletetestfile", "0123456789");
        VERIFY_IS_TRUE(CheckFileExists(L"\\deletetestfile"));
        VERIFY_WIN32_BOOL_SUCCEEDED(DeleteFileW(LXSST_P9_TEST_DIR L"\\deletetestfile"));
        VERIFY_IS_FALSE(CheckFileExists(L"\\deletetestfile"));

        // Delete a directory.
        VERIFY_WIN32_BOOL_SUCCEEDED(CreateDirectory(LXSST_P9_TEST_DIR L"\\deletetestdir", nullptr));
        VERIFY_IS_TRUE(CheckFileExists(L"\\deletetestdir"));
        VERIFY_WIN32_BOOL_SUCCEEDED(RemoveDirectory(LXSST_P9_TEST_DIR L"\\deletetestdir"));
        VERIFY_IS_FALSE(CheckFileExists(L"\\deletetestdir"));

        // Try to delete non-empty directory.
        VERIFY_WIN32_BOOL_SUCCEEDED(CreateDirectory(LXSST_P9_TEST_DIR L"\\deletetestdir", nullptr));
        CreateNewTestFile(L"\\deletetestdir\\testfile", "0123456789");
        VERIFY_WIN32_BOOL_FAILED(RemoveDirectory(LXSST_P9_TEST_DIR L"\\deletetestdir"));
        VERIFY_LAST_ERROR(ERROR_DIR_NOT_EMPTY);
        VERIFY_IS_TRUE(CheckFileExists(L"\\deletetestdir"));
    }

    // Tests renaming files and directories.
    TEST_METHOD(TestRename)
    {
        // Rename a file.
        CreateNewTestFile(L"\\renametestfile", "0123456789");
        auto id = GetFileId(L"\\renametestfile");
        VERIFY_WIN32_BOOL_SUCCEEDED(MoveFile(LXSST_P9_TEST_DIR L"\\renametestfile", LXSST_P9_TEST_DIR L"\\renametestfile2"));
        auto id2 = GetFileId(L"\\renametestfile2");
        VERIFY_ARE_EQUAL(id, id2);
        VERIFY_IS_FALSE(CheckFileExists(L"\\renametestfile"));
        CreateNewTestFile(L"\\renametestfile", "abcdefg");
        id = GetFileId(L"\\renametestfile");
        VERIFY_ARE_NOT_EQUAL(id, id2);
        VERIFY_WIN32_BOOL_FAILED(MoveFile(LXSST_P9_TEST_DIR L"\\renametestfile", LXSST_P9_TEST_DIR L"\\renametestfile2"));
        VERIFY_LAST_ERROR(ERROR_ALREADY_EXISTS);
        VERIFY_WIN32_BOOL_SUCCEEDED(
            MoveFileEx(LXSST_P9_TEST_DIR L"\\renametestfile", LXSST_P9_TEST_DIR L"\\renametestfile2", MOVEFILE_REPLACE_EXISTING));

        id2 = GetFileId(L"\\renametestfile2");
        VERIFY_ARE_EQUAL(id, id2);

        // Rename a directory
        VERIFY_WIN32_BOOL_SUCCEEDED(CreateDirectory(LXSST_P9_TEST_DIR L"\\renametestdir", nullptr));
        id = GetFileId(L"\\renametestdir");
        VERIFY_WIN32_BOOL_SUCCEEDED(MoveFile(LXSST_P9_TEST_DIR L"\\renametestdir", LXSST_P9_TEST_DIR L"\\renametestdir2"));
        id2 = GetFileId(L"\\renametestdir2");
        VERIFY_ARE_EQUAL(id, id2);
        VERIFY_IS_FALSE(CheckFileExists(L"\\renametestdir"));

        // Directory over a file.
        VERIFY_WIN32_BOOL_FAILED(
            MoveFileEx(LXSST_P9_TEST_DIR L"\\renametestdir2", LXSST_P9_TEST_DIR L"\\renametestfile2", MOVEFILE_REPLACE_EXISTING));

        VERIFY_LAST_ERROR(ERROR_DIRECTORY);

        // File over a directory.
        VERIFY_WIN32_BOOL_FAILED(
            MoveFileEx(LXSST_P9_TEST_DIR L"\\renametestfile2", LXSST_P9_TEST_DIR L"\\renametestdir2", MOVEFILE_REPLACE_EXISTING));

        VERIFY_LAST_ERROR(ERROR_ACCESS_DENIED);
    }

    // Tests listing the files in a directory.
    TEST_METHOD(TestReadDir)
    {
        constexpr int fileCount = 500;
        VERIFY_WIN32_BOOL_SUCCEEDED(CreateDirectory(LXSST_P9_TEST_DIR L"\\readdirtest", nullptr));
        for (int i = 0; i < fileCount; ++i)
        {
            wchar_t path[MAX_PATH]{};
            swprintf_s(path, L"\\readdirtest\\%d", i);
            CreateNewTestFile(path, "0123456789");
        }

        WIN32_FIND_DATA findData{};
        const wil::unique_hfind find{FindFirstFile(LXSST_P9_TEST_DIR L"\\readdirtest\\*", &findData)};
        VERIFY_WIN32_BOOL_SUCCEEDED(static_cast<bool>(find));
        int count{};
        bool foundFiles[fileCount]{};
        do
        {
            ++count;
            VERIFY_ARE_NOT_EQUAL(0u, findData.dwFileAttributes);
            VERIFY_ARE_NOT_EQUAL(0ull, *reinterpret_cast<PULONGLONG>(&findData.ftCreationTime));
            VERIFY_ARE_NOT_EQUAL(0ull, *reinterpret_cast<PULONGLONG>(&findData.ftLastAccessTime));
            VERIFY_ARE_NOT_EQUAL(0ull, *reinterpret_cast<PULONGLONG>(&findData.ftLastWriteTime));
            if (findData.cFileName[0] != L'.')
            {
                VERIFY_ARE_EQUAL(0u, findData.nFileSizeHigh);
                VERIFY_ARE_EQUAL(10u, findData.nFileSizeLow);
                int file = wcstol(findData.cFileName, nullptr, 10);
                VERIFY_IS_GREATER_THAN_OR_EQUAL(file, 0);
                VERIFY_IS_LESS_THAN(file, fileCount);
                VERIFY_IS_FALSE(foundFiles[file]);
                foundFiles[file] = true;
            }

        } while (FindNextFile(find.get(), &findData));

        VERIFY_LAST_ERROR(ERROR_NO_MORE_FILES);
        VERIFY_ARE_EQUAL(fileCount + 2, count);

        for (int i = 0; i < fileCount; ++i)
        {
            VERIFY_IS_TRUE(foundFiles[i]);
        }
    }

    // Tests using mount points inside the WSL instance.
    TEST_METHOD(TestMounts)
    {
        // Check access into mounts like procfs is allowed.
        wil::unique_hfile file{CreateFile(
            LXSST_P9_PREFIX L"\\proc\\stat",
            FILE_GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            nullptr,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
            nullptr)};

        VERIFY_WIN32_BOOL_SUCCEEDED(static_cast<bool>(file));

        char buffer[1024];
        DWORD bytes;
        VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(file.get(), buffer, sizeof(buffer), &bytes, nullptr));
        VERIFY_IS_GREATER_THAN(bytes, 0u);

        // Check access into drvfs mounts is not allowed.
        file.reset(CreateFile(
            LXSST_P9_PREFIX L"\\mnt\\c",
            FILE_GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            nullptr,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
            nullptr));

        VERIFY_IS_FALSE(static_cast<bool>(file));
        VERIFY_LAST_ERROR(ERROR_ACCESS_DENIED);

        file.reset(CreateFile(
            LXSST_P9_PREFIX L"\\mnt\\c\\Windows",
            FILE_GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            nullptr,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
            nullptr));

        VERIFY_IS_FALSE(static_cast<bool>(file));
        VERIFY_LAST_ERROR(ERROR_ACCESS_DENIED);
    }

    TEST_METHOD(TestCreate)
    {
        wil::unique_hfile file;
        IO_STATUS_BLOCK ioStatus;

        // Check error codes for non-existing files.
        auto status = CreateFileNt(&file, LXSST_P9_PREFIX L"\\dat\\p9_test", FILE_GENERIC_READ, ioStatus);
        VERIFY_ARE_EQUAL(STATUS_OBJECT_PATH_NOT_FOUND, status);
        status = CreateFileNt(&file, LXSST_P9_PREFIX L"\\data\\foo", FILE_GENERIC_READ, ioStatus);
        VERIFY_ARE_EQUAL(STATUS_OBJECT_NAME_NOT_FOUND, status);
        status = CreateFileNt(&file, LXSST_P9_PREFIX L"\\etc\\resolve.conf\\foo", FILE_GENERIC_READ, ioStatus);
        VERIFY_ARE_EQUAL(STATUS_OBJECT_PATH_NOT_FOUND, status);

        // Create a file.
        VERIFY_NT_SUCCESS(CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile", FILE_GENERIC_WRITE, ioStatus, FILE_CREATE));
        VERIFY_ARE_EQUAL(static_cast<ULONG_PTR>(FILE_CREATED), ioStatus.Information);

        // Write some test content.
        const std::string contents{"hello"};
        DWORD bytes;
        VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(file.get(), contents.data(), static_cast<DWORD>(contents.size()), &bytes, nullptr));
        VERIFY_ARE_EQUAL(contents.size(), bytes);
        file.reset();

        // Exclusive create should fail now.
        status = CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile", FILE_GENERIC_READ, ioStatus, FILE_CREATE);
        VERIFY_ARE_EQUAL(STATUS_OBJECT_NAME_COLLISION, status);

        // Open-if existing file.
        VERIFY_NT_SUCCESS(CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile", FILE_GENERIC_READ, ioStatus, FILE_OPEN_IF));
        VERIFY_ARE_EQUAL(static_cast<ULONG_PTR>(FILE_OPENED), ioStatus.Information);
        LARGE_INTEGER size;
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileSizeEx(file.get(), &size));
        VERIFY_ARE_EQUAL(5, size.QuadPart);

        // Open-if new file.
        VERIFY_NT_SUCCESS(CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile2", FILE_GENERIC_READ, ioStatus, FILE_OPEN_IF));
        VERIFY_ARE_EQUAL(static_cast<ULONG_PTR>(FILE_CREATED), ioStatus.Information);

        // Overwrite non-existing file.
        status = CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile3", FILE_GENERIC_WRITE, ioStatus, FILE_OVERWRITE);
        VERIFY_ARE_EQUAL(STATUS_OBJECT_NAME_NOT_FOUND, status);

        VERIFY_NT_SUCCESS(CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile3", FILE_GENERIC_WRITE, ioStatus, FILE_OVERWRITE_IF));
        VERIFY_ARE_EQUAL(static_cast<ULONG_PTR>(FILE_CREATED), ioStatus.Information);

        // Overwrite existing file.
        VERIFY_NT_SUCCESS(CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile", FILE_GENERIC_WRITE, ioStatus, FILE_OVERWRITE));
        VERIFY_ARE_EQUAL(static_cast<ULONG_PTR>(FILE_OVERWRITTEN), ioStatus.Information);
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileSizeEx(file.get(), &size));
        VERIFY_ARE_EQUAL(0, size.QuadPart);
        VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(file.get(), contents.data(), static_cast<DWORD>(contents.size()), &bytes, nullptr));
        VERIFY_ARE_EQUAL(contents.size(), bytes);
        file.reset();
        VERIFY_NT_SUCCESS(CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile", FILE_GENERIC_WRITE, ioStatus, FILE_OVERWRITE_IF));
        VERIFY_ARE_EQUAL(static_cast<ULONG_PTR>(FILE_OVERWRITTEN), ioStatus.Information);
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileSizeEx(file.get(), &size));
        VERIFY_ARE_EQUAL(0, size.QuadPart);

        // Open a directory with FILE_NON_DIRECTORY_FILE.
        status = CreateFileNt(&file, LXSST_P9_TEST_DIR, FILE_GENERIC_READ, ioStatus, FILE_OPEN, 0, FILE_NON_DIRECTORY_FILE);
        VERIFY_ARE_EQUAL(STATUS_FILE_IS_A_DIRECTORY, status);

        // Open a file with FILE_DIRECTORY_FILE.
        status = CreateFileNt(&file, LXSST_P9_TEST_DIR L"\\testfile", FILE_GENERIC_READ, ioStatus, FILE_OPEN, 0, FILE_DIRECTORY_FILE);
        VERIFY_ARE_EQUAL(STATUS_NOT_A_DIRECTORY, status);
    }

    static auto EnablePlan9Logging()
    {
        LxssWriteWslDistroConfig("[fileServer]\nlogFile=/plan9-logs.txt\nlogTruncate=false\nlogLevel=5");

        return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] {
            // clean up wsl.conf file
            LxsstuLaunchWsl(L"rm /etc/wsl.conf");
            TerminateDistribution();
        });
    }

    TEST_METHOD(TestPlan9ServerTimeout)
    {
        // This test has proven to be unstable, most likely because another program opens a file inside the distro, which prevents it from terminating.
        SKIP_TEST_UNSTABLE();

        auto revertLogging = EnablePlan9Logging();

        auto dumpLogs = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
            const auto output = LxsstuLaunchWslAndCaptureOutput(L"cat /plan9-logs.txt");
            LogInfo("Plan9 logs: %s", output.first.c_str());
        });

        wsl::windows::common::SvcComm service;
        auto distro = service.GetDefaultDistribution();
        service.TerminateInstance(&distro);

        auto getDistroState = [distro, &service]() -> LxssDistributionState {
            auto distros = service.EnumerateDistributions();
            const auto it =
                std::find_if(distros.begin(), distros.end(), [&](const auto& e) { return IsEqualGUID(distro, e.DistroGuid); });

            VERIFY_ARE_NOT_EQUAL(it, distros.end());

            return it->State;
        };

        VERIFY_ARE_EQUAL(getDistroState(), LxssDistributionStateInstalled);

        // Open a file via \\wsl.localhost and validate that the distro does not terminate
        auto file = CreateTestFile(L"\\9p-test-file", GENERIC_ALL, FILE_CREATE, FILE_FLAG_DELETE_ON_CLOSE);

        // Now the distro should be running
        VERIFY_ARE_EQUAL(getDistroState(), LxssDistributionStateRunning);

        // Validate that the distro does not terminate until the file is closed
        // Note: Distributions time out after 10 seconds.
        std::this_thread::sleep_for(std::chrono::seconds(20));

        // Close the file and make sure that the distro terminates
        file.reset();

        // The distro should now time out and stop
        const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(1);
        while (std::chrono::steady_clock::now() < deadline && getDistroState() != LxssDistributionStateInstalled)
        {
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }

        VERIFY_ARE_EQUAL(getDistroState(), LxssDistributionStateInstalled);
    }

    TEST_METHOD(TestPlan9AdditionalGroupAccess)
    {
        ULONG Uid{};
        ULONG Gid{};

        // Create a user for this test
        CreateUser(L"plan9testuser", &Uid, &Gid);

        // Create a folder that's unaccessible to plan9testuser
        VERIFY_ARE_EQUAL(
            LxsstuLaunchWsl(L"mkdir -p /tmp/plan9-group-test && groupadd -f plan9testgroup && chown root:plan9testgroup "
                            L"/tmp/plan9-group-test && "
                            L"echo -n foo > /tmp/plan9-group-test/bar && chmod 770 /tmp/plan9-group-test"),
            0u);

        auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
            LxsstuLaunchWsl(L"-u root rm -rf /etc/wsl.conf /tmp/plan9-group-test");
            TerminateDistribution();
        });

        // Make plan9testuser the default
        LxssWriteWslDistroConfig("[user]\ndefault=plan9testuser\n");
        TerminateDistribution();

        // Validate that folder isn't accessible
        constexpr auto path = L"\\\\wsl.localhost\\" LXSS_DISTRO_NAME_TEST "\\tmp\\plan9-group-test\\bar";
        std::wifstream file(path);
        VERIFY_IS_FALSE(file.good());

        // Add plan9testuser to plan9testgroup
        VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"-u root usermod -G plan9testgroup -a plan9testuser"), 0u);

        // Validate that the file can be accessed now
        TerminateDistribution();
        // There's a race condition on fe_release that can cause opening this file to fail.
        try
        {
            wsl::shared::retry::RetryWithTimeout<void>(
                [&file]() {
                    file.open(path);
                    LogInfo("Failed to open %ls, %d", path, errno);
                    THROW_HR_IF(E_ABORT, !file.good());
                },
                std::chrono::seconds(1),
                std::chrono::minutes(2));
        }
        catch (...)
        {
            LogError("Timed out trying to open: %ls", path);
            VERIFY_FAIL();
        }

        std::wstring content(3, '\0');
        VERIFY_IS_TRUE(file.read(content.data(), content.size()).good());

        VERIFY_ARE_EQUAL(content, L"foo");
    }

    /* Plan9 Test Helper Methods */

    static wil::unique_hfile CreateTestFile(std::wstring_view path, DWORD desiredAccess, DWORD disposition = OPEN_EXISTING, DWORD flags = 0)
    {
        std::wstring fullPath{LXSST_P9_TEST_DIR};
        fullPath += path;
        wil::unique_hfile file{CreateFile(
            fullPath.c_str(),
            desiredAccess,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            nullptr,
            disposition,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS | flags,
            nullptr)};

        VERIFY_WIN32_BOOL_SUCCEEDED(static_cast<bool>(file));

        return file;
    }

    wil::unique_hfile CreateNewTestFile(const std::wstring& path, std::string_view contents)
    {
        auto file = CreateTestFile(path, FILE_GENERIC_WRITE | FILE_GENERIC_READ, CREATE_NEW);
        DWORD bytes;
        VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(file.get(), contents.data(), static_cast<DWORD>(contents.size()), &bytes, nullptr));
        VERIFY_ARE_EQUAL(contents.size(), bytes);
        return file;
    }

    static NTSTATUS CreateFileNt(
        PHANDLE handle, LPCWSTR name, ACCESS_MASK desiredAccess, IO_STATUS_BLOCK& ioStatus, ULONG disposition = FILE_OPEN, ULONG attributes = 0, ULONG createOptions = 0)
    {
        UNICODE_STRING pathu;
        THROW_IF_NTSTATUS_FAILED(RtlDosPathNameToNtPathName_U_WithStatus(name, &pathu, nullptr, nullptr));
        wil::unique_process_heap_ptr<WCHAR> buffer{pathu.Buffer};
        OBJECT_ATTRIBUTES oa;
        InitializeObjectAttributes(&oa, &pathu, OBJ_CASE_INSENSITIVE, nullptr, nullptr);
        return NtCreateFile(
            handle,
            desiredAccess | SYNCHRONIZE,
            &oa,
            &ioStatus,
            nullptr,
            attributes,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            disposition,
            createOptions | FILE_SYNCHRONOUS_IO_ALERT,
            nullptr,
            0);
    }

    static bool CheckFileExists(std::wstring_view path)
    {
        std::wstring fullPath{LXSST_P9_TEST_DIR};
        fullPath += path;
        if (!PathFileExists(fullPath.c_str()))
        {
            VERIFY_LAST_ERROR(ERROR_FILE_NOT_FOUND);
            return false;
        }

        return true;
    }

    ULONGLONG GetFileId(std::wstring_view path)
    {
        BY_HANDLE_FILE_INFORMATION info;
        const auto file = CreateTestFile(path, FILE_READ_ATTRIBUTES);
        VERIFY_WIN32_BOOL_SUCCEEDED(GetFileInformationByHandle(file.get(), &info));
        return static_cast<ULONGLONG>(info.nFileIndexHigh) << 32 | info.nFileIndexLow;
    }
};
} // namespace Plan9Tests