import pytest
from pyos.Activity import solve_layout


def fixed(height):
    return {"layout": {"height": height}}


def flex(f, min_height=0, max_height=None):
    cfg = {"layout": {"flex": f, "min_height": min_height}}
    if max_height is not None:
        cfg["layout"]["max_height"] = max_height
    return cfg


# ---------------------------------------------------------------------------
# Empty / trivial
# ---------------------------------------------------------------------------

def test_empty_display_state():
    assert solve_layout({}, 40) == {}


def test_zero_screen_height():
    ds = {"a": fixed(5), "b": flex(1)}
    result = solve_layout(ds, 0)
    assert result["a"] == 5   # fixed regions don't get clamped by screen
    assert result["b"] == 0


# ---------------------------------------------------------------------------
# Fixed regions
# ---------------------------------------------------------------------------

def test_single_fixed_region():
    result = solve_layout({"header": fixed(3)}, 40)
    assert result == {"header": 3}


def test_multiple_fixed_regions():
    ds = {"top": fixed(2), "mid": fixed(10), "bot": fixed(1)}
    result = solve_layout(ds, 40)
    assert result == {"top": 2, "mid": 10, "bot": 1}


def test_fixed_regions_only_no_overflow_concern():
    # Fixed regions are allocated regardless of screen height
    ds = {"a": fixed(20), "b": fixed(20)}
    result = solve_layout(ds, 40)
    assert result["a"] == 20
    assert result["b"] == 20


# ---------------------------------------------------------------------------
# Single flex region
# ---------------------------------------------------------------------------

def test_single_flex_gets_all_remaining_space():
    ds = {"body": flex(1)}
    result = solve_layout(ds, 30)
    assert result["body"] == 30


def test_flex_gets_space_after_fixed():
    ds = {"top": fixed(2), "body": flex(1), "bot": fixed(1)}
    result = solve_layout(ds, 30)
    assert result["top"] == 2
    assert result["bot"] == 1
    assert result["body"] == 27


# ---------------------------------------------------------------------------
# Multiple flex regions — proportional distribution
# ---------------------------------------------------------------------------

def test_two_equal_flex_regions_split_evenly():
    ds = {"a": flex(1), "b": flex(1)}
    result = solve_layout(ds, 40)
    assert result["a"] + result["b"] == 40
    assert result["a"] == result["b"]


def test_flex_ratio_1_to_2():
    ds = {"small": flex(1), "large": flex(2)}
    result = solve_layout(ds, 30)
    # large should be roughly twice small; total must be 30
    assert result["small"] + result["large"] == 30
    assert result["large"] >= result["small"] * 1.5


def test_flex_ratio_1_to_3():
    ds = {"a": flex(1), "b": flex(3)}
    result = solve_layout(ds, 40)
    assert result["a"] + result["b"] == 40
    assert result["b"] > result["a"]


# ---------------------------------------------------------------------------
# min_height
# ---------------------------------------------------------------------------

def test_flex_min_height_respected_when_space_is_tight():
    # Fixed consumes almost everything; flex min_height should be the floor
    ds = {"top": fixed(28), "body": flex(1, min_height=5)}
    result = solve_layout(ds, 30)
    assert result["body"] >= 5


def test_flex_min_height_is_baseline_when_no_remaining_space():
    ds = {"top": fixed(30), "body": flex(1, min_height=5)}
    result = solve_layout(ds, 30)
    assert result["body"] == 5


def test_two_flex_both_hit_min_height():
    ds = {"a": flex(1, min_height=10), "b": flex(1, min_height=10)}
    # Only 15 total — both min_heights can't be fully satisfied from remaining
    result = solve_layout(ds, 15)
    assert result["a"] >= 0
    assert result["b"] >= 0


# ---------------------------------------------------------------------------
# max_height
# ---------------------------------------------------------------------------

def test_flex_max_height_caps_growth():
    ds = {"a": flex(1, max_height=5)}
    result = solve_layout(ds, 40)
    assert result["a"] == 5


def test_excess_space_not_given_to_capped_flex():
    # a is capped at 5; b should absorb the rest
    ds = {"a": flex(1, max_height=5), "b": flex(1)}
    result = solve_layout(ds, 40)
    assert result["a"] == 5
    assert result["b"] == 35


def test_flex_min_and_max_height():
    ds = {"a": flex(1, min_height=5, max_height=10)}
    result = solve_layout(ds, 40)
    assert result["a"] == 10


def test_flex_min_greater_than_available_uses_min():
    ds = {"a": flex(1, min_height=8, max_height=10)}
    result = solve_layout(ds, 5)
    assert result["a"] == 8


# ---------------------------------------------------------------------------
# Mixed fixed + multiple flex
# ---------------------------------------------------------------------------

def test_fixed_and_two_flex_regions():
    ds = {
        "top": fixed(3),
        "main": flex(2),
        "side": flex(1),
        "bot": fixed(2),
    }
    result = solve_layout(ds, 30)
    assert result["top"] == 3
    assert result["bot"] == 2
    assert result["main"] + result["side"] == 25
    assert result["main"] > result["side"]


def test_total_allocated_does_not_exceed_screen():
    ds = {
        "top": fixed(2),
        "a": flex(1),
        "b": flex(3),
        "bot": fixed(1),
    }
    result = solve_layout(ds, 24)
    assert sum(result.values()) <= 24


# ---------------------------------------------------------------------------
# Regions without layout keys (non-flex, non-fixed) are skipped by solve_layout
# ---------------------------------------------------------------------------

def test_region_without_layout_key_is_ignored():
    # solve_layout only handles regions present in the passed dict;
    # Activity.generate_line_printers converts measured regions to fixed
    # before calling solve_layout. If a region has no "height" and no "flex",
    # solve_layout simply skips it (returns nothing for that key).
    ds = {"known": fixed(5)}
    result = solve_layout(ds, 40)
    assert result == {"known": 5}


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

def test_screen_height_of_one():
    ds = {"a": flex(1)}
    result = solve_layout(ds, 1)
    assert result["a"] == 1


def test_fixed_height_zero():
    ds = {"a": fixed(0), "b": flex(1)}
    result = solve_layout(ds, 20)
    assert result["a"] == 0
    assert result["b"] == 20


def test_large_number_of_fixed_regions():
    ds = {f"r{i}": fixed(1) for i in range(10)}
    result = solve_layout(ds, 40)
    for key in ds:
        assert result[key] == 1


def test_flex_value_zero_is_treated_as_no_flex():
    # flex=0 should not be treated as a flex region
    ds = {"a": {"layout": {"flex": 0}}, "b": flex(1)}
    result = solve_layout(ds, 20)
    # "a" has flex=0 so it's not a flex item; it has no height either,
    # so solve_layout won't allocate it. "b" gets all remaining space.
    assert result.get("b") == 20


def test_multiple_flex_total_equals_screen_minus_fixed():
    ds = {
        "header": fixed(4),
        "footer": fixed(2),
        "pane_a": flex(1),
        "pane_b": flex(1),
        "pane_c": flex(2),
    }
    screen = 50
    result = solve_layout(ds, screen)
    flex_total = result["pane_a"] + result["pane_b"] + result["pane_c"]
    assert flex_total == screen - 4 - 2
