/*
 *
 * NOTE: These tests are very heavy and are used as smoke tests just to verify the app is working
 * end-to-end. Avoid adding new ones, preferring instead to use the zellij-integration-tests module
 * it tests the app as a whole, only mocking the OS interaction parts
 *
*/
#![allow(unused)]

use insta::assert_snapshot;
use zellij_utils::{
    pane_size::Size,
    position::{Column, Line, Position},
};

use rand::RngExt;
use regex::Regex;

use std::fmt::Write;
use std::path::Path;

use super::remote_runner::{RemoteRunner, RemoteTerminal, Step};

pub const QUIT: [u8; 1] = [17]; // ctrl-q
pub const ESC: [u8; 1] = [27];
pub const ENTER: [u8; 1] = [13]; // '\r'
pub const SPACE: [u8; 1] = [32];
pub const LOCK_MODE: [u8; 1] = [7]; // ctrl-g

pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l

pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p
pub const TMUX_MODE: [u8; 1] = [2]; // ctrl-b
pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n
pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p
pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d
pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r
pub const SPLIT_RIGHT_IN_TMUX_MODE: [u8; 1] = [37]; // %
pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f
pub const TOGGLE_FLOATING_PANES: [u8; 1] = [119]; // w
pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x
pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [106]; // j
pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [107]; // k
pub const MOVE_FOCUS_LEFT_IN_PANE_MODE: [u8; 1] = [104]; // h
pub const MOVE_FOCUS_RIGHT_IN_PANE_MODE: [u8; 1] = [108]; // l
pub const RENAME_PANE_MODE: [u8; 1] = [99]; // c

pub const SCROLL_MODE: [u8; 1] = [19]; // ctrl-s
pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [107]; // k
pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [106]; // j
pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = [2]; // ctrl-b
pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = [6]; // ctrl-f
pub const EDIT_SCROLLBACK: [u8; 1] = [101]; // e

pub const RESIZE_MODE: [u8; 1] = [14]; // ctrl-n
pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [106]; // j
pub const RESIZE_UP_IN_RESIZE_MODE: [u8; 1] = [107]; // k
pub const RESIZE_LEFT_IN_RESIZE_MODE: [u8; 1] = [104]; // h
pub const RESIZE_RIGHT_IN_RESIZE_MODE: [u8; 1] = [108]; // l

pub const TAB_MODE: [u8; 1] = [20]; // ctrl-t
pub const NEW_TAB_IN_TAB_MODE: [u8; 1] = [110]; // n
pub const SWITCH_NEXT_TAB_IN_TAB_MODE: [u8; 1] = [108]; // l
pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [104]; // h
pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x
pub const RENAME_TAB_MODE: [u8; 1] = [114]; // r

pub const MOVE_TAB_LEFT: [u8; 2] = [27, 105]; // Alt + i
pub const MOVE_TAB_RIGHT: [u8; 2] = [27, 111]; // Alt + o

pub const SESSION_MODE: [u8; 1] = [15]; // ctrl-o
pub const DETACH_IN_SESSION_MODE: [u8; 1] = [100]; // d

pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[200~
pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201
pub const SLEEP: [u8; 0] = [];

pub const SECOND_TAB_CONTENT: [u8; 14] =
    [84, 97, 98, 32, 35, 50, 32, 99, 111, 110, 116, 101, 110, 116]; // Tab #2 content

// what we do here is adjust snapshots for various race conditions that should hopefully be
// temporary until we can fix them - when adding stuff here, please add a detailed comment
// explaining the race condition and what needs to be done to solve it
fn account_for_races_in_snapshot(snapshot: String) -> String {
    // these replacements need to be done because plugins set themselves as "unselectable" at runtime
    // when they are loaded - since they are loaded asynchronously, sometimes the "BASE" indication
    // (which should only happen if there's more than one selectable pane) is rendered and
    // sometimes it isn't - this removes it entirely
    //
    // to fix this, we should set plugins as unselectable in the layout (before they are loaded),
    // once that happens, we should be able to remove this hack (and adjust the snapshots for the
    // trailing spaces that we had to get rid of here)
    let base_replace = Regex::new(r"Alt <\[\]>  BASE \s*\n").unwrap();
    let base_replace_tmux_mode_1 = Regex::new(r"Alt \[\|SPACE\|Alt \]  BASE \s*\n").unwrap();
    let base_replace_tmux_mode_2 = Regex::new(r"Alt \[\|Alt \]\|SPACE  BASE \s*\n").unwrap();
    let eol_arrow_replace = Regex::new(r"\s*\n").unwrap();
    let snapshot = base_replace.replace_all(&snapshot, "\n").to_string();
    let snapshot = base_replace_tmux_mode_1
        .replace_all(&snapshot, "\n")
        .to_string();
    let snapshot = base_replace_tmux_mode_2
        .replace_all(&snapshot, "\n")
        .to_string();
    let snapshot = eol_arrow_replace.replace_all(&snapshot, "\n").to_string();

    snapshot
}

// All the E2E tests are marked as "ignored" so that they can be run separately from the normal
// tests

#[test]
#[ignore]
pub fn starts_with_one_terminal() {
    let fake_win_size = Size {
        cols: 120,
        rows: 24,
    };
    let mut test_attempts = 10;
    let last_snapshot = loop {
        RemoteRunner::kill_running_sessions(fake_win_size);
        let mut runner = RemoteRunner::new(fake_win_size);
        let last_snapshot = runner.take_snapshot_after(Step {
            name: "Wait for app to load",
            instruction: |remote_terminal: RemoteTerminal| -> bool {
                let mut step_is_complete = false;
                if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 1)
                {
                    step_is_complete = true;
                }
                step_is_complete
            },
        });
        if runner.test_timed_out && test_attempts > 0 {
            test_attempts -= 1;
            continue;
        } else {
            break last_snapshot;
        }
    };

    let last_snapshot = account_for_races_in_snapshot(last_snapshot);
    assert_snapshot!(last_snapshot);
}

#[test]
#[ignore]
pub fn typing_exit_closes_pane() {
    let fake_win_size = Size {
        cols: 120,
        rows: 24,
    };
    let mut test_attempts = 10;
    let last_snapshot = loop {
        RemoteRunner::kill_running_sessions(fake_win_size);
        let mut runner = RemoteRunner::new(fake_win_size)
            .add_step(Step {
                name: "Split pane to the right",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    if remote_terminal.status_bar_appears()
                        && remote_terminal.cursor_position_is(2, 1)
                    {
                        remote_terminal.send_key(&PANE_MODE);
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
                        step_is_complete = true;
                    }
                    step_is_complete
                },
            })
            .add_step(Step {
                name: "Type exit",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    if remote_terminal.cursor_position_is(62, 2)
                        && remote_terminal.status_bar_appears()
                    {
                        remote_terminal.send_key("e".as_bytes());
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key("x".as_bytes());
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key("i".as_bytes());
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key("t".as_bytes());
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key("\n".as_bytes());
                        step_is_complete = true;
                    }
                    step_is_complete
                },
            });
        runner.run_all_steps();
        let last_snapshot = runner.take_snapshot_after(Step {
            name: "Wait for pane to close",
            instruction: |remote_terminal: RemoteTerminal| -> bool {
                let mut step_is_complete = false;
                if remote_terminal.cursor_position_is(2, 1) && remote_terminal.status_bar_appears()
                {
                    // cursor is in the original pane
                    step_is_complete = true;
                }
                step_is_complete
            },
        });
        if runner.test_timed_out && test_attempts > 0 {
            test_attempts -= 1;
            continue;
        } else {
            break last_snapshot;
        }
    };
    let last_snapshot = account_for_races_in_snapshot(last_snapshot);
    assert_snapshot!(last_snapshot);
}

#[test]
#[ignore]
pub fn resize_terminal_window() {
    // this checks the resizing of the whole terminal window (reaction to SIGWINCH) and not just one pane
    let fake_win_size = Size {
        cols: 120,
        rows: 24,
    };
    let mut test_attempts = 10;
    let last_snapshot = loop {
        RemoteRunner::kill_running_sessions(fake_win_size);
        let mut runner = RemoteRunner::new(fake_win_size)
            .add_step(Step {
                name: "Split pane to the right",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    if remote_terminal.status_bar_appears()
                        && remote_terminal.cursor_position_is(2, 1)
                    {
                        remote_terminal.send_key(&PANE_MODE);
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
                        step_is_complete = true;
                    }
                    step_is_complete
                },
            })
            .add_step(Step {
                name: "Change terminal window size",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    if remote_terminal.cursor_position_is(62, 2)
                        && remote_terminal.status_bar_appears()
                    {
                        // new pane has been opened and focused
                        remote_terminal.change_size(100, 24);
                        step_is_complete = true;
                    }
                    step_is_complete
                },
            });
        runner.run_all_steps();
        let last_snapshot = runner.take_snapshot_after(Step {
            name: "wait for terminal to be resized and app to be re-rendered",
            instruction: |remote_terminal: RemoteTerminal| -> bool {
                let mut step_is_complete = false;
                if remote_terminal.cursor_position_is(52, 2) && remote_terminal.ctrl_plus_appears()
                {
                    // size has been changed
                    step_is_complete = true;
                }
                step_is_complete
            },
        });
        if runner.test_timed_out && test_attempts > 0 {
            test_attempts -= 1;
            continue;
        } else {
            break last_snapshot;
        }
    };
    let last_snapshot = account_for_races_in_snapshot(last_snapshot);
    assert_snapshot!(last_snapshot);
}

#[test]
#[ignore]
pub fn quit_and_resurrect_session() {
    let fake_win_size = Size {
        cols: 120,
        rows: 24,
    };
    let mut test_attempts = 10;
    let layout_name = "layout_for_resurrection.kdl";
    let last_snapshot = loop {
        RemoteRunner::kill_running_sessions(fake_win_size);
        let mut runner = RemoteRunner::new_mirrored_session_with_layout(fake_win_size, layout_name)
            .add_step(Step {
                name: "Wait for session to be serialized",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    if remote_terminal.snapshot_contains("Waiting to run: top") {
                        std::thread::sleep(std::time::Duration::from_millis(5000)); // wait for
                                                                                    // serialization
                        remote_terminal.send_key(&QUIT);
                        step_is_complete = true;
                    }
                    step_is_complete
                },
            })
            .add_step(Step {
                name: "Resurrect session by attaching",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    if remote_terminal.snapshot_contains("Bye from Zellij!") {
                        remote_terminal.attach_to_original_session();
                        step_is_complete = true;
                    }
                    step_is_complete
                },
            });
        runner.run_all_steps();
        let last_snapshot = runner.take_snapshot_after(Step {
            name: "Wait for session to be resurrected",
            instruction: |remote_terminal: RemoteTerminal| -> bool {
                remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)")
                    && remote_terminal.status_bar_appears()
                    && remote_terminal.tab_bar_appears()
            },
        });
        if runner.test_timed_out && test_attempts > 0 {
            test_attempts -= 1;
            continue;
        } else {
            break last_snapshot;
        }
    };
    let last_snapshot = account_for_races_in_snapshot(last_snapshot);
    assert_snapshot!(last_snapshot);
}

#[test]
#[ignore]
pub fn send_blocking_command_through_the_cli() {
    // here we test the following flow:
    // - send a blocking command through the cli with --blocking --floating --close-on-exit
    // - the command sleeps for 2 seconds (longer than the default 1s timeout) then exits with status 42
    // - verify that the CLI blocks for the full duration (we check after 2+ seconds)
    // - verify that the floating pane appears while running and disappears after completion
    // - verify that the exit status is properly propagated
    let fake_win_size = Size {
        cols: 150,
        rows: 24,
    };
    let mut test_attempts = 10;
    let last_snapshot = loop {
        RemoteRunner::kill_running_sessions(fake_win_size);
        let mut runner = RemoteRunner::new(fake_win_size)
            .add_step(Step {
                name: "Run blocking command through the cli",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    if remote_terminal.status_bar_appears()
                        && remote_terminal.cursor_position_is(2, 1)
                    {
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal
                            .send_blocking_command_through_the_cli("bash -c 'sleep 2 && exit 42'");
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key(&ENTER);
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        step_is_complete = true;
                    }
                    step_is_complete
                },
            })
            .add_step(Step {
                name: "Wait for floating pane to appear",
                instruction: |remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    // The floating pane should appear with the running command
                    if remote_terminal.snapshot_contains("PIN [ ]") {
                        std::thread::sleep(std::time::Duration::from_millis(2000)); // wait for
                                                                                    // command to
                                                                                    // end
                        step_is_complete = true
                    }
                    step_is_complete
                },
            })
            .add_step(Step {
                name: "Wait for command to complete and verify exit status",
                instruction: |mut remote_terminal: RemoteTerminal| -> bool {
                    let mut step_is_complete = false;
                    // After 2+ seconds, the command should complete and the floating pane should close
                    // Wait until the floating pane is gone AND the shell prompt is back before
                    // asking for $?, otherwise we can race the blocking CLI process itself
                    // returning to the shell.
                    if !remote_terminal.snapshot_contains("PIN [ ]")
                        && remote_terminal.snapshot_contains("$ \u{2588}")
                        && remote_terminal.status_bar_appears()
                    {
                        remote_terminal.send_key("echo $?".as_bytes());
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        remote_terminal.send_key(&ENTER);
                        std::thread::sleep(std::time::Duration::from_millis(100));
                        step_is_complete = true
                    }
                    step_is_complete
                },
            });
        runner.run_all_steps();

        let last_snapshot = runner.take_snapshot_after(Step {
            name: "Verify CLI returned with proper exit status after command completed",
            instruction: |remote_terminal: RemoteTerminal| -> bool {
                let mut step_is_complete = false;
                // wait until echo $? is visible, the exit status rendered, and the cursor is back
                // at a blank prompt, which means the shell command actually executed
                if remote_terminal.snapshot_contains("echo $?")
                    && remote_terminal.snapshot_contains("42")
                    && remote_terminal.snapshot_contains("$ \u{2588}")
                    && remote_terminal.status_bar_appears()
                {
                    step_is_complete = true
                }
                step_is_complete
            },
        });

        if runner.test_timed_out && test_attempts > 0 {
            test_attempts -= 1;
            continue;
        } else {
            break last_snapshot;
        }
    };
    let last_snapshot = account_for_races_in_snapshot(last_snapshot);
    assert_snapshot!(last_snapshot);
}
