#![cfg(unix)]

mod common;
#[path = "stress/support.rs"]
mod support;

use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::path::Path;
use std::time::Duration;

use common::{
    assert_socket_directory_empty, assert_success, default_socket_path_in, pane_tty_paths, stderr,
    stdout, tty_has_input, unique_tmpdir, wait_for_no_child_processes, CliHarness,
    BINARY_OVERRIDE_ENV,
};
use rmux_client::{connect, ClientError};
use rmux_core::Session;
use rmux_proto::{
    CapturePaneRequest, KillSessionRequest, KillSessionResponse, LayoutName, NewSessionRequest,
    PaneTarget, Request, ResizePaneAdjustment, ResizePaneRequest, Response, SelectLayoutRequest,
    SelectLayoutResponse, SelectLayoutTarget, SendKeysRequest, SendKeysResponse,
    SplitWindowRequest, SplitWindowResponse, SplitWindowTarget, TerminalSize,
};
use rmux_server::{DaemonConfig, ServerDaemon};
use support::{
    assert_valid_non_overlapping_geometry, runtime, serialize_test_execution, single_new_tty,
    tty_sizes_by_index, unique_session_name, wait_for_tty_size,
};

const TERMINAL_COLS: u16 = 200;
const TERMINAL_ROWS: u16 = 50;
const PANE_COUNT: usize = 20;
const SECONDARY_PANE_COUNT: u32 = (PANE_COUNT - 1) as u32;
const MAIN_PANE_WIDTH: u16 = 34;
const REAP_TIMEOUT: Duration = Duration::from_secs(2);

fn wait_for_socket_directory_empty(
    socket_path: &Path,
    timeout: Duration,
) -> Result<(), Box<dyn Error>> {
    let deadline = std::time::Instant::now() + timeout;

    loop {
        if assert_socket_directory_empty(socket_path).is_ok() {
            return Ok(());
        }

        if std::time::Instant::now() >= deadline {
            assert_socket_directory_empty(socket_path)?;
        }

        std::thread::sleep(Duration::from_millis(20));
    }
}

#[test]
fn twenty_pane_layout_produces_valid_geometry_and_resizes_all_ptys() -> Result<(), Box<dyn Error>> {
    let _guard = serialize_test_execution();
    let runtime = runtime()?;
    let tmpdir = unique_tmpdir("stress-20pane");
    fs::create_dir_all(&tmpdir)?;
    let socket_path = default_socket_path_in(&tmpdir)?;
    let handle =
        runtime.block_on(ServerDaemon::new(DaemonConfig::new(socket_path.clone())).bind())?;
    let session_name = unique_session_name("stress-20pane");
    let baseline_ttys = pane_tty_paths()?;
    let mut connection = connect(&socket_path)?;
    let mut tty_paths = HashMap::new();
    let mut expected_session = Session::new(
        session_name.clone(),
        TerminalSize {
            cols: TERMINAL_COLS,
            rows: TERMINAL_ROWS,
        },
    );

    let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
        session_name: session_name.clone(),
        detached: true,
        size: Some(TerminalSize {
            cols: TERMINAL_COLS,
            rows: TERMINAL_ROWS,
        }),
        environment: None,
    }))?;
    assert!(matches!(created, Response::NewSession(_)));

    let after_create_ttys = pane_tty_paths()?;
    tty_paths.insert(0, single_new_tty(&baseline_ttys, &after_create_ttys)?);
    let mut previous_ttys = after_create_ttys;

    for expected_index in 1..PANE_COUNT as u32 {
        let split = connection.roundtrip(&Request::SplitWindow(SplitWindowRequest {
            target: SplitWindowTarget::Session(session_name.clone()),
            direction: rmux_proto::SplitDirection::Vertical,
            before: false,
            environment: None,
        }))?;
        assert_eq!(
            split,
            Response::SplitWindow(SplitWindowResponse {
                pane: PaneTarget::new(session_name.clone(), expected_index),
            })
        );
        assert_eq!(
            expected_session
                .split_active_pane_with_direction(rmux_proto::SplitDirection::Vertical)?,
            expected_index
        );

        let relayout = connection.roundtrip(&Request::SelectLayout(SelectLayoutRequest {
            target: SelectLayoutTarget::Session(session_name.clone()),
            layout: LayoutName::EvenHorizontal,
        }))?;
        assert_eq!(
            relayout,
            Response::SelectLayout(SelectLayoutResponse {
                layout: LayoutName::EvenHorizontal,
            })
        );
        expected_session.select_layout(LayoutName::EvenHorizontal);

        let current_ttys = pane_tty_paths()?;
        tty_paths.insert(
            expected_index,
            single_new_tty(&previous_ttys, &current_ttys)?,
        );
        previous_ttys = current_ttys;
    }

    assert_eq!(tty_paths.len(), PANE_COUNT);
    assert_eq!(previous_ttys.difference(&baseline_ttys).count(), PANE_COUNT);

    let pty_sizes_before_layout = tty_sizes_by_index(&tty_paths)?;

    let selected = connection.roundtrip(&Request::SelectLayout(SelectLayoutRequest {
        target: SelectLayoutTarget::Session(session_name.clone()),
        layout: LayoutName::MainVertical,
    }))?;
    assert_eq!(
        selected,
        Response::SelectLayout(SelectLayoutResponse {
            layout: LayoutName::MainVertical,
        })
    );

    let resized = connection.roundtrip(&Request::ResizePane(ResizePaneRequest {
        target: PaneTarget::new(session_name.clone(), 0),
        adjustment: ResizePaneAdjustment::AbsoluteWidth {
            columns: MAIN_PANE_WIDTH,
        },
    }))?;
    assert_eq!(
        resized,
        Response::ResizePane(rmux_proto::ResizePaneResponse {
            target: PaneTarget::new(session_name.clone(), 0),
            adjustment: ResizePaneAdjustment::AbsoluteWidth {
                columns: MAIN_PANE_WIDTH
            },
        })
    );

    expected_session.select_layout(LayoutName::MainVertical);
    expected_session.resize_pane(
        0,
        ResizePaneAdjustment::AbsoluteWidth {
            columns: MAIN_PANE_WIDTH,
        },
    )?;

    let panes = expected_session.window().panes();
    assert_eq!(panes.len(), PANE_COUNT);
    assert_valid_non_overlapping_geometry(
        panes,
        TerminalSize {
            cols: TERMINAL_COLS,
            rows: TERMINAL_ROWS,
        },
    );

    let secondary_panes: Vec<_> = panes.iter().skip(1).collect();
    let separators = SECONDARY_PANE_COUNT.saturating_sub(1);
    let usable_rows = u32::from(TERMINAL_ROWS).saturating_sub(separators);
    let secondary_rows = secondary_panes
        .iter()
        .map(|pane| u32::from(pane.geometry().rows()))
        .collect::<Vec<_>>();
    assert!(secondary_rows.iter().all(|rows| *rows > 0));
    assert_eq!(secondary_rows.iter().sum::<u32>(), usable_rows);

    for pane in panes {
        let original_size = pty_sizes_before_layout
            .get(&pane.index())
            .expect("all pane sizes should be captured before the layout change");
        let expected_size = TerminalSize {
            cols: pane.geometry().cols(),
            rows: pane.geometry().rows(),
        };
        let current_size = wait_for_tty_size(
            tty_paths
                .get(&pane.index())
                .expect("all pane tty paths should remain addressable"),
            expected_size,
        )?;

        assert_ne!(
            *original_size,
            current_size,
            "pane {} PTY size should change after the layout cycle",
            pane.index()
        );
        assert_eq!(
            current_size,
            expected_size,
            "pane {} PTY size should match the computed geometry",
            pane.index()
        );
    }

    let last_pane_index = (PANE_COUNT - 1) as u32;

    let first_marker = "stress_edge_zero_marker";
    let first_pane_write = connection.roundtrip(&Request::SendKeys(SendKeysRequest {
        target: PaneTarget::new(session_name.clone(), 0),
        keys: vec![format!("printf '{first_marker}\\n'"), "Enter".to_owned()],
    }))?;
    assert_eq!(
        first_pane_write,
        Response::SendKeys(SendKeysResponse { key_count: 2 })
    );
    let first_capture = wait_for_pane_capture(
        &mut connection,
        PaneTarget::new(session_name.clone(), 0),
        first_marker,
    )?;
    assert!(first_capture.contains(first_marker));

    let last_marker = "stress_edge_last_marker";
    let last_pane_write = connection.roundtrip(&Request::SendKeys(SendKeysRequest {
        target: PaneTarget::new(session_name.clone(), last_pane_index),
        keys: vec![format!("printf '{last_marker}\\n'"), "Enter".to_owned()],
    }))?;
    assert_eq!(
        last_pane_write,
        Response::SendKeys(SendKeysResponse { key_count: 2 })
    );
    let last_capture = wait_for_pane_capture(
        &mut connection,
        PaneTarget::new(session_name.clone(), last_pane_index),
        last_marker,
    )?;
    assert!(last_capture.contains(last_marker));

    drop(connection);
    runtime.block_on(handle.shutdown())?;
    fs::remove_dir_all(tmpdir)?;
    Ok(())
}

#[test]
fn session_name_rewrites_preserve_server_state() -> Result<(), Box<dyn Error>> {
    let _guard = serialize_test_execution();
    let cases = [
        ("stress-names-empty", "", None, false),
        ("stress-names-colon", "a:b", Some("a_b"), true),
        ("stress-names-dot", "a..b", Some("a__b"), true),
    ];

    for (label, source_name, expected_name, should_succeed) in cases {
        let harness = CliHarness::new(label)?;
        let _cleanup = harness.auto_start_cleanup()?;
        let output = harness.run_with(&["new-session", "-d", "-s", source_name], |command| {
            command.env(BINARY_OVERRIDE_ENV, harness.launcher_path());
        })?;

        if should_succeed {
            assert_success(&output);
            assert!(
                stdout(&output).is_empty(),
                "stdout should stay empty for {label}"
            );
            assert!(
                stderr(&output).is_empty(),
                "stderr should stay empty for {label}"
            );
            assert!(
                harness.pid_path().exists(),
                "sanitized session names must launch the auto-start daemon for {label}"
            );
            assert!(
                harness.socket_path().exists(),
                "sanitized session names must create a socket for {label}"
            );
            assert_success(&harness.run(&["has-session", "-t", expected_name.expect("name")])?);
        } else {
            assert_eq!(output.status.code(), Some(1));
            assert!(
                stdout(&output).is_empty(),
                "stdout should stay empty for {label}"
            );
            assert!(
                stderr(&output).contains("invalid session name"),
                "stderr should report an invalid session name for {label}, got: {}",
                stderr(&output)
            );
            assert!(
                !harness.pid_path().exists(),
                "invalid session names must not launch the auto-start daemon for {label}"
            );
            assert!(
                !harness.socket_path().exists(),
                "invalid session names must not create a socket for {label}"
            );
        }
    }

    let live_harness = CliHarness::new("stress-names-live")?;
    let mut live_daemon = live_harness.start_hidden_daemon()?;
    let stable_session = unique_session_name("stress-names-live");

    assert_success(&live_harness.run(&["new-session", "-d", "-s", stable_session.as_str()])?);

    for (_label, source_name, expected_name, should_succeed) in cases {
        let live_output = live_harness.run(&["new-session", "-d", "-s", source_name])?;
        if should_succeed {
            assert_success(&live_output);
            assert_success(&live_harness.run(&[
                "has-session",
                "-t",
                expected_name.expect("name"),
            ])?);
        } else {
            assert_eq!(live_output.status.code(), Some(1));
            assert!(stderr(&live_output).contains("invalid session name"));
        }
        assert_success(&live_harness.run(&["has-session", "-t", stable_session.as_str()])?);
    }

    common::terminate_child(live_daemon.child_mut())?;
    Ok(())
}

fn wait_for_pane_capture(
    connection: &mut rmux_client::Connection,
    target: PaneTarget,
    marker: &str,
) -> Result<String, Box<dyn Error>> {
    for _ in 0..100 {
        // The edge panes in this stress layout can be one row tall, so the
        // visible screen may roll past the marker immediately after the shell
        // repaints. Capture the full transcript to verify delivery instead.
        let response = connection.roundtrip(&Request::CapturePane(CapturePaneRequest {
            target: target.clone(),
            start: None,
            end: None,
            print: true,
            buffer_name: None,
            alternate: false,
            escape_ansi: false,
            escape_sequences: false,
            join_wrapped: false,
            use_mode_screen: false,
            preserve_trailing_spaces: false,
            do_not_trim_spaces: false,
            pending_input: false,
            quiet: false,
            start_is_absolute: true,
            end_is_absolute: true,
        }))?;
        let captured = std::str::from_utf8(
            response
                .command_output()
                .expect("capture-pane -p should return command output")
                .stdout(),
        )?
        .to_owned();
        if captured.contains(marker) {
            return Ok(captured);
        }
        std::thread::sleep(Duration::from_millis(20));
    }

    Err(format!("capture-pane -p never surfaced marker {marker}").into())
}

#[test]
fn empty_send_keys_succeeds_through_the_cli() -> Result<(), Box<dyn Error>> {
    let _guard = serialize_test_execution();
    let harness = CliHarness::new("stress-sendkeys-empty")?;
    let runtime = runtime()?;
    let baseline_ttys = pane_tty_paths()?;
    let handle = runtime.block_on(
        ServerDaemon::new(DaemonConfig::new(harness.socket_path().to_path_buf())).bind(),
    )?;
    let session_name = unique_session_name("stress-sendkeys");
    let target = format!("{session_name}:0.0");

    assert_success(&harness.run(&["new-session", "-d", "-s", session_name.as_str()])?);
    let pane_zero_tty = single_new_tty(&baseline_ttys, &pane_tty_paths()?)?;

    let output = harness.run(&["send-keys", "-t", target.as_str()])?;
    assert_success(&output);
    assert!(
        !tty_has_input(&pane_zero_tty, Duration::from_millis(150))?,
        "empty send-keys must not write bytes into pane 0"
    );
    assert_success(&harness.run(&["has-session", "-t", session_name.as_str()])?);

    runtime.block_on(handle.shutdown())?;
    Ok(())
}

#[test]
fn runtime_resolved_targets_report_absent_server_without_autostart() -> Result<(), Box<dyn Error>> {
    let _guard = serialize_test_execution();

    for (label, target) in [
        ("stress-target-pane-window", "sess:x.0"),
        ("stress-target-trailing-colon", "sess:"),
        ("stress-target-trailing-dot", "sess:0."),
        ("stress-target-overflow-pane", "sess:0.999999999999"),
        ("stress-target-negative-pane", "sess:0.-1"),
        ("stress-target-empty-session", ":0"),
    ] {
        let harness = CliHarness::new(label)?;
        let output = harness.run(&["select-pane", "-t", target])?;

        assert_eq!(output.status.code(), Some(1), "{label} should exit 1");
        assert!(
            stdout(&output).is_empty(),
            "{label} should not print stdout"
        );
        assert!(stderr(&output).contains("no server running on"));
        assert!(
            !harness.socket_path().exists(),
            "{label} must fail without auto-starting a server"
        );
    }

    Ok(())
}

#[test]
fn missing_window_targets_report_explicit_errors() -> Result<(), Box<dyn Error>> {
    let _guard = serialize_test_execution();
    let harness = CliHarness::new("stress-missing-window")?;
    let session_name = unique_session_name("stress-missing-window");

    assert_success(&harness.run(&["new-session", "-d", "-s", session_name.as_str()])?);
    let output = harness.run(&["select-pane", "-t", &format!("{session_name}:5.0")])?;

    assert_eq!(output.status.code(), Some(1));
    assert!(stdout(&output).is_empty(), "stdout should be empty");
    assert!(
        stderr(&output).contains("can't find window: 5"),
        "stderr should report the missing window, got: {}",
        stderr(&output)
    );

    Ok(())
}

#[test]
fn multi_pane_kill_reaps_children_under_the_hidden_daemon() -> Result<(), Box<dyn Error>> {
    let _guard = serialize_test_execution();
    let harness = CliHarness::new("stress-zombies")?;
    let mut daemon = harness.start_hidden_daemon()?;
    let session_name = unique_session_name("stress-zombies");

    assert_success(&harness.run(&["new-session", "-d", "-s", session_name.as_str()])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", session_name.as_str()])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", session_name.as_str()])?);
    assert_success(&harness.run(&["kill-session", "-t", session_name.as_str()])?);

    wait_for_no_child_processes(daemon.pid(), REAP_TIMEOUT)?;
    wait_for_socket_directory_empty(harness.socket_path(), REAP_TIMEOUT)?;

    common::terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn shutdown_removes_socket_files_after_all_sessions_are_killed() -> Result<(), Box<dyn Error>> {
    let _guard = serialize_test_execution();
    let runtime = runtime()?;
    let tmpdir = unique_tmpdir("stress-cleanup");
    fs::create_dir_all(&tmpdir)?;
    let socket_path = default_socket_path_in(&tmpdir)?;
    let handle =
        runtime.block_on(ServerDaemon::new(DaemonConfig::new(socket_path.clone())).bind())?;
    let session_name = unique_session_name("stress-cleanup");
    let mut connection = connect(&socket_path)?;

    let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
        session_name: session_name.clone(),
        detached: true,
        size: Some(TerminalSize { cols: 80, rows: 24 }),
        environment: None,
    }))?;
    assert!(matches!(created, Response::NewSession(_)));

    for _ in 0..2 {
        let split = connection.roundtrip(&Request::SplitWindow(SplitWindowRequest {
            target: SplitWindowTarget::Session(session_name.clone()),
            direction: rmux_proto::SplitDirection::Vertical,
            before: false,
            environment: None,
        }))?;
        assert!(matches!(split, Response::SplitWindow(_)));
    }

    let killed = connection.roundtrip(&Request::KillSession(KillSessionRequest {
        target: session_name.clone(),
        kill_all_except_target: false,
        clear_alerts: false,
    }));
    match killed {
        Ok(killed) => assert_eq!(
            killed,
            Response::KillSession(KillSessionResponse { existed: true })
        ),
        Err(ClientError::UnexpectedEof) => {}
        Err(error) => return Err(Box::new(error)),
    }
    drop(connection);

    runtime.block_on(handle.shutdown())?;
    assert_socket_directory_empty(&socket_path)?;
    fs::remove_dir_all(tmpdir)?;
    Ok(())
}
