"""Professional Services Request Prioritization

Order: high -> medium -> low; within a priority, fewer estimated_hours first;
full ties keep their original order.
Works on plain Python 3 (no external libraries), so it runs on ideone as-is.
"""

# Lower number = handled earlier. A dict makes the rule easy to read and extend.
PRIORITY_RANK = {"high": 0, "medium": 1, "low": 2}


def _sort_key(request):
    """Validate one request and return its (priority_rank, hours) sort key."""
    # Required fields must exist
    for field in ("id", "customer", "priority", "estimated_hours"):
        if field not in request:
            raise ValueError(f"Request is missing '{field}': {request}")

    # Normalise priority so "High" / " HIGH " still work
    priority = str(request["priority"]).strip().lower()
    if priority not in PRIORITY_RANK:
        raise ValueError(
            f"Unknown priority '{request['priority']}' in {request['id']}; "
            f"expected one of {list(PRIORITY_RANK)}"
        )

    # Hours must be a non-negative number (bool is an int in Python, so reject it)
    hours = request["estimated_hours"]
    if isinstance(hours, bool) or not isinstance(hours, (int, float)) or hours < 0:
        raise ValueError(f"Invalid estimated_hours '{hours}' in {request['id']}")

    return PRIORITY_RANK[priority], hours


def prioritize_requests(requests):
    """Return a NEW list of requests in the order they should be handled.

    sorted() is stable: items with equal keys keep their original relative order,
    which satisfies the "preserve original order on a full tie" rule for free.
    The input list is not modified.
    """
    return sorted(requests, key=_sort_key)


# ----------------------------- tests -----------------------------

def ids(requests):
    return [r["id"] for r in requests]


def make(req_id, priority, hours, customer="Acme"):
    return {"id": req_id, "customer": customer, "priority": priority, "estimated_hours": hours}


def test_sample_input():
    sample = [
        make("REQ-101", "medium", 3, "Alpha Ltd"),
        make("REQ-102", "high", 5, "Beta Corp"),
        make("REQ-103", "low", 2, "Alpha Ltd"),
        make("REQ-104", "high", 4, "Gamma Inc"),
    ]
    assert ids(prioritize_requests(sample)) == ["REQ-104", "REQ-102", "REQ-101", "REQ-103"]


def test_same_priority_orders_by_hours():
    reqs = [make("A", "medium", 8), make("B", "medium", 1), make("C", "medium", 4)]
    assert ids(prioritize_requests(reqs)) == ["B", "C", "A"]


def test_tie_preserves_original_order():
    # Same priority AND same hours -> must stay in input order (stability)
    reqs = [make("FIRST", "high", 3), make("SECOND", "high", 3), make("THIRD", "high", 3)]
    assert ids(prioritize_requests(reqs)) == ["FIRST", "SECOND", "THIRD"]


def test_priority_beats_hours():
    # A low-priority 1-hour job must not jump ahead of a high-priority 40-hour job
    reqs = [make("LOW", "low", 1), make("HIGH", "high", 40)]
    assert ids(prioritize_requests(reqs)) == ["HIGH", "LOW"]


def test_empty_list():
    assert prioritize_requests([]) == []


def test_single_request_and_zero_hours():
    reqs = [make("ONLY", "low", 0)]
    assert ids(prioritize_requests(reqs)) == ["ONLY"]


def test_priority_case_and_whitespace_insensitive():
    reqs = [make("L", "LOW", 1), make("H", " High ", 9)]
    assert ids(prioritize_requests(reqs)) == ["H", "L"]


def test_float_hours():
    reqs = [make("A", "high", 2.5), make("B", "high", 2.25)]
    assert ids(prioritize_requests(reqs)) == ["B", "A"]


def test_input_not_modified():
    reqs = [make("A", "low", 1), make("B", "high", 1)]
    snapshot = list(reqs)
    prioritize_requests(reqs)
    assert reqs == snapshot


def test_invalid_data_raises():
    bad_inputs = [
        [make("X", "urgent", 2)],                      # unknown priority
        [make("X", "high", -1)],                       # negative hours
        [make("X", "high", "3")],                      # hours not a number
        [make("X", "high", True)],                     # bool is not valid hours
        [{"id": "X", "customer": "A", "priority": "high"}],   # missing field
    ]
    for bad in bad_inputs:
        try:
            prioritize_requests(bad)
        except ValueError:
            continue
        raise AssertionError(f"Expected ValueError for {bad}")


def run_tests():
    tests = [v for k, v in globals().items() if k.startswith("test_") and callable(v)]
    for test in tests:
        test()
        print(f"PASS  {test.__name__}")
    print(f"\nAll {len(tests)} tests passed")


if __name__ == "__main__":
    sample_requests = [
        make("REQ-101", "medium", 3, "Alpha Ltd"),
        make("REQ-102", "high", 5, "Beta Corp"),
        make("REQ-103", "low", 2, "Alpha Ltd"),
        make("REQ-104", "high", 4, "Gamma Inc"),
    ]
    print("Handling order:")
    for position, req in enumerate(prioritize_requests(sample_requests), start=1):
        print(f"{position}. {req['id']}  {req['customer']:<10} "
              f"{req['priority']:<6} {req['estimated_hours']}h")
    print()
    run_tests()