I implement a working version of the [Lift Kata][1]

Can you provide me some tips to improve my code?

I am not very proud of the `handleLiftWithOpenDoors` method that have a side effect on `calls`: as I am in a stream().map() function, I which the lamba to be pure and has no side effect. Do you think of something elegant to solve this problem?


 public class LiftSystem {
 private final List<Integer> floors;
 private final Map<Integer, List<Call>> calls;
 private List<Lift> lifts;
 
 public LiftSystem(List<Integer> floors, List<Lift> lifts, List<Call> calls) {
 this.floors = floors;
 this.calls = calls.stream().collect(Collectors.groupingBy(Call::fromFloor));
 this.lifts = lifts;
 }
 
 public List<Integer> getFloorsInDescendingOrder() {
 List<Integer> shallowCopy = new ArrayList<>(floors);
 Collections.reverse(shallowCopy);
 return shallowCopy;
 }
 
 public List<Call> getCallsForFloor(int floor) {
 return calls.getOrDefault(floor, Collections.emptyList());
 }
 
 public List<Lift> getLifts() {
 return lifts;
 }
 
 public void tick() {
 lifts = lifts.stream()
 .map(lift -> {
 List<Call> callsAtThisFloor = calls.getOrDefault(lift.floor(), Collections.emptyList());
 if (lift.doorsOpen()) {
 // FIXME handleLiftWithOpenDoors have a side effect on callsAtThisFloor
 return handleLiftWithOpenDoors(lift, callsAtThisFloor);
 } else {
 return handleLiftWithClosedDoors(lift, callsAtThisFloor);
 }
 })
 .collect(Collectors.toList());
 }
 
 private Lift handleLiftWithOpenDoors(Lift lift, List<Call> callsAtThisFloor) {
 List<Integer> remainingRequests = lift.filterRequests(lift.floor());
 
 if (!callsAtThisFloor.isEmpty()) {
 Direction liftDirection = liftDirection(lift, remainingRequests).orElse(callsAtThisFloor.get(0).direction());
 
 Map<Boolean, List<Call>> callsInLiftDirection = callsInLiftDirection(callsAtThisFloor, liftDirection);
 
 remainingRequests.addAll(callsInLiftDirection.get(true).stream().map(Call::destinationFloor).toList());
 
 // FIXME side effect : update the calls map ?
 calls.put(lift.floor(), callsInLiftDirection.get(false));
 }
 return lift.closeDoors(remainingRequests);
 }
 
 private Lift handleLiftWithClosedDoors(Lift lift, List<Call> callsAtThisFloor) {
 if (!lift.requests().isEmpty()) {
 return handleLiftWithRequests(lift, callsAtThisFloor);
 } else {
 if (!callsAtThisFloor.isEmpty()) {
 return lift.openDoors(lift.requests());
 } else {
 return handleWaitingCallsIfAny(lift);
 }
 }
 }
 
 private Lift handleWaitingCallsIfAny(Lift lift) {
 // find the closest call if any
 int floor = lift.floor();
 for (int i = 1; i < Math.max(lift.floor(), floors.size() - 1 - lift.floor()); i++) {
 if (isNotEmpty(calls.get(floor + i))) {
 return lift.moveUp();
 }
 if (isNotEmpty(calls.get(floor - i))) {
 return lift.moveDown();
 }
 }
 return lift;
 }
 
 private Lift handleLiftWithRequests(Lift lift, List<Call> callsForFloor) {
 if (lift.hasRequestForFloor(lift.floor())) {
 return lift.openDoors(lift.filterRequests(lift.floor()));
 } else {
 Direction liftDirection = liftDirection(lift, lift.requests()).get();
 if (hasCallsInLiftDirection(liftDirection, callsForFloor)) {
 return lift.openDoors(lift.requests());
 } else {
 return liftDirection.equals(Direction.UP) ? lift.moveUp() : lift.moveDown();
 }
 }
 }
 
 private boolean hasCallsInLiftDirection(Direction liftDirection, List<Call> callsForFloor) {
 return callsForFloor.stream().anyMatch(call -> call.direction().equals(liftDirection));
 }
 
 private static Map<Boolean, List<Call>> callsInLiftDirection(List<Call> callsForFloor, Direction liftDirection) {
 return callsForFloor.stream()
 .collect(Collectors.partitioningBy(call -> call.direction().equals(liftDirection)));
 }
 
 public boolean stillMoving() {
 return calls.values().stream().anyMatch(Predicate.not(Collection::isEmpty))
 || lifts.stream().anyMatch(lift -> lift.doorsOpen() || !lift.requests().isEmpty());
 }
 
 private Optional<Direction> liftDirection(Lift lift, List<Integer> remainingRequests) {
 if (remainingRequests.isEmpty()) {
 return Optional.empty();
 } else {
 int destination = lift.closestDestination();
 return Optional.of(destination > lift.floor() ? Direction.UP : Direction.DOWN);
 }
 }
 
 private boolean isNotEmpty(Collection<?> collection) {
 return collection != null && !collection.isEmpty();
 }
 }


 public record Lift(String id, int floor, List<Integer> requests, boolean doorsOpen) {
 public boolean hasRequestForFloor(int floor) {
 return this.requests.contains(floor);
 }
 
 public Lift openDoors(List<Integer> requests) {
 return new Lift(id, floor, requests, true);
 }
 
 public Lift closeDoors(List<Integer> requests) {
 return new Lift(id, floor, requests, false);
 }
 
 public int closestDestination() {
 return requests
 .stream()
 .min(Comparator.comparingInt(i -> Math.abs(i - floor)))
 .orElse(floor);
 }
 
 public Lift moveUp() {
 return new Lift(id, floor + 1, requests, doorsOpen);
 }
 
 public Lift moveDown() {
 return new Lift(id, floor - 1, requests, doorsOpen);
 }
 
 public List<Integer> filterRequests(int unwantedFloor) {
 return requests.stream()
 .filter(request -> request != unwantedFloor)
 .collect(Collectors.toList());
 }
 }

 public enum Direction {
 UP, DOWN;
 }

 public record Call (int fromFloor, int destinationFloor, Direction direction) {
 }


 [1]: https://github.com/emilybache/Lift-Kata