"""
Tests for the Ratatui-inspired constraint-based layout solver.

Covers:
- Each constraint type in isolation (Length, Percentage, Min, Max, Fill)
- Mixed combinations
- Integer remainder / Bresenham distribution correctness
- Priority ordering (Length > Percentage > Fill)
- Min floor enforcement
- Max cap enforcement with excess redistribution
- Edge cases (zero screen, single region, all types together)
"""

import pytest
from pyos.layout import Length, Percentage, Min, Max, Fill, solve_constraints


# ---------------------------------------------------------------------------
# Helpers — build minimal display_state entries
# ---------------------------------------------------------------------------

def L(n):
    return {"layout": Length(n), "line_generator": lambda ctx, h: []}

def P(pct):
    return {"layout": Percentage(pct), "line_generator": lambda ctx, h: []}

def F(weight=1.0):
    return {"layout": Fill(weight), "line_generator": lambda ctx, h: []}

def MI(n):
    return {"layout": Min(n), "line_generator": lambda ctx, h: []}

def MA(n):
    return {"layout": Max(n), "line_generator": lambda ctx, h: []}


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

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


def test_zero_screen_height_length():
    # Length is always satisfied; even on a 0-height screen it allocates its value
    result = solve_constraints({"a": L(3)}, 0)
    assert result["a"] == 3


def test_zero_screen_height_fill():
    result = solve_constraints({"a": F(1)}, 0)
    assert result["a"] == 0


def test_zero_screen_height_percentage():
    result = solve_constraints({"a": P(50)}, 0)
    assert result["a"] == 0


# ---------------------------------------------------------------------------
# Length
# ---------------------------------------------------------------------------

def test_single_length():
    result = solve_constraints({"header": L(3)}, 40)
    assert result["header"] == 3


def test_multiple_lengths():
    ds = {"top": L(2), "mid": L(10), "bot": L(1)}
    result = solve_constraints(ds, 40)
    assert result == {"top": 2, "mid": 10, "bot": 1}


def test_length_oversubscribed_still_allocates():
    # Length is always satisfied; the total can exceed screen_height.
    ds = {"a": L(20), "b": L(30)}
    result = solve_constraints(ds, 40)
    assert result["a"] == 20
    assert result["b"] == 30


def test_length_leaves_correct_remainder_for_fill():
    ds = {"top": L(2), "body": F(1), "bot": L(1)}
    result = solve_constraints(ds, 30)
    assert result["top"] == 2
    assert result["bot"] == 1
    assert result["body"] == 27


# ---------------------------------------------------------------------------
# Percentage
# ---------------------------------------------------------------------------

def test_single_percentage_exact():
    # 50% of 40 = 20 exactly
    result = solve_constraints({"a": P(50)}, 40)
    assert result["a"] == 20


def test_single_percentage_rounds_down():
    # 33% of 10 = 3.3 → 3
    result = solve_constraints({"a": P(33)}, 10)
    assert result["a"] == 3


def test_percentage_100():
    result = solve_constraints({"a": P(100)}, 30)
    assert result["a"] == 30


def test_percentage_zero():
    result = solve_constraints({"a": P(0)}, 30)
    assert result["a"] == 0


def test_two_percentages_sum_to_100():
    ds = {"a": P(50), "b": P(50)}
    result = solve_constraints(ds, 40)
    assert result["a"] + result["b"] == 40


def test_three_percentages_integer_remainder():
    # 3 × 33.33% of 100 = 99.99 → floor 33 each, one remainder unit distributed
    ds = {"a": P(33.333), "b": P(33.333), "c": P(33.333)}
    result = solve_constraints(ds, 100)
    total = result["a"] + result["b"] + result["c"]
    # The three regions together should use at most 100 and at least 99
    assert 99 <= total <= 100
    for v in result.values():
        assert v in (33, 34)


def test_two_percentages_leave_remainder_for_fill():
    # 30% + 30% = 60% of 100, leaving 40 for fill
    ds = {"left": P(30), "right": P(30), "fill": F(1)}
    result = solve_constraints(ds, 100)
    assert result["left"] == 30
    assert result["right"] == 30
    assert result["fill"] == 40


def test_percentage_capped_to_remaining_after_length():
    # Length(10) takes 10; only 10 remain for Percentage(100%) of total=20 → capped at 10
    ds = {"fixed": L(10), "pct": P(100)}
    result = solve_constraints(ds, 20)
    assert result["fixed"] == 10
    assert result["pct"] <= 10


# ---------------------------------------------------------------------------
# Fill
# ---------------------------------------------------------------------------

def test_single_fill_gets_all():
    result = solve_constraints({"body": F(1)}, 30)
    assert result["body"] == 30


def test_two_equal_fills_split_evenly():
    ds = {"a": F(1), "b": F(1)}
    result = solve_constraints(ds, 40)
    assert result["a"] + result["b"] == 40
    assert result["a"] == result["b"]


def test_fill_ratio_2_to_1():
    ds = {"large": F(2), "small": F(1)}
    result = solve_constraints(ds, 30)
    assert result["large"] + result["small"] == 30
    assert result["large"] == 20
    assert result["small"] == 10


def test_fill_ratio_3_to_1():
    ds = {"a": F(3), "b": F(1)}
    result = solve_constraints(ds, 40)
    assert result["a"] + result["b"] == 40
    assert result["a"] == 30
    assert result["b"] == 10


def test_fill_weight_zero_gets_nothing():
    ds = {"a": F(0), "b": F(1)}
    result = solve_constraints(ds, 20)
    assert result["a"] == 0
    assert result["b"] == 20


def test_three_fills_integer_remainder():
    # 3 equal Fill on 10 lines → two get 3, one gets 4 (or some combination summing to 10)
    ds = {"a": F(1), "b": F(1), "c": F(1)}
    result = solve_constraints(ds, 10)
    assert result["a"] + result["b"] + result["c"] == 10
    for v in result.values():
        assert v in (3, 4)


# ---------------------------------------------------------------------------
# Min floor
# ---------------------------------------------------------------------------

def test_single_min_gets_at_least_n():
    result = solve_constraints({"a": MI(5)}, 30)
    assert result["a"] >= 5


def test_single_min_expands_to_fill_all():
    # Min acts like Fill(1) with a floor; with nothing else it should get all space
    result = solve_constraints({"a": MI(5)}, 30)
    assert result["a"] == 30


def test_min_floor_respected_when_space_is_tight():
    ds = {"fixed": L(25), "body": MI(5)}
    result = solve_constraints(ds, 30)
    assert result["body"] >= 5


def test_min_floor_respected_when_no_remaining():
    # Length takes everything; Min still gets its floor
    ds = {"fixed": L(30), "body": MI(5)}
    result = solve_constraints(ds, 30)
    assert result["body"] == 5


def test_two_mins_both_get_floors():
    ds = {"a": MI(5), "b": MI(3)}
    result = solve_constraints(ds, 20)
    assert result["a"] >= 5
    assert result["b"] >= 3


def test_min_and_fill_share_remaining():
    # Min(3) gets at least 3; Fill gets the rest proportionally
    ds = {"fixed": L(5), "guarded": MI(3), "free": F(1)}
    result = solve_constraints(ds, 30)
    assert result["guarded"] >= 3
    assert result["free"] >= 0
    assert result["fixed"] == 5
    assert result["guarded"] + result["free"] == 25


# ---------------------------------------------------------------------------
# Max cap
# ---------------------------------------------------------------------------

def test_single_max_capped():
    # Max(5) with lots of space — should be capped at 5
    result = solve_constraints({"a": MA(5)}, 40)
    assert result["a"] == 5


def test_max_cap_excess_redistributed_to_fill():
    ds = {"capped": MA(5), "free": F(1)}
    result = solve_constraints(ds, 40)
    assert result["capped"] == 5
    assert result["free"] == 35


def test_max_cap_when_fill_unavailable():
    # Two Max regions; excess from caps is not redistributable (no Fill) — just lost
    ds = {"a": MA(3), "b": MA(3)}
    result = solve_constraints(ds, 40)
    assert result["a"] == 3
    assert result["b"] == 3


def test_max_cap_not_enforced_when_space_is_tight():
    # If space < max, max acts as an upper bound that doesn't come into play
    ds = {"fixed": L(35), "a": MA(10)}
    result = solve_constraints(ds, 40)
    assert result["a"] <= 10


# ---------------------------------------------------------------------------
# Mixed combos
# ---------------------------------------------------------------------------

def test_length_and_fill():
    ds = {"top": L(2), "body": F(1), "bot": L(2)}
    result = solve_constraints(ds, 24)
    assert result["top"] == 2
    assert result["bot"] == 2
    assert result["body"] == 20


def test_length_percentage_fill():
    # Length(2) + Length(2) = 4 fixed; Percentage(25%) of 40 = 10; Fill gets 40-4-10=26
    ds = {"top": L(2), "info": P(25), "body": F(1), "bot": L(2)}
    result = solve_constraints(ds, 40)
    assert result["top"] == 2
    assert result["bot"] == 2
    assert result["info"] == 10
    assert result["body"] == 26


def test_min_max_fill_together():
    ds = {"guarded": MI(3), "capped": MA(5), "free": F(2)}
    result = solve_constraints(ds, 30)
    assert result["guarded"] >= 3
    assert result["capped"] <= 5
    assert result["guarded"] + result["capped"] + result["free"] == 30


def test_all_types_together():
    ds = {
        "top":  L(2),
        "info": P(20),
        "guard": MI(3),
        "cap":  MA(4),
        "body": F(1),
        "bot":  L(2),
    }
    result = solve_constraints(ds, 50)
    assert result["top"] == 2
    assert result["bot"] == 2
    assert result["info"] == 10  # 20% of 50
    assert result["guard"] >= 3
    assert result["cap"] <= 4
    total = sum(result.values())
    # Length + Percentage allocations are exact; flexible regions share the remainder
    assert total >= 2 + 2 + 10 + 3  # at minimum


# ---------------------------------------------------------------------------
# Integer remainder / Bresenham correctness
# ---------------------------------------------------------------------------

def test_two_fills_odd_total():
    # 2 equal Fill on 11 lines → one gets 5, one gets 6
    ds = {"a": F(1), "b": F(1)}
    result = solve_constraints(ds, 11)
    assert result["a"] + result["b"] == 11
    assert abs(result["a"] - result["b"]) <= 1


def test_percentage_remainder_distributed():
    # 3 regions at 33% each of 100; floor 33+33+33=99, one unit of remainder
    ds = {"a": P(33), "b": P(33), "c": P(33)}
    result = solve_constraints(ds, 100)
    total = sum(result.values())
    # Total must be ≤ 100 (can't give more than available)
    assert total <= 100
    # And must use as much as possible given floor allocations
    assert total >= 99


# ---------------------------------------------------------------------------
# Priority: Length beats Fill when space is scarce
# ---------------------------------------------------------------------------

def test_length_satisfied_when_fill_would_starve():
    # Length(20) + Length(20) = 40 on a 30-line screen; Fill gets 0 (nothing left)
    ds = {"a": L(20), "b": L(20), "c": F(1)}
    result = solve_constraints(ds, 30)
    assert result["a"] == 20
    assert result["b"] == 20
    assert result["c"] == 0


def test_length_satisfied_before_percentage():
    # Length(25) takes most of 30; Percentage(50% of 30=15) capped to remaining 5
    ds = {"fixed": L(25), "pct": P(50)}
    result = solve_constraints(ds, 30)
    assert result["fixed"] == 25
    assert result["pct"] <= 5


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

def test_single_region_fill():
    result = solve_constraints({"only": F(1)}, 1)
    assert result["only"] == 1


def test_single_region_length_large():
    result = solve_constraints({"only": L(100)}, 10)
    assert result["only"] == 100


def test_large_number_of_fill_regions():
    ds = {f"r{i}": F(1) for i in range(10)}
    result = solve_constraints(ds, 100)
    assert sum(result.values()) == 100
    for v in result.values():
        assert v == 10


def test_fill_weight_fractional():
    ds = {"a": F(0.5), "b": F(0.5)}
    result = solve_constraints(ds, 20)
    assert result["a"] + result["b"] == 20
    assert result["a"] == result["b"]


def test_percentage_over_100_capped_to_remaining():
    # Percentage(200%) of 30 = 60, but only 30 available → capped
    result = solve_constraints({"a": P(200)}, 30)
    assert result["a"] <= 30
