import os
import json
import subprocess
import sys
import pytest

from lib_py.layout import (
    compute_2xk_layout,
    LayoutDecision,
)


def test_empty_or_malformed_json_fallback():
    d = compute_2xk_layout({}, default_anchor_id="pane-123")
    assert d.target_pane_id == "pane-123"
    assert d.direction == "right"
    assert not d.is_overflow


def test_1_pane_split_right():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 120, "height": 80}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.target_pane_id == "p1"
    assert d.direction == "right"
    assert not d.is_overflow
    assert d.reason == "new_column_right"


def test_1_pane_height_constrained_splits_right():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 160, "height": 30}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.target_pane_id == "p1"
    assert d.direction == "right"
    assert not d.is_overflow


def test_1_pane_overflow():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 30}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.target_pane_id == "p1"
    assert d.direction == "overflow"
    assert d.is_overflow


def test_2_panes_fill_left_column_down():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 80}},
                {"pane_id": "p2", "rect": {"x": 80, "y": 0, "width": 80, "height": 80}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.target_pane_id == "p1"
    assert d.direction == "down"
    assert not d.is_overflow
    assert d.reason == "fill_column"


def test_3_panes_to_4_panes_fill_singleton():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}},
                {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}},
                {"pane_id": "p3", "rect": {"x": 80, "y": 0, "width": 80, "height": 80}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.target_pane_id == "p3"
    assert d.direction == "down"
    assert not d.is_overflow
    assert d.reason == "fill_column"


def test_4_panes_to_5_panes_overflows_at_capacity():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 120, "height": 40}},
                {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 120, "height": 40}},
                {"pane_id": "p3", "rect": {"x": 120, "y": 0, "width": 120, "height": 40}},
                {"pane_id": "p4", "rect": {"x": 120, "y": 40, "width": 120, "height": 40}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.direction == "overflow"
    assert d.is_overflow
    assert d.reason == "grid_capacity_reached"


def test_4_panes_overflow_when_width_constrained():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 60, "height": 40}},
                {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 60, "height": 40}},
                {"pane_id": "p3", "rect": {"x": 60, "y": 0, "width": 60, "height": 40}},
                {"pane_id": "p4", "rect": {"x": 60, "y": 40, "width": 60, "height": 40}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.direction == "overflow"
    assert d.is_overflow


def test_max_columns_limit():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 40}},
                {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 100, "height": 40}},
                {"pane_id": "p3", "rect": {"x": 100, "y": 0, "width": 100, "height": 40}},
                {"pane_id": "p4", "rect": {"x": 100, "y": 40, "width": 100, "height": 40}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=30, min_rows=20, max_columns=2)
    assert d.direction == "overflow"
    assert d.is_overflow


def test_headless_0x0_transitions():
    p1 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}]}}
    assert compute_2xk_layout(p1).direction == "right"

    p2 = {"result": {"panes": [
        {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}},
        {"pane_id": "p2", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}
    ]}}
    d2 = compute_2xk_layout(p2)
    assert d2.direction == "down"
    assert d2.target_pane_id == "p1"

    p3 = {"result": {"panes": [
        {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}},
        {"pane_id": "p2", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}},
        {"pane_id": "p3", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}
    ]}}
    d3 = compute_2xk_layout(p3)
    assert d3.direction == "down"
    assert d3.target_pane_id == "p2"

    p4 = {"result": {"panes": [
        {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}},
        {"pane_id": "p2", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}},
        {"pane_id": "p3", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}},
        {"pane_id": "p4", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}
    ]}}
    d4 = compute_2xk_layout(p4)
    assert d4.direction == "overflow"
    assert d4.is_overflow


def test_real_herdr_080_nested_layout_format():
    payload = {
        "result": {
            "layout": {
                "area": {"height": 78, "width": 120, "x": 26, "y": 1},
                "focused_pane_id": "wK:p1",
                "panes": [
                    {"pane_id": "wK:p1", "rect": {"height": 39, "width": 60, "x": 26, "y": 1, "focused": True}},
                    {"pane_id": "wK:p2", "rect": {"height": 39, "width": 60, "x": 26, "y": 40, "focused": False}},
                    {"pane_id": "wK:p3", "rect": {"height": 78, "width": 60, "x": 86, "y": 1, "focused": False}}
                ],
                "splits": [{"direction": "right", "id": "split_0_root", "ratio": 0.5}],
                "workspace_id": "wK",
                "tab_id": "wK:t1",
                "zoomed": False
            },
            "type": "pane_layout"
        }
    }
    d = compute_2xk_layout(payload, min_cols=30, min_rows=20)
    assert d.target_pane_id == "wK:p3"
    assert d.direction == "down"


def test_cli_invocation_pipe():
    payload = json.dumps({
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 120, "height": 80}}
            ]
        }
    })
    skills_dir = os.path.abspath(".agents/skills")
    env = {**os.environ, "PYTHONPATH": skills_dir}
    res = subprocess.run(
        [sys.executable, "-m", "lib_py.layout", "--min-cols", "60", "--min-rows", "20"],
        input=payload,
        capture_output=True,
        text=True,
        env=env
    )
    assert res.returncode == 0
    assert res.stdout.strip() == "right p1"

    res_json = subprocess.run(
        [sys.executable, "-m", "lib_py.layout", "--json"],
        input=payload,
        capture_output=True,
        text=True,
        env=env
    )
    assert res_json.returncode == 0
    data = json.loads(res_json.stdout)
    assert data["target_pane_id"] == "p1"
    assert data["direction"] == "right"
    assert not data["is_overflow"]


def test_lib_sh_no_local_in_shim_heredoc():
    """Verify F-1: No 'local' declarations inside the top-level shim heredoc dispatcher."""
    import os
    lib_path = os.path.abspath(".agents/skills/lib.sh")
    with open(lib_path, "r", encoding="utf-8") as f:
        content = f.read()

    start_idx = content.find("cat <<'EOF' > \"$tmp_file\"")
    end_idx = content.find("\nEOF\n", start_idx)
    assert start_idx != -1 and end_idx != -1

    heredoc = content[start_idx:end_idx]
    # Check specifically in the layout block
    layout_idx = heredoc.find('split_dir=""')
    assert layout_idx != -1
    layout_block = heredoc[layout_idx:layout_idx + 800]
    assert "local " not in layout_block, f"Forbidden 'local' found in top-level shim heredoc:\n{layout_block}"


def test_5_panes_to_6_panes_fill_singleton_in_3rd_column():
    """Verify 5 panes (2x2 full + 1 singleton in 3rd col) -> splits 3rd col singleton down to make 2x3 grid."""
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}},
                {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}},
                {"pane_id": "p3", "rect": {"x": 80, "y": 0, "width": 80, "height": 40}},
                {"pane_id": "p4", "rect": {"x": 80, "y": 40, "width": 80, "height": 40}},
                {"pane_id": "p5", "rect": {"x": 160, "y": 0, "width": 80, "height": 80}}
            ]
        }
    }
    d = compute_2xk_layout(payload, min_cols=60, min_rows=20)
    assert d.target_pane_id == "p5"
    assert d.direction == "down"
    assert not d.is_overflow


def test_lib_sh_layout_split_in_set_e_subshell(tmp_path):
    """Verify F-1 & F-2: lib.sh layout split block executes cleanly in set -euo pipefail top-level script."""
    import os
    skills_dir = os.path.abspath(".agents/skills")
    script = f"""#!/usr/bin/env bash
set -euo pipefail
export PYTHONPATH="{skills_dir}"

_real_herdr() {{
  if [ "${{1:-}}" = "pane" ] && [ "${{2:-}}" = "layout" ]; then
    echo '{{"result": {{"panes": [{{"pane_id": "p1", "rect": {{"x": 0, "y": 0, "width": 120, "height": 80}}}}]}}}}'
    return 0
  fi
  return 1
}}

sample_pane="p1"
split_dir=""

# Exact snippet from lib.sh layout invocation
if [ -n "$sample_pane" ]; then
  layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "")
  read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${{MAM_MIN_PANE_COLS:-15}}" --min-rows "${{MAM_MIN_PANE_ROWS:-0}}" --max-cols "${{MAM_MAX_PANE_COLS:-2}}" --max-rows "${{MAM_MAX_PANE_ROWS:-2}}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane")
  split_dir="${{split_dir:-right}}"
  sample_pane="${{split_target:-$sample_pane}}"
fi

echo "SPLIT_DIR=$split_dir"
echo "SAMPLE_PANE=$sample_pane"
"""
    res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
    assert res.returncode == 0, f"Script failed with code {res.returncode}. Stderr: {res.stderr}"
    assert "SPLIT_DIR=right" in res.stdout
    assert "SAMPLE_PANE=p1" in res.stdout


def test_real_generated_shim_layout_split(tmp_path):
    """Verify generated shim executes layout.py without command not found or local aborts."""
    import os
    skills_dir = os.path.abspath(".agents/skills")
    ws_dir = str(tmp_path / "ws")
    os.makedirs(ws_dir, exist_ok=True)

    test_script = f"""#!/usr/bin/env bash
set -euo pipefail
export WORKSPACE_ROOT="{ws_dir}"
export SKILL_DIR="{skills_dir}"
source "{skills_dir}/lib.sh"

_init_herdr_isolation

shim_path="$WORKSPACE_ROOT/.mam/shim/herdr"
if [ ! -x "$shim_path" ]; then
  echo "ERROR: shim not generated or not executable" >&2
  exit 1
fi

# Verify no 'local ' inside the shim heredoc body
if grep -E '^[[:space:]]*local layout_' "$shim_path"; then
  echo "ERROR: 'local layout_' found in generated shim" >&2
  exit 1
fi
echo "SHIM_OK"
"""
    res = subprocess.run(["bash", "-c", test_script], capture_output=True, text=True)
    assert res.returncode == 0, f"Shim test failed: {res.stderr}"
    assert "SHIM_OK" in res.stdout


def _four_panes_two_columns():
    """GUI payload: 2 full columns x 2 rows (4 panes). Shared by the max-cols tests."""
    return {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 40}},
                {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 100, "height": 40}},
                {"pane_id": "p3", "rect": {"x": 100, "y": 0, "width": 100, "height": 40}},
                {"pane_id": "p4", "rect": {"x": 100, "y": 40, "width": 100, "height": 40}},
            ]
        }
    }


def test_cli_max_cols_flag_triggers_overflow():
    """CLI --max-cols reaches compute_2xk_layout (the lib.sh-facing path)."""
    payload = json.dumps(_four_panes_two_columns())
    skills_dir = os.path.abspath(".agents/skills")
    env = {**os.environ, "PYTHONPATH": skills_dir}
    res = subprocess.run(
        [sys.executable, "-m", "lib_py.layout",
         "--min-cols", "30", "--min-rows", "20", "--max-cols", "2", "--json"],
        input=payload, capture_output=True, text=True, env=env)
    assert res.returncode == 0, res.stderr
    d = json.loads(res.stdout)
    assert d["direction"] == "overflow" and d["is_overflow"]
    assert d["reason"] == "grid_capacity_reached"


def test_env_max_cols_applies_without_flag():
    """MAM_MAX_PANE_COLS is honoured with no --max-cols flag."""
    payload = json.dumps(_four_panes_two_columns())
    skills_dir = os.path.abspath(".agents/skills")
    env = {**os.environ, "PYTHONPATH": skills_dir, "MAM_MAX_PANE_COLS": "2"}
    res = subprocess.run(
        [sys.executable, "-m", "lib_py.layout",
         "--min-cols", "30", "--min-rows", "20", "--json"],
        input=payload, capture_output=True, text=True, env=env)
    assert res.returncode == 0, res.stderr
    assert json.loads(res.stdout)["reason"] == "grid_capacity_reached"


def _headless(n):
    return {"result": {"panes": [
        {"pane_id": f"p{i}", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}}
        for i in range(1, n + 1)]}}


def test_headless_max_columns_growth_guard():
    """Headless capacity: n=1 right, n=2/3 down, n>=4 overflow at max_columns=2, max_rows=2."""
    d4 = compute_2xk_layout(_headless(4), max_columns=2, max_rows=2)
    assert d4.is_overflow and d4.direction == "overflow"
    assert d4.reason == "grid_capacity_reached"

    d2 = compute_2xk_layout(_headless(2), max_columns=2, max_rows=2)
    assert d2.direction == "down" and d2.target_pane_id == "p1" and not d2.is_overflow

    d3 = compute_2xk_layout(_headless(3), max_columns=2, max_rows=2)
    assert d3.direction == "down" and d3.target_pane_id == "p2" and not d3.is_overflow

    d5 = compute_2xk_layout(_headless(5), max_columns=2, max_rows=2)
    assert d5.is_overflow and d5.reason == "grid_capacity_reached"

    # Default cap is 2 even when the caller omits max_columns.
    assert compute_2xk_layout(_headless(4)).direction == "overflow"


_LAYOUT_ENV_VARS = ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", "MAM_MIN_ROWS",
                    "MAM_MIN_PANE_ROWS", "MAM_MAX_COLS", "MAM_MAX_PANE_COLS",
                    "MAM_MAX_ROWS", "MAM_MAX_PANE_ROWS")


def _run_layout(payload, args=(), env_extra=None):
    env = {**os.environ, "PYTHONPATH": os.path.abspath(".agents/skills")}
    for k in _LAYOUT_ENV_VARS:
        env.pop(k, None)          # 호출자 셸의 오염 차단
    env.update(env_extra or {})
    res = subprocess.run([sys.executable, "-m", "lib_py.layout", "--json", *args],
                         input=json.dumps(payload), capture_output=True, text=True, env=env)
    assert res.returncode == 0, res.stderr
    return json.loads(res.stdout)


# height//2 = 15 < min_rows(20) 로 제약 분기 진입, width//2 = 25 가 min_cols 와 비교됨.
_ZERO_TRAP = {"result": {"panes": [
    {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 30}}]}}


def test_j1_env_zero_min_cols_matches_flag_zero():
    """J-1: MAM_MIN_PANE_COLS=0 must mean 0, not fall through to the 15 default."""
    flag = _run_layout(_ZERO_TRAP, ("--min-cols", "0", "--min-rows", "20"))
    assert flag["direction"] == "right" and flag["reason"] == "new_column_right"
    for var in ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS"):
        assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {var: "0"}) == flag, var


def test_j1_env_zero_min_rows_matches_flag_zero():
    flag = _run_layout(_ZERO_TRAP, ("--min-rows", "0"))
    assert flag["direction"] == "right" and flag["reason"] == "new_column_right"
    for var in ("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS"):
        assert _run_layout(_ZERO_TRAP, (), {var: "0"}) == flag, var


def test_j1_nonzero_and_malformed_env_behaviour_unchanged():
    """Behaviour neutrality: non-zero env still applies, and a lone typo still
    lands on the documented default instead of crashing on a None comparison."""
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25", "MAM_MIN_PANE_ROWS": "20"}) == \
           _run_layout(_ZERO_TRAP, ("--min-cols", "25", "--min-rows", "20"))
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "abc"}) == _run_layout(_ZERO_TRAP)


def test_j1b_invalid_alias_does_not_shadow_the_documented_var():
    """C-2: MAM_MIN_COLS is a legacy alias checked first; MAM_MIN_PANE_COLS is the
    name .mam.env.example documents. An unparsable value in the alias must be
    skipped, not abort the search and discard the documented setting.

    Empty values already fell through (`if raw:`); this makes invalid values
    behave the same way. When every candidate is unusable, `default` still wins.
    """
    good = _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_PANE_COLS": "25"})
    assert good["direction"] == "right"
    # 별칭이 깨져 있어도 문서화된 변수가 적용된다
    assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_COLS": "foo",
                                        "MAM_MIN_PANE_COLS": "25"}) == good
    # 0 도 마찬가지 (J-1 과의 상호작용)
    assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_COLS": "foo",
                                        "MAM_MIN_PANE_COLS": "0"}) == \
           _run_layout(_ZERO_TRAP, ("--min-cols", "0", "--min-rows", "20"))
    # 모든 후보가 무효면 문서화된 기본값으로 흡수 (Rev.1 불변식 보존)
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo",
                                        "MAM_MIN_PANE_COLS": "bar"}) == _run_layout(_ZERO_TRAP)


def test_default_min_cols_is_15_and_min_rows_is_0():
    """Default min_cols=15, min_rows=0. N=1 opens a column with right."""
    p1_54x23 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}}
    d1 = compute_2xk_layout(p1_54x23)
    assert d1.direction == "right"
    assert not d1.is_overflow
    assert d1.reason == "new_column_right"

    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 40}},
            ]
        }
    }
    decision = compute_2xk_layout(payload)
    assert decision.direction == "right"
    assert not decision.is_overflow
    assert decision.reason == "new_column_right"


def test_30_col_2_column_splitting_boundary():
    """N=1: width >= 30 allows a right split (min_cols=15); 29 overflows."""
    payload_30 = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 40}},
            ]
        }
    }
    d30 = compute_2xk_layout(payload_30)
    assert d30.direction == "right"
    assert not d30.is_overflow

    payload_29 = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 29, "height": 40}},
            ]
        }
    }
    d29 = compute_2xk_layout(payload_29)
    assert d29.direction == "overflow"
    assert d29.is_overflow
    assert d29.reason == "column_width_overflow"


def test_54x23_compact_viewport_single_workspace_multi_pane_tiling():
    """1→2 right, 2→3 down left, 3→4 down right, 4→5 overflow at capacity."""
    p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}}
    d1 = compute_2xk_layout(p1_layout)
    assert d1.direction == "right"
    assert d1.target_pane_id == "p1"
    assert not d1.is_overflow

    p2_layout = {"result": {"panes": [
        {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 23}},
        {"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}},
    ]}}
    d2 = compute_2xk_layout(p2_layout)
    assert d2.direction == "down"
    assert d2.target_pane_id == "p1"
    assert not d2.is_overflow

    p3_layout = {"result": {"panes": [
        {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}},
        {"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}},
        {"pane_id": "p3", "rect": {"x": 26, "y": 12, "width": 27, "height": 12}},
    ]}}
    d3 = compute_2xk_layout(p3_layout)
    assert d3.direction == "down"
    assert d3.target_pane_id == "p2"
    assert not d3.is_overflow

    p4_layout = {"result": {"panes": [
        {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}},
        {"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 11}},
        {"pane_id": "p3", "rect": {"x": 26, "y": 12, "width": 27, "height": 12}},
        {"pane_id": "p4", "rect": {"x": 53, "y": 12, "width": 27, "height": 12}},
    ]}}
    d4 = compute_2xk_layout(p4_layout)
    assert d4.direction == "overflow"
    assert d4.is_overflow
    assert d4.reason == "grid_capacity_reached"


def _payload(panes):
    return {"result": {"panes": [
        {"pane_id": p["id"], "rect": {"x": p["x"], "y": p["y"], "width": p["w"], "height": p["h"]}}
        for p in panes
    ]}}


def _bsp_split(panes, tid, direction):
    """herdr contract: bisect only the target pane's rect."""
    out = []
    next_id = f"p{len(panes) + 1}"
    for p in panes:
        if p["id"] != tid:
            out.append(dict(p))
            continue
        if direction == "right":
            w1 = p["w"] // 2
            w2 = p["w"] - w1
            out.append({**p, "w": w1})
            out.append({"id": next_id, "x": p["x"] + w1, "y": p["y"], "w": w2, "h": p["h"]})
        elif direction == "down":
            h1 = p["h"] // 2
            h2 = p["h"] - h1
            out.append({**p, "h": h1})
            out.append({"id": next_id, "x": p["x"], "y": p["y"] + h1, "w": p["w"], "h": h2})
        else:
            raise AssertionError(f"unexpected split {direction}")
    return out


def test_four_panes_form_clean_2x2():
    panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}]
    for _ in range(3):
        d = compute_2xk_layout(_payload(panes))
        assert not d.is_overflow, d
        panes = _bsp_split(panes, d.target_pane_id, d.direction)
    assert len(panes) == 4
    assert max(p["w"] for p in panes) - min(p["w"] for p in panes) <= 2
    assert max(p["h"] for p in panes) - min(p["h"] for p in panes) <= 2


def test_fifth_pane_overflows():
    panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}]
    for _ in range(3):
        d = compute_2xk_layout(_payload(panes))
        assert not d.is_overflow
        panes = _bsp_split(panes, d.target_pane_id, d.direction)
    d = compute_2xk_layout(_payload(panes))
    assert d.is_overflow and d.direction == "overflow"
    assert d.reason == "grid_capacity_reached"


@pytest.mark.parametrize("n,expected", [(1, "right"), (2, "down"), (3, "down"), (4, "overflow")])
def test_headless_matches_gui_decision(n, expected):
    hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2)
    assert hl.direction == expected, f"headless n={n}"


def test_headless_gui_direction_parity_full_sequence():
    panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}]
    for n in range(1, 5):
        gui = compute_2xk_layout(_payload(panes), max_columns=2, max_rows=2)
        hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2)
        assert gui.direction == hl.direction, f"divergence at n={n}"
        if gui.is_overflow:
            break
        panes = _bsp_split(panes, gui.target_pane_id, gui.direction)


def test_headless_fills_alternating_columns():
    assert compute_2xk_layout(_headless(2), max_columns=2, max_rows=2).target_pane_id == "p1"
    assert compute_2xk_layout(_headless(3), max_columns=2, max_rows=2).target_pane_id == "p2"


def test_headless_ignores_sample_pane_anchor():
    d = compute_2xk_layout(_headless(3), max_columns=2, max_rows=2, default_anchor_id="p1")
    assert d.target_pane_id == "p2"


def test_never_splits_right_on_partial_height_pane():
    payload = {
        "result": {
            "panes": [
                {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 277, "height": 39}},
                {"pane_id": "p2", "rect": {"x": 0, "y": 39, "width": 277, "height": 39}},
            ]
        }
    }
    d = compute_2xk_layout(payload, max_columns=2, max_rows=2)
    assert d.direction != "right"


def test_max_cols_default_reaches_cli_path():
    payload = json.dumps(_four_panes_two_columns())
    skills_dir = os.path.abspath(".agents/skills")
    env = {**os.environ, "PYTHONPATH": skills_dir}
    for k in _LAYOUT_ENV_VARS:
        env.pop(k, None)
    res = subprocess.run(
        [sys.executable, "-m", "lib_py.layout", "--json"],
        input=payload, capture_output=True, text=True, env=env)
    assert res.returncode == 0, res.stderr
    d = json.loads(res.stdout)
    assert d["direction"] == "overflow"
    assert d["reason"] == "grid_capacity_reached"


def test_lib_sh_passes_max_cols_and_rows():
    lib_path = os.path.abspath(".agents/skills/lib.sh")
    content = open(lib_path, encoding="utf-8").read()
    assert '--max-cols "${MAM_MAX_PANE_COLS:-2}"' in content
    assert '--max-rows "${MAM_MAX_PANE_ROWS:-2}"' in content


