#include <unistd.h>
#include <cmath>
#include <chrono>
#include <filesystem>
#include <format>
#include <thread>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/string/VarList2.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>

#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"

using namespace Hyprutils::Utils;

/// Spawns a kitty and creates a file and returns its name. The removal of the file triggers
/// activation of the spawned kitty window.
///
/// On failure, returns an empty string, possibly leaving a temporary file.
static std::string spawnKittyActivating(const std::string& class_ = "kitty_activating") {
    // `XXXXXX` is what `mkstemp` expects to find in the string
    std::string tmpFilename = (std::filesystem::temp_directory_path() / "XXXXXX").string();
    int         fd          = mkstemp(tmpFilename.data());

    if (fd < 0) {
        NLog::red("Error: could not create tmp file: errno {}", errno);
        return "";
    }

    (void)close(fd);

    const std::vector<std::string> args = {
        "-o", "allow_remote_control=yes", "--", "/bin/sh", "-c", std::format("while [ -f \"{}\" ]; do :; done; kitten @ focus-window; sleep infinity", tmpFilename)};

    if (!Tests::spawnKitty(class_, args)) {
        NLog::red("Error: failed to spawn kitty");
        return "";
    }

    return tmpFilename;
}

static std::string getWindowAddress(const std::string& winInfo) {
    auto pos  = winInfo.find("Window ");
    auto pos2 = winInfo.find(" -> ");
    if (pos == std::string::npos || pos2 == std::string::npos) {
        NLog::log("{}Wrong window info", Colors::RED);
        return "Wrong window info";
    }
    return winInfo.substr(pos + 7, pos2 - pos - 7);
}

TEST_CASE(swapWindow) {
    // test on workspace "swapwindow"
    NLog::log("{}Switching to workspace \"swapwindow\"", Colors::YELLOW);
    getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:swapwindow' })");

    if (!Tests::spawnKitty("kitty_A")) {
        FAIL_TEST("Could not spawn kitty");
    }

    if (!Tests::spawnKitty("kitty_B")) {
        FAIL_TEST("Could not spawn kitty");
    }

    NLog::log("{}Expecting 2 windows", Colors::YELLOW);
    ASSERT(Tests::windowCount(), 2);

    // Test swapwindow by direction
    {
        getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })");
        auto pos = std::format("at: {}", Tests::getAttribute(getFromSocket("/activewindow"), "at"));
        NLog::log("{}Testing kitty_A {}, swapwindow with direction 'r'", Colors::YELLOW, pos);

        OK(getFromSocket("/dispatch hl.dsp.window.swap({ direction = 'right' })"));
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"));

        EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
    }

    // Test swapwindow by class
    {
        getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })");
        auto pos = std::format("at: {}", Tests::getAttribute(getFromSocket("/activewindow"), "at"));
        NLog::log("{}Testing kitty_A {}, swapwindow with class:kitty_B", Colors::YELLOW, pos);

        OK(getFromSocket("/dispatch hl.dsp.window.swap({ target = 'class:kitty_B' })"));
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"));

        EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
    }

    // Test swapwindow by address
    {
        getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })");
        auto addr = getWindowAddress(getFromSocket("/activewindow"));
        getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })");
        auto pos = std::format("at: {}", Tests::getAttribute(getFromSocket("/activewindow"), "at"));
        NLog::log("{}Testing kitty_A {}, swapwindow with address:0x{}(kitty_B)", Colors::YELLOW, pos, addr);

        OK(getFromSocket(std::format("/dispatch hl.dsp.window.swap({{ target = 'address:0x{}' }})", addr)));
        OK(getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'address:0x{}' }})", addr)));

        EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
    }

    NLog::log("{}Testing swapwindow with fullscreen. Expecting to fail", Colors::YELLOW);
    {
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));

        auto str = getFromSocket("/dispatch hl.dsp.window.swap({ direction = 'left' })");
        EXPECT_CONTAINS(str, "Can't swap fullscreen window");

        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));
    }

    NLog::log("{}Testing swapwindow with different workspace", Colors::YELLOW);
    {
        getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })");
        auto addr = getWindowAddress(getFromSocket("/activewindow"));
        auto ws   = std::format("workspace: {}", Tests::getAttribute(getFromSocket("/activewindow"), "workspace"));
        NLog::log("{}Sending address:0x{}(kitty_B) to workspace \"swapwindow2\"", Colors::YELLOW, addr);

        OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = 'name:swapwindow2', follow = false })"));
        OK(getFromSocket(std::format("/dispatch hl.dsp.window.swap({{ target = 'address:0x{}' }})", addr)));
        getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })");
        EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", ws));
    }
}

TEST_CASE(windowGroupRules) {
    OK(getFromSocket("/eval hl.config({ general = { border_size = 8 } })"));
    OK(getFromSocket("/eval hl.workspace_rule({ workspace = 'w[tv1]', border_size = 0 })"));
    OK(getFromSocket("/eval hl.workspace_rule({ workspace = 'f[1]', border_size = 0 })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { workspace = 'w[tv1]' }, border_size = 0 })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { workspace = 'f[1]' }, border_size = 0 })"));

    if (!Tests::spawnKitty("kitty_A")) {
        FAIL_TEST("Could not spawn kitty");
    }

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "0");
    }

    if (!Tests::spawnKitty("kitty_B")) {
        FAIL_TEST("Could not spawn kitty");
    }

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "8");
    }

    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
    OK(getFromSocket("/dispatch hl.dsp.group.toggle()"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })"));

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "0");
    }

    OK(getFromSocket("/dispatch hl.dsp.group.next()"));

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "0");
    }

    if (!Tests::spawnKitty("kitty_C")) {
        FAIL_TEST("Could not spawn kitty");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.move({ out_of_group = 'right' })"));

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "8");
    }
}

static bool isActiveWindow(const std::string& class_, char fullscreen = '0', bool log = true) {
    std::string activeWin     = getFromSocket("/activewindow");
    auto        winClass      = Tests::getAttribute(activeWin, "class");
    auto        winFullscreen = Tests::getAttribute(activeWin, "fullscreen").back();
    if (winClass == class_ && winFullscreen == fullscreen)
        return true;
    else {
        if (log)
            NLog::log("{}Wrong active window: expected class {} fullscreen '{}', found class {}, fullscreen '{}'", Colors::RED, class_, fullscreen, winClass, winFullscreen);
        return false;
    }
}

static bool waitForActiveWindow(const std::string& class_, char fullscreen = '0', bool logLastCheck = true, int maxTries = 50) {
    int cnt = 0;
    while (!isActiveWindow(class_, fullscreen, false)) {
        ++cnt;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        if (cnt > maxTries) {
            return isActiveWindow(class_, fullscreen, logLastCheck);
        }
    }
    return true;
}

/// Tests behavior of a window being focused when on that window's workspace
/// another fullscreen window exists.
TEST_CASE(windowFocusOnFullscreenConflict) {
    SPAWN_KITTY("kitty_A");
    SPAWN_KITTY("kitty_B");

    OK(getFromSocket("/eval hl.config({ misc = { focus_on_activate = true } })"));

    // Unfullscreen on conflict
    {
        OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 2 } })"));

        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);

        // Dispatch-focus the same window
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);

        // Dispatch-focus a different window
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"));
        EXPECT(isActiveWindow("kitty_B", '0'), true);

        // Make a window that will request focus
        const std::string removeToActivate = spawnKittyActivating();
        if (removeToActivate.empty())
            FAIL_TEST("Could not spawn kitty_activating");
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);
        std::filesystem::remove(removeToActivate);
        EXPECT(waitForActiveWindow("kitty_activating", '0'), true);
        OK(getFromSocket("/dispatch hl.dsp.window.kill()"));
        Tests::waitUntilWindowsN(2);
    }

    // Take over on conflict
    {
        OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 1 } })"));

        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);

        // Dispatch-focus the same window
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);

        // Dispatch-focus a different window
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"));
        EXPECT(isActiveWindow("kitty_B", '2'), true);
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 0, client = 0, action = 'set' })"));

        // Make a window that will request focus
        const std::string removeToActivate = spawnKittyActivating();
        if (removeToActivate.empty())
            FAIL_TEST("Could not spawn kitty_activating");
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);
        std::filesystem::remove(removeToActivate);
        EXPECT(waitForActiveWindow("kitty_activating", '2'), true);
        OK(getFromSocket("/dispatch hl.dsp.window.kill()"));
        Tests::waitUntilWindowsN(2);
    }

    // Keep the old focus on conflict
    {
        OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 0 } })"));

        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);

        // Dispatch-focus the same window
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);

        // Make a window that will request focus - the setting is treated normally
        const std::string removeToActivate = spawnKittyActivating();
        if (removeToActivate.empty())
            FAIL_TEST("Could not spawn kitty_activating");
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
        EXPECT(isActiveWindow("kitty_A", '2'), true);
        std::filesystem::remove(removeToActivate);
        EXPECT(waitForActiveWindow("kitty_A", '2'), true);
    }
}

TEST_CASE(focusFloatingOrTilingUnderFullscreen) {
    OK(getFromSocket("/eval hl.config({ misc = { focus_on_activate = true } })"));
    OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 2 } })"));

    ASSERT(!!Tests::spawnKitty("kitty_a"), true);
    ASSERT(!!Tests::spawnKitty("kitty_b"), true);

    ASSERT(isActiveWindow("kitty_b", '0'), true);
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })"));

    // Test 1: focus floating from fullscreen tiled

    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_a' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
    ASSERT(isActiveWindow("kitty_a", '2'), true);

    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'floating' })"));
    std::string active = getFromSocket("/activewindow");
    EXPECT_CONTAINS(Tests::getAttribute(active, "class"), "kitty_b");
    std::string kitty_a = getFromSocket("/clients");
    // Assuming clients appear in the same order that they connected in
    kitty_a = kitty_a.substr(0, kitty_a.size() - active.size());
    ASSERT_COUNT_STRING(kitty_a, "class:", 1); // Check that the cut is right
    EXPECT(Tests::getAttribute(kitty_a, "class") == "kitty_a", true);
    // Remains fullscreen, since we just focused a floating window over it
    EXPECT(Tests::getAttribute(kitty_a, "fullscreen") == "2", true);
    EXPECT(Tests::getAttribute(kitty_a, "fullscreenClient") == "2", true);

    // Test 2: focus tiled from fullscreen floating

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'tiled' })"));
    active = getFromSocket("/activewindow");
    EXPECT_CONTAINS(Tests::getAttribute(active, "class"), "kitty_a");
    std::string kitty_b = getFromSocket("/clients");
    // Assuming clients appear in the same order that they connected in
    kitty_b = kitty_b.substr(active.size());
    ASSERT_COUNT_STRING(kitty_b, "class:", 1); // Check that the cut is right
    EXPECT(Tests::getAttribute(kitty_b, "class") == "kitty_b", true);
    // Exited fullscreen, since we switched focus to a window under it
    EXPECT(Tests::getAttribute(kitty_b, "fullscreen") == "0", true);
    EXPECT(Tests::getAttribute(kitty_b, "fullscreen") == "0", true);
}

TEST_CASE(windowMaximizeSize) {
    SPAWN_KITTY("kitty_A");

    // check kitty properties. Maximizing shouldnt change its size
    {
        auto str = getFromSocket("/clients");
        EXPECT(str.contains("at: 22,22"), true);
        EXPECT(str.contains("size: 1876,1036"), true);
        EXPECT(str.contains("fullscreen: 0"), true);
    }

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized' })"));

    {
        auto str = getFromSocket("/clients");
        EXPECT(str.contains("at: 22,22"), true);
        EXPECT(str.contains("size: 1876,1036"), true);
        EXPECT(str.contains("fullscreen: 1"), true);
    }
}

TEST_CASE(floatingFocusOnFullscreen) {
    SPAWN_KITTY("kitty_A");
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle' })"));

    SPAWN_KITTY("kitty_B");
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized' })"));

    OK(getFromSocket("/dispatch hl.dsp.window.cycle_next()"));

    OK(getFromSocket("/eval hl.plugin.test.floating_focus_on_fullscreen()"));
}

TEST_CASE(groupFallbackFocus) {
    SPAWN_KITTY("kitty_A");

    OK(getFromSocket("/dispatch hl.dsp.group.toggle()"));

    SPAWN_KITTY("kitty_B");
    SPAWN_KITTY("kitty_C");
    SPAWN_KITTY("kitty_D");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("class: kitty_D"), true);
    }

    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_D' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.kill()"));

    Tests::waitUntilWindowsN(3);

    // Focus must return to the last focus, in this case B.
    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("class: kitty_B"), true);
    }
}

TEST_CASE(bringActiveToTopMouseMovement) {
    OK(getFromSocket("/eval hl.config({ input = { follow_mouse = 2 } })"));
    OK(getFromSocket("/eval hl.config({ input = { float_switch_override_focus = 0 } })"));

    SPAWN_KITTY("a");
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 500, y = 300 })"));
    OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 400, y = 400 })"));

    SPAWN_KITTY("b");
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 500, y = 300 })"));
    OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 400, y = 400 })"));

    auto getTopWindow = []() -> std::string {
        auto clients = getFromSocket("/clients");
        return (clients.rfind("class: a") > clients.rfind("class: b")) ? "a" : "b";
    };

    ASSERT(getTopWindow(), std::string("b"));
    OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 700, y = 500 })"));

    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })"));
    ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: a");

    OK(getFromSocket("/dispatch hl.dsp.window.bring_to_top()"));
    ASSERT(getTopWindow(), std::string("a"));

    OK(getFromSocket("/eval hl.plugin.test.click(272, 1)"));
    OK(getFromSocket("/eval hl.plugin.test.click(272, 0)"));

    ASSERT(getTopWindow(), std::string("a"));
}

TEST_CASE(initialFloatSize) {
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty' }, float = true })"));
    OK(getFromSocket("/eval hl.config({ input = { float_switch_override_focus = 0 } })"));

    SPAWN_KITTY("kitty");

    {
        // Kitty by default opens as 640x400, if this changes this test will break
        auto str = getFromSocket("/clients");
        EXPECT(str.contains("size: 640,400"), true);
    }

    OK(getFromSocket("/reload"));

    Tests::killAllWindows();

    OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true })"));

    Tests::waitUntilWindowsN(1);

    {
        // Kitty by default opens as 640x400, if this changes this test will break
        auto str = getFromSocket("/clients");
        EXPECT(str.contains("size: 640,400"), true);
        EXPECT(str.contains("floating: 1"), true);
    }
}

/// Tests that the `focus_on_activate` effect of window rules always overrides
/// the `misc:focus_on_activate` variable.
TEST_CASE(windowRuleFocusOnActivate) {
    SPAWN_KITTY("kitty_default");

    // Do not focus anyone automatically
    // TODO: this looks like a bug: the following line should not be commented out
    ///////////OK(getFromSocket("/eval hl.window_rule({ match = { class = '.*' }, no_initial_focus = true })"));

    // `focus_on_activate off` takes over
    {
        OK(getFromSocket("/eval hl.config({ misc = { focus_on_activate = true } })"));
        OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_antifocus' }, focus_on_activate = false })"));

        const std::string removeToActivate = spawnKittyActivating("kitty_antifocus");
        if (removeToActivate.empty()) {
            FAIL_TEST("Could not spawn kitty_antifocus");
        }
        ASSERT(waitForActiveWindow("kitty_antifocus"), true);
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_default' })"));
        ASSERT(isActiveWindow("kitty_default"), true);

        std::filesystem::remove(removeToActivate);
        // The focus should NOT transition, since the window rule explicitly forbids that
        ASSERT(waitForActiveWindow("kitty_antifocus", '0', false), false);
    }

    // `focus_on_activate on` takes over
    {
        OK(getFromSocket("/eval hl.config({ misc = { focus_on_activate = false } })"));
        OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_superfocus' }, focus_on_activate = true })"));

        const std::string removeToActivate = spawnKittyActivating("kitty_superfocus");
        if (removeToActivate.empty()) {
            FAIL_TEST("Could not spawn kitty_superfocus");
        }
        ASSERT(waitForActiveWindow("kitty_superfocus"), true);
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_default' })"));
        ASSERT(isActiveWindow("kitty_default"), true);

        std::filesystem::remove(removeToActivate);
        // Now that we requested activation, the focus SHOULD transition to kitty_superfocus, according to the window rule
        ASSERT(waitForActiveWindow("kitty_superfocus"), true);
    }
}

// tests if a pinned window contains the valid workspace after change
TEST_CASE(pinnedWorkspacesValid) {
    getFromSocket("/dispatch hl.dsp.focus({ workspace = '1337' })");

    SPAWN_KITTY("kitty");

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:kitty' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.pin({ action = 'toggle', window = 'class:kitty' })"));

    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("workspace: 1337"), true);
        EXPECT(str.contains("pinned: 1"), true);
    }

    getFromSocket("/dispatch hl.dsp.focus({ workspace = '1338' })");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("workspace: 1338"), true);
        EXPECT(str.contains("pinned: 1"), true);
    }

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'unset', window = 'class:kitty' })"));

    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("workspace: 1338"), true);
        EXPECT(str.contains("pinned: 0"), true);
    }
}

TEST_CASE(windowruleWorkspaceEmpty) {
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_A' }, workspace = 'empty' })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_B' }, workspace = 'emptyn' })"));

    getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })");

    SPAWN_KITTY("kitty");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("workspace: 3"), true);
    }

    SPAWN_KITTY("kitty_A");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("workspace: 1"), true);
    }

    getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })");
    SPAWN_KITTY("kitty_B");
    {
        auto str = getFromSocket("/activewindow");
        EXPECT(str.contains("workspace: 4"), true);
    }
}

TEST_CASE(contentWindowRules) {
    // kill me PLEASE

    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_content_string' }, content = 'game' })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_content_numbers' }, content = '3' })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { content = 'game' }, border_size = 10 })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { content = '3' }, opacity = '0.5' })"));

#define TEST_PROPS()                                                                                                                                                               \
    EXPECT_CONTAINS(getFromSocket("/getprop active border_size"), "10");                                                                                                           \
    EXPECT_CONTAINS(getFromSocket("/getprop active opacity"), "0.5");

    SPAWN_KITTY("kitty_content_string");
    waitForActiveWindow("kitty_content_string");
    TEST_PROPS();

    Tests::killAllWindows();
    ASSERT(Tests::windowCount(), 0);

    SPAWN_KITTY("kitty_content_numbers");
    waitForActiveWindow("kitty_content_numbers");
    TEST_PROPS();

    Tests::killAllWindows();
    ASSERT(Tests::windowCount(), 0);

#undef TEST_PROPS
}

TEST_CASE(issue14038) {
    SPAWN_KITTY("kitty_14038");

    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = 'special:a', follow = false, window = 'class:kitty_14038' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle', window = 'class:kitty_14038' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.pin({ action = 'toggle', window = 'class:kitty_14038' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle', window = 'class:kitty_14038' })"));

    // this should not crash hyprland. If we are alive, we good.
}

TEST_CASE(issue14134) {
    OK(getFromSocket("/output create headless HEADLESS-4"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-4' })"));

    SPAWN_KITTY("kitty_14134");

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle', window = 'class:kitty_14134' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.pin({ action = 'toggle', window = 'class:kitty_14134' })"));

    OK(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-4', disabled = true })"));

    // will return error, should not be OK.
    getFromSocket("/dispatch hl.dsp.window.pin({ action = 'toggle', window = 'class:kitty_14134' })");

    OK(getFromSocket("/reload"));
    Tests::killAllWindows();
    ASSERT(Tests::windowCount(), 0);
    OK(getFromSocket("/output remove HEADLESS-4"));
}

TEST_CASE(specialFloatRecenters) {
    SPAWN_KITTY("kitty_special_float_recenter");

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:kitty_special_float_recenter' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 10, y = 10, window = 'class:kitty_special_float_recenter' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = 'special:recenter', follow = false, window = 'class:kitty_special_float_recenter' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 50000, y = 50000, window = 'class:kitty_special_float_recenter' })"));

    OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('recenter')"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_special_float_recenter' })"));

    const auto active = getFromSocket("/activewindow");
    EXPECT_CONTAINS(active, "class: kitty_special_float_recenter");
    EXPECT_CONTAINS(active, "size: 10,10");
    EXPECT_CONTAINS(active, "at: 955,535");

    OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('recenter')"));
    Tests::killAllWindows();
    ASSERT(Tests::windowCount(), 0);
}

TEST_CASE(exactWindowSelectors) {
    SPAWN_KITTY("kitty_A");
    SPAWN_KITTY("kitty_B");

    getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })");
    auto target    = getFromSocket("/activewindow");
    auto addr      = getWindowAddress(target);
    auto pid       = Tests::getAttribute(target, "pid");
    auto stable_id = Tests::getAttribute(target, "stableID");

    getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })");
    // Focus window by address
    OK(getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'address:0x{}' }})", addr)));
    EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: kitty_B");

    getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })");
    // Focus window by PID
    OK(getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'pid:{}' }})", pid)));
    EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: kitty_B");

    getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })");
    // Focus window by stable ID
    OK(getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'stableid:{}' }})", stable_id)));
    EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: kitty_B");
}

// TODO: decompose this into multiple test cases
TEST_CASE(windows) {
    // test on workspace "window"
    OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })"));
    NLog::log("{}Switching to workspace `window`", Colors::YELLOW);
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:window' })"));

    SPAWN_KITTY("kitty_A");

    // check kitty properties. One kitty should take the entire screen, as this is smart gaps
    NLog::log("{}Expecting kitty_A to take up the whole screen", Colors::YELLOW);
    {
        auto str = getFromSocket("/clients");
        EXPECT(str.contains("at: 0,0"), true);
        EXPECT(str.contains("size: 1920,1080"), true);
        EXPECT(str.contains("fullscreen: 0"), true);
    }

    NLog::log("{}Testing window split ratios", Colors::YELLOW);
    {
        const double INITIAL_RATIO = 1.25;
        const int    GAPSIN        = 5;
        const int    GAPSOUT       = 20;
        const int    BORDERSIZE    = 2;
        const int    BORDERS       = BORDERSIZE * 2;
        const int    MONITOR_W     = 1920;
        const int    MONITOR_H     = 1080;

        const float  totalAvailableHeight   = MONITOR_H - (GAPSOUT * 2);
        const int    HEIGHT                 = std::floor(totalAvailableHeight) - BORDERS;
        const float  availableWidthForSplit = MONITOR_W - (GAPSOUT * 2) - GAPSIN;

        auto         calculateFinalWidth = [&](double boxWidth, bool isLeftWindow) {
            double gapLeft  = isLeftWindow ? GAPSOUT : GAPSIN;
            double gapRight = isLeftWindow ? GAPSIN : GAPSOUT;
            return std::floor(boxWidth - gapLeft - gapRight - BORDERS);
        };

        double       geomBoxWidthA_R1 = (availableWidthForSplit * INITIAL_RATIO / 2.0) + GAPSOUT + (GAPSIN / 2.0);
        double       geomBoxWidthB_R1 = MONITOR_W - geomBoxWidthA_R1;
        const int    WIDTH1           = calculateFinalWidth(geomBoxWidthB_R1, false);

        const double INVERTED_RATIO   = 0.75;
        double       geomBoxWidthA_R2 = (availableWidthForSplit * INVERTED_RATIO / 2.0) + GAPSOUT + (GAPSIN / 2.0);
        double       geomBoxWidthB_R2 = MONITOR_W - geomBoxWidthA_R2;
        const int    WIDTH2           = calculateFinalWidth(geomBoxWidthB_R2, false);
        const int    WIDTH_A_FINAL    = calculateFinalWidth(geomBoxWidthA_R2, true);

        OK(getFromSocket("/eval hl.config({ dwindle = { default_split_ratio = 1.25 } })"));

        SPAWN_KITTY("kitty_B");

        NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH1, HEIGHT);
        EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH1, HEIGHT));

        OK(getFromSocket("/dispatch hl.dsp.window.kill({ window = 'activewindow' })"));
        Tests::waitUntilWindowsN(1);

        NLog::log("{}Inverting the split ratio", Colors::YELLOW);
        OK(getFromSocket("/eval hl.config({ dwindle = { default_split_ratio = 0.75 } })"));

        SPAWN_KITTY("kitty_B");

        try {
            NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH2, HEIGHT);

            {
                auto data = getFromSocket("/activewindow");
                data      = data.substr(data.find("size:") + 5);
                data      = data.substr(0, data.find('\n'));

                Hyprutils::String::CVarList2 sizes(std::move(data), 0, ',');

                EXPECT_MAX_DELTA(std::stoi(std::string{sizes[0]}), WIDTH2, 2);
                EXPECT_MAX_DELTA(std::stoi(std::string{sizes[1]}), HEIGHT, 2);
            }

            OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"));
            NLog::log("{}Expecting kitty_A size: {},{}", Colors::YELLOW, WIDTH_A_FINAL, HEIGHT);

            {
                auto data = getFromSocket("/activewindow");
                data      = data.substr(data.find("size:") + 5);
                data      = data.substr(0, data.find('\n'));

                Hyprutils::String::CVarList2 sizes(std::move(data), 0, ',');

                EXPECT_MAX_DELTA(std::stoi(std::string{sizes[0]}), WIDTH_A_FINAL, 2);
                EXPECT_MAX_DELTA(std::stoi(std::string{sizes[1]}), HEIGHT, 2);
            }

        } catch (...) { FAIL_TEST("Exception thrown"); }

        OK(getFromSocket("/eval hl.config({ dwindle = { default_split_ratio = 1 } })"));
    }

    // open xeyes
    NLog::log("{}Spawning xeyes", Colors::YELLOW);
    getFromSocket("/dispatch hl.dsp.exec_cmd('xeyes')");

    NLog::log("{}Keep checking if xeyes spawned", Colors::YELLOW);
    Tests::waitUntilWindowsN(3);

    NLog::log("{}Expecting 3 windows", Colors::YELLOW);
    ASSERT(Tests::windowCount(), 3);

    NLog::log("{}Checking props of xeyes", Colors::YELLOW);
    // check some window props of xeyes, try to float it
    {
        auto str = getFromSocket("/clients");
        EXPECT_NOT_CONTAINS(str, "floating: 1");
        getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:XEyes' })");
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        str = getFromSocket("/clients");
        EXPECT_CONTAINS(str, "floating: 1");
    }

    // kill all
    NLog::log("{}Killing all windows", Colors::YELLOW);
    Tests::killAllWindows();

    NLog::log("{}Expecting 0 windows", Colors::YELLOW);
    ASSERT(Tests::windowCount(), 0);

    getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })");

    NLog::log("{}Testing spawning a floating window over a fullscreen window", Colors::YELLOW);
    {
        SPAWN_KITTY("kitty_A");
        OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })"));
        ASSERT(Tests::windowCount(), 1);

        OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true })"));
        Tests::waitUntilWindowsN(2);

        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:^kitty$' })"));
        const auto focused1 = getFromSocket("/activewindow");
        EXPECT_CONTAINS(focused1, "class: kitty\n");

        OK(getFromSocket("/dispatch hl.dsp.window.kill({ window = 'activewindow' })"));
        Tests::waitUntilWindowsN(1);

        // The old window should be focused again
        const auto focused2 = getFromSocket("/activewindow");
        EXPECT_CONTAINS(focused2, "class: kitty_A\n");

        NLog::log("{}Killing all windows", Colors::YELLOW);
        Tests::killAllWindows();
    }

    NLog::log("{}Testing minsize/maxsize rules for tiled windows", Colors::YELLOW);
    {
        // Enable the config for testing, test max/minsize for tiled windows and centering
        OK(getFromSocket("/eval hl.config({ misc = { size_limits_tiled = 1 } })"));
        OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', match = { class = 'kitty_maxsize' } })"));
        OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', max_size = '1500 500' })"));
        OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', min_size = '1200 500' })"));
        SPAWN_KITTY("kitty_maxsize");

        auto dwindle = getFromSocket("/activewindow");
        EXPECT_CONTAINS(dwindle, "size: 1500,500");
        EXPECT_CONTAINS(dwindle, "at: 210,290");

        // Fuck this test, it's fucking stupid - vax
        // if (!spawnKitty("kitty_maxsize"))
        //     return false;
        // EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500");

        Tests::killAllWindows();
        ASSERT(Tests::windowCount(), 0);

        OK(getFromSocket("r/eval hl.config({ general = { layout = 'master' } })"));

        SPAWN_KITTY("kitty_maxsize");

        auto master = getFromSocket("/activewindow");
        EXPECT_CONTAINS(master, "size: 1500,500");
        EXPECT_CONTAINS(master, "at: 210,290");

        SPAWN_KITTY("kitty_maxsize");

        // FIXME: I can't be arsed.
        OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_maxsize' })"));
        //        EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500")

        NLog::log("{}Reloading config", Colors::YELLOW);
        OK(getFromSocket("/reload"));
        Tests::killAllWindows();
        ASSERT(Tests::windowCount(), 0);
    }

    NLog::log("{}Testing minsize/maxsize rules", Colors::YELLOW);
    {
        // Disable size limits tiled and check if props are working and not getting skipped
        OK(getFromSocket("/eval hl.config({ misc = { size_limits_tiled = 0 } })"));
        OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', match = { class = 'kitty_maxsize' } })"));
        OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', max_size = '1500 500' })"));
        OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', min_size = '1200 500' })"));
        SPAWN_KITTY("kitty_maxsize");

        {
            auto res = getFromSocket("/getprop active max_size");
            EXPECT_CONTAINS(res, "1500");
            EXPECT_CONTAINS(res, "500");
        }

        {
            auto res = getFromSocket("/getprop active min_size");
            EXPECT_CONTAINS(res, "1200");
            EXPECT_CONTAINS(res, "500");
        }

        NLog::log("{}Reloading config", Colors::YELLOW);
        OK(getFromSocket("/reload"));
        Tests::killAllWindows();
        ASSERT(Tests::windowCount(), 0);
    }

    {
        // Set float
        OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', match = { class = 'kitty_maxsize' } })"));
        OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', max_size = '1200 500' })"));
        OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', min_size = '1200 500' })"));
        OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', float = true })"));
        SPAWN_KITTY("kitty_maxsize");

        {
            auto res = getFromSocket("/getprop active max_size");
            EXPECT_CONTAINS(res, "1200");
            EXPECT_CONTAINS(res, "500");
        }

        {
            auto res = getFromSocket("/getprop active min_size");
            EXPECT_CONTAINS(res, "1200");
            EXPECT_CONTAINS(res, "500");
        }

        {
            auto res = getFromSocket("/activewindow");
            EXPECT_CONTAINS(res, "size: 1200,500");
        }

        NLog::log("{}Reloading config", Colors::YELLOW);
        OK(getFromSocket("/reload"));
        Tests::killAllWindows();
        ASSERT(Tests::windowCount(), 0);
    }

    NLog::log("{}Testing window rules", Colors::YELLOW);
    SPAWN_KITTY("wr_kitty");
    {
        auto      str  = getFromSocket("/activewindow");
        const int SIZE = 200;
        EXPECT_CONTAINS(str, "floating: 1");
        EXPECT_CONTAINS(str, std::format("size: {},{}", SIZE, SIZE));
        EXPECT_NOT_CONTAINS(str, "pinned: 1");
    }

    OK(getFromSocket("/eval hl.window_rule({ name = 'wr-kitty-stuff', opacity = '0.5 0.5 override' })"));

    {
        auto str = getFromSocket("/getprop active opacity");
        EXPECT_CONTAINS(str, "0.5");
    }

    OK(getFromSocket("/eval hl.window_rule({ name = 'special-magic-kitty', match = { class = 'magic_kitty' } })"));
    OK(getFromSocket("/eval hl.window_rule({ name = 'special-magic-kitty', workspace = 'special:magic' })"));

    SPAWN_KITTY("magic_kitty");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT_CONTAINS(str, "special:magic");
        EXPECT_NOT_CONTAINS(str, "workspace: 9");
    }

    if (auto str = getFromSocket("/monitors"); str.contains("magic)")) {
        OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('magic')"));
    }

    Tests::killAllWindows();

    OK(getFromSocket("/eval hl.window_rule({ name = 'border-magic-kitty', match = { class = 'border_kitty' } })"));
    OK(getFromSocket("/eval hl.window_rule({ name = 'border-magic-kitty', border_color = 'rgba(c6ff00ff) rgba(ff0000ee) 45deg' })"));

    SPAWN_KITTY("border_kitty");

    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:border_kitty' })"));

    {
        auto str = getFromSocket("/getprop active active_border_color");
        EXPECT_CONTAINS(str, "ffc6ff00");
        EXPECT_CONTAINS(str, "eeff0000");
        EXPECT_CONTAINS(str, "45deg");
    }

    Tests::killAllWindows();

    SPAWN_KITTY("tag_kitty");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT_CONTAINS(str, "floating: 1");
    }

    OK(getFromSocket("/reload"));
    Tests::killAllWindows();

    // test rules that overlap effects but don't overlap props
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'overlap_kitty' }, border_size = 0 })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { fullscreen = false }, border_size = 10 })"));

    SPAWN_KITTY("overlap_kitty");

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "10");
    }

    OK(getFromSocket("/reload"));
    Tests::killAllWindows();

    // test persistent_size between floating window launches
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'persistent_size_kitty' }, persistent_size = true, float = true })"));

    SPAWN_KITTY("persistent_size_kitty");

    OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 600, y = 400 })"));

    {
        auto str = getFromSocket("/activewindow");
        EXPECT_CONTAINS(str, "size: 600,400");
        EXPECT_CONTAINS(str, "floating: 1");
    }

    Tests::killAllWindows();

    SPAWN_KITTY("persistent_size_kitty");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT_CONTAINS(str, "size: 600,400");
        EXPECT_CONTAINS(str, "floating: 1");
    }

    OK(getFromSocket("/reload"));
    Tests::killAllWindows();

    OK(getFromSocket("/eval hl.config({ general = { border_size = 0 } })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { float = true }, border_size = 10 })"));

    SPAWN_KITTY("border_kitty");

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "0");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle' })"));

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "10");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle' })"));

    {
        auto str = getFromSocket("/getprop active border_size");
        EXPECT_CONTAINS(str, "0");
    }

    OK(getFromSocket("/reload"));
    Tests::killAllWindows();

    // test expression rules
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'expr_kitty' }, float = true, size = {'monitor_w * 0.5', 'monitor_h * 0.5'}, "
                     "min_size = {'monitor_w * 0.25', 'monitor_h * 0.25'}, max_size = {'monitor_w * 0.75', 'monitor_h * 0.75'}, "
                     "move = {'20 + (monitor_w * 0.1)', 'monitor_h * 0.5'} })"));

    SPAWN_KITTY("expr_kitty");

    {
        auto str = getFromSocket("/activewindow");
        EXPECT_CONTAINS(str, "floating: 1");
        EXPECT_CONTAINS(str, "at: 212,540");
        EXPECT_CONTAINS(str, "size: 960,540");

        auto min = getFromSocket("/getprop active min_size");
        EXPECT_CONTAINS(min, "480");
        EXPECT_CONTAINS(min, "270");

        auto max = getFromSocket("/getprop active max_size");
        EXPECT_CONTAINS(max, "1440");
        EXPECT_CONTAINS(max, "810");
    }

    OK(getFromSocket("/reload"));
    Tests::killAllWindows();

    OK(getFromSocket("/eval hl.plugin.test.add_window_rule()"));
    OK(getFromSocket("/reload"));

    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'plugin_kitty' }, plugin_rule = 'effect' })"));

    SPAWN_KITTY("plugin_kitty");

    OK(getFromSocket("/eval hl.plugin.test.check_window_rule()"));

    OK(getFromSocket("/reload"));
    Tests::killAllWindows();

    OK(getFromSocket("/eval hl.plugin.test.add_window_rule()"));
    OK(getFromSocket("/reload"));

    OK(getFromSocket("/eval hl.window_rule({ name = 'test-plugin-rule', match = { class = 'plugin_kitty' } })"));
    OK(getFromSocket("/eval hl.window_rule({ name = 'test-plugin-rule', plugin_rule = 'effect' })"));

    SPAWN_KITTY("plugin_kitty");

    OK(getFromSocket("/eval hl.plugin.test.check_window_rule()"));
}

TEST_CASE(cycle_nextTiled) {
    // If the user specifically requests a tiled window, give them a tiled window

    for (const auto& win : {"a", "b", "c", "d"}) {
        SPAWN_KITTY(win);
    }

    // float the class:a window
    OK(getFromSocket("/dispatch hl.dsp.window.float({action = 'enable', window = 'class:a'})"));
    // float the class:c window
    OK(getFromSocket("/dispatch hl.dsp.window.float({action = 'enable', window = 'class:c'})"));
    // float the class:d window
    OK(getFromSocket("/dispatch hl.dsp.window.float({action = 'enable', window = 'class:d'})"));

    // establish focus history
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})")); // floating
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:b'})")); // tiled  <-- What we want to focus on
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:c'})")); // floating
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:d'})")); // floating

    // request a tiled window
    OK(getFromSocket("/dispatch hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false})"));

    ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: b");
}

TEST_CASE(cycle_nextFloating) {
    // If the user specifically requests a floating window, give them a floating window

    for (const auto& win : {"a", "b", "c", "d"}) {
        SPAWN_KITTY(win);
    }

    // float the class:b window
    OK(getFromSocket("/dispatch hl.dsp.window.float({action = 'enable', window = 'class:b'})"));

    // establish focus history
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})")); // tiled
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:b'})")); // floating  <-- What we want to focus on
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:c'})")); // tiled
    OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:d'})")); // tiled

    // request a floating window
    OK(getFromSocket("/dispatch hl.dsp.window.cycle_next({ next = true, tiled = false, floating = true})"));

    ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: b");
}

TEST_CASE(discussion14233) {
    // toggling fullscreen should not mess up workspace state on a 2 window workspace
    // with smart gaps

    OK(getFromSocket(R"#(/eval hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 })
hl.workspace_rule({ workspace = "f[1]",   gaps_out = 0, gaps_in = 0 })
hl.window_rule({
    name  = "no-gaps-wtv1",
    match = { float = false, workspace = "w[tv1]" },
    border_size = 0,
    rounding    = 0,
})
hl.window_rule({
    name  = "no-gaps-f1",
    match = { float = false, workspace = "f[1]" },
    border_size = 0,
    rounding    = 0,
})
)#"));

    ASSERT(!!Tests::spawnKitty(), true);

    // just make sure smart gaps work
    {
        auto str = getFromSocket("/clients");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "at: 0,0");
    }

    ASSERT(!!Tests::spawnKitty(), true);

    {
        auto str = getFromSocket("/clients");
        ASSERT_COUNT_STRING(str, "size: 931,1036", 2);
        ASSERT_CONTAINS(str, "at: 22,22");
        ASSERT_CONTAINS(str, "at: 967,22");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));

    {
        auto str = getFromSocket("/clients");
        ASSERT_COUNT_STRING(str, "size: 931,1036", 2);
        ASSERT_CONTAINS(str, "at: 22,22");
        ASSERT_CONTAINS(str, "at: 967,22");
    }
}

TEST_CASE(execRulesWorkspaceOverride) {
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_exec_override' }, workspace = '2' })"));

    OK(getFromSocket("/dispatch hl.dsp.exec_cmd('[workspace 3] kitty --class kitty_exec_override')"));

    Tests::waitUntilWindowsN(1);

    auto str = getFromSocket("/activewindow");
    EXPECT_CONTAINS(str, "class: kitty_exec_override");
    EXPECT_CONTAINS(str, "workspace: 3");

    Tests::killAllWindows();
}

TEST_CASE(execRulesTagMutation) {
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_tag_mutate' }, workspace = '2' })"));
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_tag_mutate' }, tag = 'test_tag' })"));

    OK(getFromSocket("/dispatch hl.dsp.exec_cmd('[workspace 3] kitty --class kitty_tag_mutate')"));

    Tests::waitUntilWindowsN(1);

    auto str = getFromSocket("/activewindow");
    EXPECT_CONTAINS(str, "class: kitty_tag_mutate");
    EXPECT_CONTAINS(str, "workspace: 3");

    Tests::killAllWindows();
}

TEST_CASE(execRulesDescendantProcess) {
    OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty --class kitty_exec_workspace & wait', { workspace = '2 silent' })"));

    Tests::waitUntilWindowsN(1);

    EXPECT_CONTAINS(getFromSocket("/getprop class:kitty_exec_workspace no_focus"), "false");
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_exec_workspace' })"));

    {
        const auto active = getFromSocket("/activewindow");
        EXPECT_CONTAINS(active, "class: kitty_exec_workspace");
        EXPECT_CONTAINS(active, "workspace: 2");
    }

    OK(getFromSocket(
        "/dispatch hl.dsp.exec_cmd('[workspace 3; border_size 7] kitty --class kitty_exec_special & wait', { workspace = 'special:exec_rules silent', no_initial_focus = true, "
        "no_focus = true })"));

    Tests::waitUntilWindowsN(2);

    EXPECT_CONTAINS(getFromSocket("/getprop class:kitty_exec_special no_focus"), "true");
    EXPECT_CONTAINS(getFromSocket("/getprop class:kitty_exec_special border_size"), "7");

    {
        const auto active = getFromSocket("/activewindow");
        EXPECT_CONTAINS(active, "class: kitty_exec_workspace");
        EXPECT_CONTAINS(active, "workspace: 2");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.set_prop({ window = 'class:kitty_exec_special', prop = 'no_focus', value = 'false' })"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_exec_special' })"));

    {
        const auto active = getFromSocket("/activewindow");
        EXPECT_CONTAINS(active, "class: kitty_exec_special");
        EXPECT_CONTAINS(active, "special:exec_rules");
    }

    Tests::killAllWindows();
}

TEST_CASE(execRulesPendingIsolation) {
    OK(getFromSocket(
        "/eval do hl.exec_cmd('sleep 1 && kitty --class kitty_exec_first', { workspace = '2 silent' }); "
        "hl.exec_cmd('sleep 2 && kitty --class kitty_exec_second', { workspace = 'special:exec_rules_pending silent', no_initial_focus = true, no_focus = true }) end"));

    Tests::waitUntilWindowsN(1);

    EXPECT_CONTAINS(getFromSocket("/getprop class:kitty_exec_first no_focus"), "false");
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_exec_first' })"));

    {
        const auto active = getFromSocket("/activewindow");
        EXPECT_CONTAINS(active, "class: kitty_exec_first");
        EXPECT_CONTAINS(active, "workspace: 2");
    }

    Tests::waitUntilWindowsN(2);

    EXPECT_CONTAINS(getFromSocket("/getprop class:kitty_exec_first no_focus"), "false");
    EXPECT_CONTAINS(getFromSocket("/getprop class:kitty_exec_second no_focus"), "true");

    OK(getFromSocket("/dispatch hl.dsp.window.set_prop({ window = 'class:kitty_exec_second', prop = 'no_focus', value = 'false' })"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_exec_second' })"));

    {
        const auto active = getFromSocket("/activewindow");
        EXPECT_CONTAINS(active, "class: kitty_exec_second");
        EXPECT_CONTAINS(active, "special:exec_rules_pending");
    }

    Tests::killAllWindows();
}

TEST_CASE(pinnedRetainsPositionOnWorkspaceChange) {
    ASSERT(!!Tests::spawnKitty("a"), true);

    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"));

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'enable' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.pin()"));

    {
        auto str = getFromSocket("/clients");
        ASSERT_CONTAINS(str, "pinned: 1");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 1900, y = 1000 })"));

    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '2' })"));

    {
        auto str = getFromSocket("/workspaces");
        ASSERT_CONTAINS(str, "workspace ID 2");
    }

    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"));

    {
        auto str = getFromSocket("/workspaces");
        ASSERT_CONTAINS(str, "workspace ID 1");
    }

    {
        auto str = getFromSocket("/clients");
        ASSERT_CONTAINS(str, "pinned: 1");
        ASSERT_CONTAINS(str, "at: 1900,1000");
    }
}

TEST_CASE(monitorrule) {
    if (getFromSocket("/monitors all").contains("HEADLESS-3"))
        OK(getFromSocket("/output remove HEADLESS-3"));

    OK(getFromSocket("/output create headless HEADLESS-3"));
    const auto  MONALL = getFromSocket("/monitors all");
    CScopeGuard guard([&] {
        if (MONALL.contains("HEADLESS-3"))
            OK(getFromSocket("/output remove HEADLESS-3"));
    });

    OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })"));
    OK(getFromSocket("/eval hl.window_rule({ name = 'monitorrule', match = { class = 'monitor_kitty' }, monitor = 'HEADLESS-3' })"));

    Tests::spawnKitty("monitor_kitty");
    ASSERT(Tests::windowCount(), 1);
    const auto MON_SRC_ID = Tests::getAttribute(getFromSocket("/activewindow"), "monitor");
    ASSERT_CONTAINS(MONALL, std::format("HEADLESS-3 (ID {}", MON_SRC_ID));
    EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "HEADLESS-3");

    Tests::killAllWindows();
    OK(getFromSocket("/reload"));

    OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })"));
    OK(getFromSocket("/eval hl.window_rule({ name = 'monitorrule', match = { class = 'silent_kitty' }, monitor = 'HEADLESS-3 silent' })"));

    Tests::spawnKitty("silent_kitty");
    ASSERT(Tests::windowCount(), 1);
    const auto SILENT_SRC_ID = Tests::getAttribute(getFromSocket("/clients"), "monitor");
    ASSERT_CONTAINS(MONALL, std::format("HEADLESS-3 (ID {}", SILENT_SRC_ID));
    EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "HEADLESS-2");
}

TEST_CASE(mouseResize) {
#define RESET_WINDOW()                                                                                                                                                             \
    OK(getFromSocket("r/reload"));                                                                                                                                                 \
    OK(getFromSocket("r/eval hl.unbind('mouse:273')"));                                                                                                                            \
    OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 640, y = 400, window = 'class:kitty' })"));                                                                             \
    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 0, y = 0, window = 'class:kitty' })"));                                                                                   \
    OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 640, y = 400 })"));                                                                                                       \
    EXPECT_CONTAINS(getFromSocket("/clients"), "size: 640,400");                                                                                                                   \
    EXPECT_CONTAINS(getFromSocket("/clients"), "at: 0,0");

    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty' }, float = true })"));
    Tests::spawnKitty();
    ASSERT(Tests::windowCount(), 1);
    OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty' })"));

    RESET_WINDOW();
    OK(getFromSocket("r/eval hl.bind('mouse:273', hl.dsp.window.resize(), { mouse = true })"));
    OK(getFromSocket("/eval hl.plugin.test.click(273, 1)"));

    // Position setting works immediately, but updating window sizes interactively doesn't.
    for (size_t i = 0; i < 50; i++) {
        OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 700, y = 200 })"));
        if (getFromSocket("/clients").contains("size: 700,200")) {
            break;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
    OK(getFromSocket("/eval hl.plugin.test.click(273, 0)"));
    EXPECT_CONTAINS(getFromSocket("/clients"), "size: 700,200");

    RESET_WINDOW();
    OK(getFromSocket("r/eval hl.bind('mouse:273', hl.dsp.window.resize({ keep_aspect_ratio = true }), { mouse = true })"));
    OK(getFromSocket("/eval hl.plugin.test.click(273, 1)"));
    OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 1280, y = 100 })"));
    OK(getFromSocket("/eval hl.plugin.test.click(273, 0)"));
    EXPECT_CONTAINS(getFromSocket("/clients"), "size: 1280,800");

    RESET_WINDOW();
    OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty' }, keep_aspect_ratio = true })"));
    OK(getFromSocket("r/eval hl.bind('mouse:273', hl.dsp.window.resize({ keep_aspect_ratio = false }), { mouse = true })"));
    OK(getFromSocket("/eval hl.plugin.test.click(273, 1)"));
    OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 700, y = 200 })"));
    OK(getFromSocket("/eval hl.plugin.test.click(273, 0)"));
    EXPECT_CONTAINS(getFromSocket("/clients"), "size: 700,200");
#undef RESET_WINDOW
}

TEST_CASE(getFullscreenHandler) {

    // Correctly detect the FS handler of a window

    // We will use scrolling as it has a layout handled FS

    // First test that it's obtained properly in a default FS handled layout - dwindle

    OK(getFromSocket("r/eval hl.config({ general = { layout = 'dwindle' } })"));

    // Tiled
    Tests::spawnKitty("cat");

    // FS - check - unFS
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));
    EXPECT(Tests::getAttribute(getFromSocket("/activewindow"), "fullscreenHandler"), "default");
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));

    // Floating

    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })"));

    // FS - check - unFS
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));
    EXPECT(Tests::getAttribute(getFromSocket("/activewindow"), "fullscreenHandler"), "default");
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));

    // switch to scrolling layout

    OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));

    // Window still floating. There is no layout handled floating FS behaviour

    // FS - check - unFS
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));
    EXPECT(Tests::getAttribute(getFromSocket("/activewindow"), "fullscreenHandler"), "default");
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));

    // tile the window, and explicitly default handle it
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'unset' })"));

    // FS - check - unFS
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ layout_aware = false })"));
    EXPECT(Tests::getAttribute(getFromSocket("/activewindow"), "fullscreenHandler"), "default");
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ layout_aware = false })"));

    // check if layout handler is correctly ontaind

    // FS - check - unFS
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));
    EXPECT(Tests::getAttribute(getFromSocket("/activewindow"), "fullscreenHandler"), "scrolling");
    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()"));
}

TEST_CASE(sendFsWindowToAnotherWorkspace) {

    /*
    If default handled, will be moved as default handled to other workspace.
    If layout handled, will be reFSed using target workspace's layout handled if exists

    FS sizes are the same for all FS handlers so we share them

    */

    // Test Default Handled FS

    // Establish 5 workspaces with layouts
    OK(getFromSocket("/eval hl.workspace_rule({workspace = '1', layout = 'dwindle'})"));
    OK(getFromSocket("/eval hl.workspace_rule({workspace = '2', layout = 'master'})"));
    OK(getFromSocket("/eval hl.workspace_rule({workspace = '3', layout = 'monocle'})"));
    OK(getFromSocket("/eval hl.workspace_rule({workspace = '4', layout = 'scrolling'})"));
    OK(getFromSocket("/eval hl.workspace_rule({workspace = '5', layout = 'scrolling'})"));

    // set gaps_out to a high value and establish a workspace rule to catch cases where maximised gets the wrong work area size
    OK(getFromSocket("/eval hl.config({ general = { gaps_out = 20,}})"));
    OK(getFromSocket("/eval hl.workspace_rule({ workspace = 'f[1]', gaps_out = 0, })"));

    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"));

    Tests::spawnKitty("traveller_1");

    // Testing with Tiled window

    /*
    Test with default handled FS window
    */

    // Testing Fullscreen

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'fullscreen'})"));

    // dwindle
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 1");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // master - try with follow = false
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '2', follow = false })"));
    // go to that workspace post move - expect FS window to be in focus
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 2 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 2");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // monocle
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '3' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 3");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // scroll
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // Try with follow = false for scroll since it's default to layout handled and vice versa and should be tested thoroughly

    // monocle
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '3' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 3");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // scroll - try with follow = false
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4', follow = false })"));
    // go to that workspace post move - expect FS window to be in focus
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 4 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // before next test kill the window and make a new one on workspace 1 to get a fresh start

    Tests::killAllWindows();
    Tests::waitUntilWindowsN(0);

    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"));
    Tests::spawnKitty("traveller_2");

    // Testing Maximised

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'maximized'})"));

    // dwindle
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 1");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // master - try with follow = false
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '2', follow = false })"));
    // go to that workspace post move - expect FS window to be in focus
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 2 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 2");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // monocle
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '3' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 3");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // scroll
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // Try with follow = false for scroll since it's default to layout handled and vice versa and should be tested thoroughly

    // monocle
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '3' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 3");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // scroll - try with follow = false
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4', follow = false })"));
    // go to that workspace post move - expect FS window to be in focus
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 4 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // Before next test kill the window and make a new one on workspace 1 to get a fresh start

    Tests::killAllWindows();
    Tests::waitUntilWindowsN(0);

    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"));
    Tests::spawnKitty("traveller_3");

    /*
    Test Layout Handled FS - Move btw scroll-scroll and scroll-dwindle
    */

    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4' })"));

    // fullscreen

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'fullscreen'})"));

    // scroll
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: scrolling");
    }

    // move to another scrolling workspace
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '5' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 5");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        // both have layout handlers so expected to use that
        ASSERT_CONTAINS(str, "fullscreenHandler: scrolling");
    }

    // dwindle
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '1' })"));
    // go to that workspace post move - expect FS window to be in focus
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 1");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        // No layout handler in dwindle so expect it to fall back to using the only available handler which is default
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // scroll - try with follow = false
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4', follow = false })"));
    // go to that workspace post move - expect FS window to be in focus
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 4 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        // It's not coming from a default handler so expect it to remain as such
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // maximised

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'maximized'})"));

    // scroll
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: scrolling");
    }

    // move to another scrolling workspace
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '5' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 5");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        // both have layout handlers so expected to use that
        ASSERT_CONTAINS(str, "fullscreenHandler: scrolling");
    }

    // dwindle
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '1' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 1");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        // No layout handler in dwindle so expect it to fall back to using the only available handler which is default
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // scroll - try with follow = false
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4', follow = false })"));
    // go to that workspace post move - expect FS window to be in focus
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 4 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        // It's coming from a default handler so expect it to remain as such
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
    }

    // Testing the same things but with floating
    // floating has no layout handled Fs so just test moving a floating FS window around with maximised and fullscreen values changing between every move

    Tests::killAllWindows();
    Tests::waitUntilWindowsN(0);

    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"));
    Tests::spawnKitty("traveller_float");
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:traveller_float' })"));

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'fullscreen'})"));

    // workspace 1 (dwindle) - fullscreen
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 1");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    // move to workspace 2 (master) - still fullscreen, then toggle to maximized
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '2' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 2");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'maximized'})"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 2");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    // move to workspace 3 (monocle) - still maximized, then toggle to fullscreen
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '3' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 3");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'fullscreen'})"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 3");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    // move to workspace 4 (scrolling) - still fullscreen, then toggle to maximized
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '4' })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'maximized'})"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 4");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    // move to workspace 5 (scrolling) with follow = false - still maximized, then toggle to fullscreen
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '5', follow = false })"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 5 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 2,2");
        ASSERT_CONTAINS(str, "size: 1916,1076");
        ASSERT_CONTAINS(str, "workspace: 5");
        ASSERT_CONTAINS(str, "fullscreen: 1");
        ASSERT_CONTAINS(str, "fullscreenClient: 1");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'fullscreen'})"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 5");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }

    // move back to workspace 1 (dwindle) with follow = false - still fullscreen
    OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '1', follow = false })"));
    OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 1 })"));
    {
        const auto str = getFromSocket("/activewindow");
        ASSERT_CONTAINS(str, "at: 0,0");
        ASSERT_CONTAINS(str, "size: 1920,1080");
        ASSERT_CONTAINS(str, "workspace: 1");
        ASSERT_CONTAINS(str, "fullscreen: 2");
        ASSERT_CONTAINS(str, "fullscreenClient: 2");
        ASSERT_CONTAINS(str, "fullscreenHandler: default");
        ASSERT_CONTAINS(str, "floating: 1");
    }
}

TEST_CASE(floatingForceOnscreen) {
    // Test: move a floating window around
    NLog::log("{}Testing force-onscreen for existing floating windows", Colors::GREEN);
    Tests::spawnKitty();
    OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'on' })"));
    OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 100, y = 100, relative = false })"));

    // No constraints
    OK(getFromSocket("/eval hl.config({ misc = { float_force_onscreen = 0 }})"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = -110, y = 0, relative = false })"));
    ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -110,0");

    // Partially onscreen
    OK(getFromSocket("/eval hl.config({ misc = { float_force_onscreen = 1 }})"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = -110, y = 0, relative = false })"));
    ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -50,0");

    // Fully onscreen
    OK(getFromSocket("/eval hl.config({ misc = { float_force_onscreen = 2 }})"));
    OK(getFromSocket("/dispatch hl.dsp.window.move({ x = -10, y = 0, relative = false })"));
    ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: 2,2");

    OK(getFromSocket("/dispatch hl.dsp.window.kill()"));

    // Test: spawn new floating windows offscreen
    // NOTE: These new-window tests all run with float_force_onscreen = 2.
    // That's intended, and it shouldn't limit where new windows can spawn.
    NLog::log("{}Testing force-onscreen for new floating windows", Colors::GREEN);

    // Helper macro, because spawnKitty() doesn't support executing with rules
#define WAIT_FOR_WINDOW(N)                                                                                                                                                         \
    do {                                                                                                                                                                           \
        std::this_thread::sleep_for(std::chrono::milliseconds(10));                                                                                                                \
        int counter = 0;                                                                                                                                                           \
        while (Tests::windowCount() == N) {                                                                                                                                        \
            counter++;                                                                                                                                                             \
            std::this_thread::sleep_for(std::chrono::milliseconds(100));                                                                                                           \
            if (counter > 50)                                                                                                                                                      \
                break;                                                                                                                                                             \
        }                                                                                                                                                                          \
    } while (0)

    // No constraints
    OK(getFromSocket("/eval hl.config({ misc = { new_float_force_onscreen = 0 }})"));
    OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true, size = {100, 100}, move = {-110, 0} })"));
    WAIT_FOR_WINDOW(0);
    ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -110,0");
    OK(getFromSocket("/dispatch hl.dsp.window.kill()"));

    // Partially onscreen
    OK(getFromSocket("/eval hl.config({ misc = { new_float_force_onscreen = 1 }})"));
    OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true, size = {100, 100}, move = {-110, 0} })"));
    WAIT_FOR_WINDOW(0);
    ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -50,0");
    OK(getFromSocket("/dispatch hl.dsp.window.kill()"));

    // Fully onscreen
    OK(getFromSocket("/eval hl.config({ misc = { new_float_force_onscreen = 2 }})"));
    OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true, size = {100, 100}, move = {-10, 0} })"));
    WAIT_FOR_WINDOW(0);
    // NOTE: As we spawn with rules, apparently border_size isn't known yet, so
    // we get placed at (0,0) rather than (2,2)
    ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0");
    OK(getFromSocket("/dispatch hl.dsp.window.kill()"));

#undef WAIT_FOR_WINDOW
}
