// Integration coverage for the client window layout contract.
//
// Scope: exercise `layout::apply` end-to-end against a fake `swaymsg` binary.
// Targets: `client/src/layout.rs`.
// Why: this keeps layout policy coverage centralized in `tests` while
// avoiding source-LOC ratchet growth in the client crate.

use lesavka_client::layout::{Layout, apply};
use serial_test::serial;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use temp_env::with_var;
use tempfile::tempdir;

fn install_sway_stub(dir: &std::path::Path) -> std::path::PathBuf {
    let sway_path = dir.join("swaymsg");
    fs::write(
        &sway_path,
        r#"#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "-t" && "${2:-}" == "get_outputs" && "${3:-}" == "-r" ]]; then
  printf '%s\n' "${LESAVKA_TEST_SWAY_OUTPUTS:-[]}"
  exit "${LESAVKA_TEST_SWAY_GET_OUTPUTS_EXIT:-0}"
fi
echo "$*" >> "${LESAVKA_TEST_SWAY_LOG}"
exit "${LESAVKA_TEST_SWAY_PLACE_EXIT:-0}"
"#,
    )
    .expect("write swaymsg stub");
    let mut perms = fs::metadata(&sway_path)
        .expect("read swaymsg stub metadata")
        .permissions();
    perms.set_mode(0o755);
    fs::set_permissions(&sway_path, perms).expect("mark swaymsg stub executable");
    sway_path
}

fn run_apply_with_outputs(layout: Layout, outputs_json: &str) -> Option<String> {
    let dir = tempdir().expect("create temp dir");
    install_sway_stub(dir.path());
    let log_path = dir.path().join("sway.log");
    let old_path = std::env::var("PATH").unwrap_or_default();
    let new_path = format!("{}:{old_path}", dir.path().display());
    let log_value = log_path.to_string_lossy().to_string();

    with_var("PATH", Some(new_path.as_str()), || {
        with_var("LESAVKA_TEST_SWAY_LOG", Some(log_value.as_str()), || {
            with_var("LESAVKA_TEST_SWAY_OUTPUTS", Some(outputs_json), || {
                apply(layout);
            });
        });
    });

    fs::read_to_string(log_path).ok()
}

fn run_apply_without_swaymsg(layout: Layout) {
    with_var(
        "PATH",
        Some("/definitely-missing-lesavka-test-path"),
        || {
            apply(layout);
        },
    );
}

#[test]
#[serial]
fn side_by_side_places_each_eye_on_half_of_focused_output() {
    let outputs = r#"[{"focused":true,"rect":{"x":10,"y":20,"width":1920,"height":1080}}]"#;
    let log = run_apply_with_outputs(Layout::SideBySide, outputs).expect("read command log");
    assert!(log.contains(r#"[title="^Lesavka-eye-0$"] resize set 960 1080; move position 10 20"#));
    assert!(log.contains(r#"[title="^Lesavka-eye-1$"] resize set 960 1080; move position 970 20"#));
}

#[test]
#[serial]
fn full_layout_modes_hide_the_opposite_eye_offscreen() {
    let outputs = r#"[{"focused":true,"rect":{"x":100,"y":50,"width":640,"height":360}}]"#;
    let left = run_apply_with_outputs(Layout::FullLeft, outputs).expect("left layout log");
    assert!(left.contains(r#"[title="^Lesavka-eye-0$"] resize set 640 360; move position 100 50"#));
    assert!(left.contains(r#"[title="^Lesavka-eye-1$"] resize set 1 1; move position 1380 770"#));

    let right = run_apply_with_outputs(Layout::FullRight, outputs).expect("right layout log");
    assert!(
        right.contains(r#"[title="^Lesavka-eye-1$"] resize set 640 360; move position 100 50"#)
    );
    assert!(right.contains(r#"[title="^Lesavka-eye-0$"] resize set 1 1; move position 1380 770"#));
}

#[test]
#[serial]
fn apply_skips_window_commands_when_outputs_payload_is_invalid() {
    let log = run_apply_with_outputs(Layout::SideBySide, "{not-json").unwrap_or_default();
    assert!(
        log.trim().is_empty(),
        "expected no placement calls, got: {log}"
    );
}

#[test]
#[serial]
fn apply_skips_window_commands_when_no_output_is_focused() {
    let outputs = r#"[{"focused":false,"rect":{"x":0,"y":0,"width":1920,"height":1080}}]"#;
    let log = run_apply_with_outputs(Layout::SideBySide, outputs).unwrap_or_default();
    assert!(
        log.trim().is_empty(),
        "expected no placement calls, got: {log}"
    );
}

#[test]
#[serial]
fn apply_skips_window_commands_when_outputs_shape_is_not_an_array() {
    let log = run_apply_with_outputs(Layout::SideBySide, r#"{"focused":true}"#).unwrap_or_default();
    assert!(
        log.trim().is_empty(),
        "expected no placement calls, got: {log}"
    );
}

#[test]
#[serial]
fn apply_tolerates_nonzero_window_placement_exit_status() {
    let dir = tempdir().expect("create temp dir");
    install_sway_stub(dir.path());
    let log_path = dir.path().join("sway.log");
    let old_path = std::env::var("PATH").unwrap_or_default();
    let new_path = format!("{}:{old_path}", dir.path().display());
    let outputs = r#"[{"focused":true,"rect":{"x":0,"y":0,"width":640,"height":360}}]"#;
    let log_value = log_path.to_string_lossy().to_string();

    with_var("PATH", Some(new_path.as_str()), || {
        with_var("LESAVKA_TEST_SWAY_LOG", Some(log_value.as_str()), || {
            with_var("LESAVKA_TEST_SWAY_OUTPUTS", Some(outputs), || {
                with_var("LESAVKA_TEST_SWAY_PLACE_EXIT", Some("2"), || {
                    apply(Layout::SideBySide);
                });
            });
        });
    });

    let log = fs::read_to_string(log_path).expect("read command log");
    assert!(log.contains(r#"[title="^Lesavka-eye-0$"]"#));
    assert!(log.contains(r#"[title="^Lesavka-eye-1$"]"#));
}

#[test]
#[serial]
fn apply_handles_missing_sway_binary_without_panicking() {
    run_apply_without_swaymsg(Layout::SideBySide);
}
