#!perl
# vim:ts=4:sw=4:expandtab
#
# Please read the following documents before working on tests:
# • https://build.i3wm.org/docs/testsuite.html
#   (or docs/testsuite)
#
# • https://build.i3wm.org/docs/lib-i3test.html
#   (alternatively: perldoc ./testcases/lib/i3test.pm)
#
# • https://build.i3wm.org/docs/ipc.html
#   (or docs/ipc)
#
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
#   (unless you are already familiar with Perl)
#
# Tests for the 'move [window|container] to mark' command
# Ticket: #1643
use i3test;

# In the following tests descriptions, we will always use the following names:
#  * 'S' for the source container which is going to be moved,
#  * 'M' for the marked target container to which 'S' will be moved.

my ($A, $B, $S, $M, $F, $source_ws, $target_ws, $ws);
my ($nodes, $focus);
my $__i3_scratch;
my $cmd_result;

my $_NET_WM_STATE_REMOVE = 0;
my $_NET_WM_STATE_ADD = 1;
my $_NET_WM_STATE_TOGGLE = 2;

sub set_urgency {
    my ($win, $urgent_flag) = @_;
    my $msg = pack "CCSLLLLLL",
        X11::XCB::CLIENT_MESSAGE, # response_type
        32, # format
        0, # sequence
        $win->id, # window
        $x->atom(name => '_NET_WM_STATE')->id, # message type
        ($urgent_flag ? $_NET_WM_STATE_ADD : $_NET_WM_STATE_REMOVE), # data32[0]
        $x->atom(name => '_NET_WM_STATE_DEMANDS_ATTENTION')->id, # data32[1]
        0, # data32[2]
        0, # data32[3]
        0; # data32[4]

    $x->send_event(0, $x->get_root_window(), X11::XCB::EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
}

###############################################################################
# Given 'M' and 'S' in a horizontal split, when 'S' is moved to 'M', then
# verify that nothing changed.
###############################################################################

$ws = fresh_workspace;
$M = open_window;
cmd 'mark target';
$S = open_window;

cmd 'move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($ws);
is(@{$nodes}, 2, 'there are two containers');
is($nodes->[0]->{window}, $M->{id}, 'M is left of S');
is($nodes->[1]->{window}, $S->{id}, 'S is right of M');

###############################################################################
# Given 'S' and 'M' in a horizontal split, when 'S' is moved to 'M', then
# both containers switch places.
###############################################################################

$ws = fresh_workspace;
$S = open_window;
$M = open_window;
cmd 'mark target';
cmd 'focus left';

cmd 'move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($ws);
is(@{$nodes}, 2, 'there are two containers');
is($nodes->[0]->{window}, $M->{id}, 'M is left of S');
is($nodes->[1]->{window}, $S->{id}, 'S is right of M');

###############################################################################
# Given 'S' and no container 'M' exists, when 'S' is moved to 'M', then
# the command is unsuccessful.
###############################################################################

$ws = fresh_workspace;
$S = open_window;

$cmd_result = cmd 'move container to mark absent';

is($cmd_result->[0]->{success}, 0, 'command was unsuccessful');

###############################################################################
# Given 'S' and 'M' on different workspaces, when 'S' is moved to 'M', then
# 'S' ends up on the same workspace as 'M'.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$M = open_window;
cmd 'mark target';

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($source_ws);
is(@{$nodes}, 0, 'source workspace is empty');

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 2, 'both containers are on the target workspace');
is($nodes->[0]->{window}, $M->{id}, 'M is left of S');
is($nodes->[1]->{window}, $S->{id}, 'S is right of M');

###############################################################################
# Given 'S' and 'M' on different workspaces and 'S' is urgent, when 'S' is
# moved to 'M', then the urgency flag is transferred to the target workspace.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;
$F = open_window;
$target_ws = fresh_workspace;
$M = open_window;
cmd 'mark target';
cmd 'workspace ' . $source_ws;
set_urgency($S, 1);

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

$source_ws = get_ws($source_ws);
$target_ws = get_ws($target_ws);
ok(!$source_ws->{urgent}, 'source workspace is no longer urgent');
ok($target_ws->{urgent}, 'target workspace is urgent');

###############################################################################
# Given 'S' and 'M' where 'M' is inside a tabbed container, when 'S' is moved
# to 'M', then 'S' ends up as a new tab.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;

# open tabbed container ['A' 'M' 'B']
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$M = open_window;
cmd 'mark target';
$B = open_window;

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');

$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 4, 'all four containers are on the target workspace');
is($nodes->[0]->{window}, $A->{id}, 'A is the first tab');
is($nodes->[1]->{window}, $M->{id}, 'M is the second tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');
is($nodes->[3]->{window}, $B->{id}, 'B is the fourth tab');

###############################################################################
# Given 'S' and 'M' where 'M' is a tabbed container where the currently focused
# tab is a nested layout, when 'S' is moved to 'M', then 'S' is a new tab
# within 'M'.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;

$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
cmd 'focus parent';
cmd 'mark target';
cmd 'focus child';
$B = open_window;
cmd 'split h';
$F = open_window;

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');

$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 3, 'there are three tabs');

is($nodes->[0]->{window}, $A->{id}, 'A is the first tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');

###############################################################################
# Given 'S' and 'M' where 'M' is inside a split container inside a tabbed
# container, when 'S' is moved to 'M', then 'S' ends up as a container
# within the same tab as 'M'.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;

# open tabbed container ['A'['B' 'M']]
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$B = open_window;
cmd 'split h';
$M = open_window;
cmd 'mark target';

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');

$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 2, 'there are two tabs');

$nodes = $nodes->[1]->{nodes};
is(@{$nodes}, 3, 'the tab with the marked children has three children');
is($nodes->[0]->{window}, $B->{id}, 'B is the first tab');
is($nodes->[1]->{window}, $M->{id}, 'M is the second tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');

###############################################################################
# Given 'S', 'A' and 'B' where 'A' and 'B' are inside the tabbed container 'M',
# when 'S' is moved to 'M', then 'S' ends up as a new tab in 'M'.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$B = open_window;
cmd 'focus parent';
cmd 'mark target';
cmd 'focus child';

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');

$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 3, 'there are three tabs');

is($nodes->[0]->{window}, $A->{id}, 'A is the first tab');
is($nodes->[1]->{window}, $B->{id}, 'B is the second tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');

###############################################################################
# Given 'S', 'A', 'F' and 'M', where 'M' is a workspace containing a tabbed
# container, when 'S' is moved to 'M', then 'S' does not end up as a tab, but
# rather as a new window next to the tabbed container.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$F = open_window;
$M = $target_ws;
cmd 'focus parent';
cmd 'focus parent';
cmd 'mark target';
cmd 'focus ' . $source_ws;

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 2, 'there is a tabbed container and a window');
is($nodes->[1]->{window}, $S->{id}, 'S is the second window');

###############################################################################
# Given 'S' and 'M' where 'S' is floating and 'M' on a different workspace,
# when 'S' is moved to 'M', then 'S' is a floating container on the same
# workspaces as 'M'.
###############################################################################

$source_ws = fresh_workspace;
$S = open_floating_window;
$target_ws = fresh_workspace;
$M = open_window;
cmd 'mark target';

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

is(@{get_ws($target_ws)->{floating_nodes}}, 1, 'target workspace has the container now');

###############################################################################
# Given 'S' and 'M' where 'M' is floating and on a different workspace,
# when 'S' is moved to 'M', then 'S' ends up as a tiling container on the
# same workspace as 'M'.
###############################################################################

$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$M = open_floating_window;
cmd 'mark target';

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'tiling container moved to the target workspace');

###############################################################################
# Given 'S' and 'M' where 'M' is inside a floating container but not its direct
# child, when 'S' is moved to 'M', i3 should not crash.
# See issue: #3402
###############################################################################

$target_ws = fresh_workspace;
$S = open_window;
open_window;
cmd 'splitv';
$M = open_window;
cmd 'mark target';
cmd 'focus parent, floating enable, focus child';

cmd '[id="' . $S->{id} . '"] move container to mark target';
does_i3_live;

# Note: this is not actively supported behavior.
$nodes = get_ws($target_ws)->{floating_nodes}->[0]->{nodes}->[0]->{nodes};
is(1, (grep { $_->{window} == $S->{id} } @{$nodes}), 'tiling container moved inside floating container');

###############################################################################
# Given 'S' and 'M' are the same container, when 'S' is moved to 'M', then
# the command is ignored.
###############################################################################

$ws = fresh_workspace;
$S = open_window;
$M = $S;
cmd 'mark target';

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

does_i3_live;

###############################################################################
# Given 'S' and 'M' where 'M' is a workspace and 'S' is on a different
# workspace, then 'S' ends up as a tiling container on 'M'.
# See issue: #2003
###############################################################################

fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$M = $target_ws;
cmd 'mark target';

cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;

does_i3_live;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'tiling container moved to the target workspace');

###############################################################################
# Given 'S' and 'M' where 'S' is a workspace and 'M' is a container on a
# different workspace, then all the contents of workspace 'S' end up in 'M's
# workspace.
###############################################################################

$S = fresh_workspace;
cmd 'mark S';
open_window;
open_window;
cmd 'splitv';
open_window;
open_floating_window;
$target_ws = fresh_workspace;
$M = open_window;
cmd 'mark target';

cmd '[con_mark=S] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 2, 'there is a window and a container with the contents of the original workspace');
is($nodes->[0]->{window}, $M->{id}, 'M remains the first window');
is(@{get_ws($target_ws)->{floating_nodes}}, 1, 'target workspace has the floating container');

###############################################################################
# Given 'S' and 'M', where 'S' is a container and 'M' is a container hidden in
# the scratchpad, then move 'S' to the scratchpad
###############################################################################

$ws = fresh_workspace;
$S = open_window;
cmd 'mark S';
$M = open_window;
cmd 'mark target';
cmd 'move container to scratchpad';

cmd '[con_mark=S] move container to mark target';
sync_with_i3;

($nodes, $focus) = get_ws_content($ws);
is(@{$nodes}, 0, 'there are no tiling windows on the workspace');
is(@{get_ws($ws)->{floating_nodes}}, 0, 'there are no floating containers on the workspace');

$__i3_scratch = get_ws('__i3_scratch');
is(@{$__i3_scratch->{nodes}}, 0, 'there are no tiling windows on the scratchpad workspace');
is(@{$__i3_scratch->{floating_nodes}}, 2, 'there are two floating containers in the scratchpad');

###############################################################################

done_testing;
