import pytest
from elevator_traffic_analysis.building import Building
from elevator_traffic_analysis.elevator_algorithms.fcfs import FirstComeFirstServedLogic
from elevator_traffic_analysis.elevator_algorithms.sstf import ShortestSeekTimeFirstLogic
from elevator_traffic_analysis.elevator_algorithms.scan import ScanLogic
from elevator_traffic_analysis.elevator_algorithms.look import LookLogic


class TestFCFSLogic:
    """Tests for First Come First Served (FCFS) algorithm"""

    def test_fcfs_creation(self):
        """
        Given: A building with floors and elevators
        When: FCFS logic is created
        Then: The logic is properly initialized with building reference
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = FirstComeFirstServedLogic(building)
        assert logic.building is building
        assert repr(logic) == "FirstComeFirstServedLogic"

    def test_fcfs_no_passengers_no_target(self):
        """
        Given: A building with no passengers
        When: FCFS assigns elevator targets
        Then: No target is set for the elevator
        """
        building = Building(n_floors=5, n_elevators=1)
        logic = FirstComeFirstServedLogic(building)
        elevator = building.elevators[0]

        logic.assign_elevator_targets()

        assert elevator.target_floor is None

    def test_fcfs_onboard_passenger_priority(self):
        """
        Given: An elevator with an onboard passenger
        When: FCFS assigns elevator targets
        Then: The onboard passenger's destination is prioritized
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = FirstComeFirstServedLogic(building)
        elevator = building.elevators[0]

        # Add onboard passenger going to floor 5
        passenger = building.floors[0].create_passenger("P__1", building.floors[5], 0)
        building.floors[0].remove_waiting_passenger(passenger)
        passenger.embark(1)
        elevator.onboard_passengers.append(passenger)

        logic.assign_elevator_targets()

        assert elevator.target_floor == building.floors[5]

    def test_fcfs_picks_earliest_waiting_passenger(self):
        """
        Given: Multiple waiting passengers with different appearance times
        When: FCFS assigns elevator targets
        Then: The earliest waiting passenger's floor is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = FirstComeFirstServedLogic(building)
        elevator = building.elevators[0]

        # Add passengers at different times
        building.create_and_add_passenger("P__1", "F__2", "F__7", tick_appeared=10)
        building.create_and_add_passenger("P__2", "F__3", "F__8", tick_appeared=5)
        building.create_and_add_passenger("P__3", "F__4", "F__9", tick_appeared=15)

        logic.assign_elevator_targets()

        # Should pick floor 3 (P__2 appeared at tick 5, earliest)
        assert elevator.target_floor == building.floors[3]

    def test_fcfs_first_embarked_passenger_priority(self):
        """
        Given: Multiple onboard passengers who embarked at different times
        When: FCFS assigns elevator targets
        Then: The first embarked passenger's destination is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = FirstComeFirstServedLogic(building)
        elevator = building.elevators[0]

        # Add multiple onboard passengers
        p1 = building.floors[0].create_passenger("P__1", building.floors[7], 0)
        p2 = building.floors[0].create_passenger("P__2", building.floors[5], 0)
        p3 = building.floors[0].create_passenger("P__3", building.floors[9], 0)

        building.floors[0].remove_waiting_passenger(p1)
        building.floors[0].remove_waiting_passenger(p2)
        building.floors[0].remove_waiting_passenger(p3)

        p1.embark(10)
        p2.embark(5)
        p3.embark(15)

        elevator.onboard_passengers.extend([p1, p2, p3])

        logic.assign_elevator_targets()

        # Should pick p2's destination (embarked at tick 5)
        assert elevator.target_floor == building.floors[5]

    def test_fcfs_ignores_at_capacity(self):
        """
        Given: An elevator at capacity with waiting passengers
        When: FCFS assigns elevator targets
        Then: Only onboard passenger destinations are considered
        """
        building = Building(n_floors=10, n_elevators=1, elevator_capacity=2)
        logic = FirstComeFirstServedLogic(building)
        elevator = building.elevators[0]

        # Fill elevator to capacity
        p1 = building.floors[0].create_passenger("P__1", building.floors[5], 0)
        p2 = building.floors[0].create_passenger("P__2", building.floors[6], 0)
        building.floors[0].remove_waiting_passenger(p1)
        building.floors[0].remove_waiting_passenger(p2)
        p1.embark(1)
        p2.embark(2)
        elevator.onboard_passengers.extend([p1, p2])

        # Add waiting passenger
        building.create_and_add_passenger("P__3", "F__3", "F__8", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should target onboard passenger destination, not waiting passenger
        assert elevator.target_floor in [building.floors[5], building.floors[6]]

    def test_fcfs_clears_target_when_no_passengers(self):
        """
        Given: An elevator at target floor with no passengers
        When: FCFS assigns elevator targets
        Then: The target is cleared
        """
        building = Building(n_floors=5, n_elevators=1)
        logic = FirstComeFirstServedLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[3]
        elevator.target_floor = building.floors[3]

        logic.assign_elevator_targets()

        assert elevator.target_floor is None

    def test_fcfs_doesnt_update_while_moving(self):
        """
        Given: An elevator moving towards a target with new passengers appearing
        When: FCFS assigns elevator targets
        Then: The target remains unchanged
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = FirstComeFirstServedLogic(building)
        elevator = building.elevators[0]

        # Set target and start moving
        elevator.target_floor = building.floors[5]
        elevator.move_up()

        # Add new passenger
        building.create_and_add_passenger("P__1", "F__2", "F__7", tick_appeared=0)

        logic.assign_elevator_targets()

        # Target should remain floor 5
        assert elevator.target_floor == building.floors[5]


class TestSSTFLogic:
    """Tests for Shortest Seek Time First (SSTF) algorithm"""

    def test_sstf_creation(self):
        """
        Given: A building with floors and elevators
        When: SSTF logic is created
        Then: The logic is properly initialized with building reference
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ShortestSeekTimeFirstLogic(building)
        assert logic.building is building
        assert repr(logic) == "ShortestSeekTimeFirstLogic"

    def test_sstf_picks_closest_destination(self):
        """
        Given: Multiple passengers at different distances from elevator
        When: SSTF assigns elevator targets
        Then: The closest passenger's floor is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ShortestSeekTimeFirstLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]

        # Add passengers at different distances
        building.create_and_add_passenger("P__1", "F__2", "F__8", tick_appeared=0)  # Distance 3
        building.create_and_add_passenger("P__2", "F__7", "F__9", tick_appeared=0)  # Distance 2
        building.create_and_add_passenger("P__3", "F__9", "F__1", tick_appeared=0)  # Distance 4

        logic.assign_elevator_targets()

        # Should pick floor 7 (closest at distance 2)
        assert elevator.target_floor == building.floors[7]

    def test_sstf_onboard_passenger_closest(self):
        """
        Given: Multiple onboard passengers with different destination distances
        When: SSTF assigns elevator targets
        Then: The closest destination is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ShortestSeekTimeFirstLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]

        # Add onboard passengers
        p1 = building.floors[0].create_passenger("P__1", building.floors[9], 0)
        p2 = building.floors[0].create_passenger("P__2", building.floors[6], 0)
        p3 = building.floors[0].create_passenger("P__3", building.floors[3], 0)

        building.floors[0].remove_waiting_passenger(p1)
        building.floors[0].remove_waiting_passenger(p2)
        building.floors[0].remove_waiting_passenger(p3)

        p1.embark(1)
        p2.embark(1)
        p3.embark(1)
        elevator.onboard_passengers.extend([p1, p2, p3])

        logic.assign_elevator_targets()

        # Should pick floor 6 (distance 1)
        assert elevator.target_floor == building.floors[6]

    def test_sstf_can_switch_targets_while_moving(self):
        """
        Given: An elevator moving to a target when a closer passenger appears
        When: SSTF assigns elevator targets
        Then: The target switches to the closer floor
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ShortestSeekTimeFirstLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[0]
        elevator.target_floor = building.floors[8]
        elevator.move_up()

        # Add closer waiting passenger
        building.create_and_add_passenger("P__1", "F__3", "F__7", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should switch to floor 3 (closer than floor 8)
        assert elevator.target_floor == building.floors[3]

    def test_sstf_excludes_current_floor(self):
        """
        Given: Passengers at current floor and other floors
        When: SSTF assigns elevator targets
        Then: Current floor is excluded and next closest floor is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ShortestSeekTimeFirstLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]

        # Add passengers: one at current floor, one elsewhere
        building.create_and_add_passenger("P__1", "F__5", "F__8", tick_appeared=0)
        building.create_and_add_passenger("P__2", "F__7", "F__9", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should target floor 7 (closer than 8 and not current floor)
        assert elevator.target_floor == building.floors[7]

    def test_sstf_no_passengers_no_target(self):
        """
        Given: A building with no passengers
        When: SSTF assigns elevator targets
        Then: No target is set for the elevator
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ShortestSeekTimeFirstLogic(building)
        elevator = building.elevators[0]

        logic.assign_elevator_targets()

        assert elevator.target_floor is None

    def test_sstf_respects_capacity(self):
        """
        Given: An elevator at capacity with closer waiting passengers
        When: SSTF assigns elevator targets
        Then: Only onboard passenger destinations are considered
        """
        building = Building(n_floors=10, n_elevators=1, elevator_capacity=1)
        logic = ShortestSeekTimeFirstLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]

        # Fill elevator
        p1 = building.floors[0].create_passenger("P__1", building.floors[9], 0)
        building.floors[0].remove_waiting_passenger(p1)
        p1.embark(1)
        elevator.onboard_passengers.append(p1)

        # Add closer waiting passenger
        building.create_and_add_passenger("P__2", "F__6", "F__8", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should target floor 9 (onboard passenger), not floor 6
        assert elevator.target_floor == building.floors[9]


class TestScanLogic:
    """Tests for SCAN (Elevator) algorithm"""

    def test_scan_creation(self):
        """
        Given: A building with floors and elevators
        When: SCAN logic is created
        Then: The logic is properly initialized with building reference
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        assert logic.building is building
        assert repr(logic) == "ScanLogic"

    def test_scan_continues_direction_up(self):
        """
        Given: An elevator moving upward with an upward request
        When: SCAN assigns elevator targets
        Then: The elevator continues to target upward floors
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[3]

        # Add passenger going up
        p1 = building.floors[0].create_passenger("P__1", building.floors[7], 0)
        building.floors[0].remove_waiting_passenger(p1)
        p1.embark(1)
        elevator.onboard_passengers.append(p1)

        # Set elevator moving up
        elevator.target_floor = building.floors[7]
        elevator.move_up()
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        logic.assign_elevator_targets()

        # Should continue to floor 7 or beyond
        assert int(elevator.target_floor) >= 7

    def test_scan_continues_direction_down(self):
        """
        Given: An elevator moving downward with a downward request
        When: SCAN assigns elevator targets
        Then: The elevator continues to target downward floors
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[7]

        # Add passenger going down
        p1 = building.floors[8].create_passenger("P__1", building.floors[2], 0)
        building.floors[8].remove_waiting_passenger(p1)
        p1.embark(1)
        elevator.onboard_passengers.append(p1)

        # Set elevator moving down
        elevator.target_floor = building.floors[2]
        elevator.move_down()
        logic.elevator_directions[elevator.elevator_id] = 'DOWN'

        logic.assign_elevator_targets()

        # Should continue to floor 2 or below
        assert int(elevator.target_floor) <= 2

    def test_scan_goes_to_boundary_when_no_requests(self):
        """
        Given: An elevator with no requests in current direction
        When: SCAN assigns elevator targets
        Then: The elevator targets the boundary floor
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        logic.assign_elevator_targets()

        # Should go to top floor (9)
        assert elevator.target_floor == building.floors[9]

    def test_scan_reverses_at_top(self):
        """
        Given: An elevator at top boundary with downward requests
        When: SCAN assigns elevator targets
        Then: The elevator reverses direction and targets downward
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[9]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add passenger going down
        building.create_and_add_passenger("P__1", "F__5", "F__2", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should reverse and target floor 5
        assert elevator.target_floor == building.floors[5]

    def test_scan_reverses_at_bottom(self):
        """
        Given: An elevator at bottom boundary with upward requests
        When: SCAN assigns elevator targets
        Then: The elevator reverses direction and targets upward
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[0]
        logic.elevator_directions[elevator.elevator_id] = 'DOWN'

        # Add passenger going up
        building.create_and_add_passenger("P__1", "F__5", "F__8", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should reverse and target floor 5
        assert elevator.target_floor == building.floors[5]

    def test_scan_picks_furthest_in_direction(self):
        """
        Given: Multiple passengers in elevator's current direction
        When: SCAN assigns elevator targets
        Then: The furthest floor in that direction is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[2]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add multiple passengers going up
        building.create_and_add_passenger("P__1", "F__4", "F__6", tick_appeared=0)
        building.create_and_add_passenger("P__2", "F__6", "F__8", tick_appeared=0)
        building.create_and_add_passenger("P__3", "F__3", "F__5", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should pick furthest (floor 6)
        assert int(elevator.target_floor) >= 6

    def test_scan_ignores_opposite_direction_passengers(self):
        """
        Given: Passengers in both directions with elevator moving up
        When: SCAN assigns elevator targets
        Then: Only upward passengers are considered
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add passengers: one going up from above, one going down from below
        building.create_and_add_passenger("P__1", "F__2", "F__0", tick_appeared=0)  # Below, going down
        building.create_and_add_passenger("P__2", "F__7", "F__9", tick_appeared=0)  # Above, going up

        logic.assign_elevator_targets()

        # Should pick floor 7 (in current direction), not floor 2
        assert int(elevator.target_floor) >= 7

    def test_scan_default_direction_up(self):
        """
        Given: An elevator without a set direction and passengers in both directions
        When: SCAN assigns elevator targets
        Then: The elevator defaults to moving upward
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = ScanLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]

        # Add passengers both above and below
        building.create_and_add_passenger("P__1", "F__2", "F__0", tick_appeared=0)
        building.create_and_add_passenger("P__2", "F__7", "F__9", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should go up by default
        assert int(elevator.target_floor) >= 5


class TestLookLogic:
    """Tests for LOOK (C-LOOK variant) algorithm"""

    def test_look_creation(self):
        """
        Given: A building with floors and elevators
        When: LOOK logic is created
        Then: The logic is properly initialized with building reference
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        assert logic.building is building
        assert repr(logic) == "LookLogic"

    def test_look_continues_direction_up(self):
        """
        Given: An elevator moving upward with an upward request
        When: LOOK assigns elevator targets
        Then: The elevator continues to target upward floors
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[3]

        # Add passenger going up
        p1 = building.floors[0].create_passenger("P__1", building.floors[7], 0)
        building.floors[0].remove_waiting_passenger(p1)
        p1.embark(1)
        elevator.onboard_passengers.append(p1)

        # Set elevator moving up
        elevator.target_floor = building.floors[7]
        elevator.move_up()
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        logic.assign_elevator_targets()

        # Should continue to floor 7
        assert elevator.target_floor == building.floors[7]

    def test_look_reverses_immediately_when_no_requests(self):
        """
        Given: An elevator with no requests in current direction but requests behind
        When: LOOK assigns elevator targets
        Then: The elevator immediately reverses direction
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add passenger below current floor going down
        building.create_and_add_passenger("P__1", "F__2", "F__0", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should reverse immediately to floor 2
        assert elevator.target_floor == building.floors[2]

    def test_look_picks_closest_in_direction(self):
        """
        Given: Multiple passengers in elevator's current direction
        When: LOOK assigns elevator targets
        Then: The closest floor in that direction is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[2]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add multiple passengers going up
        building.create_and_add_passenger("P__1", "F__4", "F__6", tick_appeared=0)
        building.create_and_add_passenger("P__2", "F__7", "F__9", tick_appeared=0)
        building.create_and_add_passenger("P__3", "F__3", "F__5", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should pick closest (floor 3)
        assert elevator.target_floor == building.floors[3]

    def test_look_doesnt_go_to_boundary(self):
        """
        Given: An elevator with requests only in opposite direction
        When: LOOK assigns elevator targets
        Then: The elevator reverses without reaching boundary
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add passenger below going down
        building.create_and_add_passenger("P__1", "F__3", "F__1", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should reverse to floor 3, not go to top boundary first
        assert elevator.target_floor == building.floors[3]
        assert int(elevator.target_floor) < 9

    def test_look_serves_onboard_first(self):
        """
        Given: Multiple onboard passengers in current direction
        When: LOOK assigns elevator targets
        Then: The closest onboard destination is targeted
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[2]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add onboard passengers
        p1 = building.floors[0].create_passenger("P__1", building.floors[5], 0)
        p2 = building.floors[0].create_passenger("P__2", building.floors[7], 0)
        building.floors[0].remove_waiting_passenger(p1)
        building.floors[0].remove_waiting_passenger(p2)
        p1.embark(1)
        p2.embark(1)
        elevator.onboard_passengers.extend([p1, p2])

        logic.assign_elevator_targets()

        # Should target floor 5 (closest onboard destination)
        assert elevator.target_floor == building.floors[5]

    def test_look_reverses_direction_tracking(self):
        """
        Given: An elevator that needs to reverse direction
        When: LOOK assigns elevator targets
        Then: The direction tracking is updated accordingly
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[7]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add passenger below going down
        building.create_and_add_passenger("P__1", "F__3", "F__1", tick_appeared=0)

        logic.assign_elevator_targets()

        # Direction should be updated to DOWN
        assert logic.elevator_directions[elevator.elevator_id] == 'DOWN'

    def test_look_ignores_opposite_direction_passengers(self):
        """
        Given: Passengers in both directions with elevator moving up
        When: LOOK assigns elevator targets
        Then: Only upward passengers are considered
        """
        building = Building(n_floors=10, n_elevators=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Add passengers: one above going up, one below going down
        building.create_and_add_passenger("P__1", "F__2", "F__0", tick_appeared=0)  # Below, going down
        building.create_and_add_passenger("P__2", "F__7", "F__9", tick_appeared=0)  # Above, going up

        logic.assign_elevator_targets()

        # Should pick floor 7 (in current direction)
        assert elevator.target_floor == building.floors[7]

    def test_look_respects_capacity(self):
        """
        Given: An elevator at capacity with closer waiting passengers
        When: LOOK assigns elevator targets
        Then: Only onboard passenger destinations are considered
        """
        building = Building(n_floors=10, n_elevators=1, elevator_capacity=1)
        logic = LookLogic(building)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[3]
        logic.elevator_directions[elevator.elevator_id] = 'UP'

        # Fill elevator
        p1 = building.floors[0].create_passenger("P__1", building.floors[8], 0)
        building.floors[0].remove_waiting_passenger(p1)
        p1.embark(1)
        elevator.onboard_passengers.append(p1)

        # Add waiting passenger closer
        building.create_and_add_passenger("P__2", "F__5", "F__7", tick_appeared=0)

        logic.assign_elevator_targets()

        # Should target floor 8 (onboard), not floor 5 (waiting)
        assert elevator.target_floor == building.floors[8]


class TestAlgorithmComparison:
    """Integration tests comparing algorithm behaviors"""

    def test_all_algorithms_handle_simple_case(self):
        """
        Given: All algorithms with a single passenger
        When: Each algorithm assigns elevator targets
        Then: All algorithms set a target floor
        """
        algorithms_with_buildings = [
            (FirstComeFirstServedLogic, Building(n_floors=10, n_elevators=1)),
            (ShortestSeekTimeFirstLogic, Building(n_floors=10, n_elevators=1)),
            (ScanLogic, Building(n_floors=10, n_elevators=1)),
            (LookLogic, Building(n_floors=10, n_elevators=1))
        ]

        for AlgoClass, building in algorithms_with_buildings:
            # Add passenger at floor 2 (not ground floor where elevator starts)
            building.create_and_add_passenger("P__1", "F__2", "F__5", tick_appeared=0)
            logic = AlgoClass(building)
            elevator = building.elevators[0]
            elevator.target_floor = None

            logic.assign_elevator_targets()

            # All should set a target (either floor 2 to pick up or go somewhere)
            assert elevator.target_floor is not None

    def test_fcfs_and_sstf_clear_target_when_done(self):
        """
        Given: FCFS and SSTF elevators at target with no passengers
        When: Each algorithm assigns elevator targets
        Then: Both algorithms clear the target
        """
        # Only test FCFS and SSTF as SCAN/LOOK may set targets to boundaries
        algorithms_with_buildings = [
            (FirstComeFirstServedLogic, Building(n_floors=5, n_elevators=1)),
            (ShortestSeekTimeFirstLogic, Building(n_floors=5, n_elevators=1))
        ]

        for AlgoClass, building in algorithms_with_buildings:
            logic = AlgoClass(building)
            elevator = building.elevators[0]
            # Set elevator at target floor with no passengers
            elevator.current_floor = building.floors[3]
            elevator.target_floor = building.floors[3]

            logic.assign_elevator_targets()

            # FCFS and SSTF should clear target when no passengers and at target
            assert elevator.target_floor is None

    def test_fcfs_vs_sstf_different_choices(self):
        """
        Given: Same scenario with close/late and far/early passengers
        When: FCFS and SSTF assign elevator targets
        Then: FCFS picks earliest, SSTF picks closest
        """
        building_fcfs = Building(n_floors=10, n_elevators=1)
        building_sstf = Building(n_floors=10, n_elevators=1)

        # Setup: elevator at floor 5, passengers at different distances and times
        building_fcfs.elevators[0].current_floor = building_fcfs.floors[5]
        building_sstf.elevators[0].current_floor = building_sstf.floors[5]

        # Close passenger (distance 1) but appeared later (tick 10)
        building_fcfs.create_and_add_passenger("P__1", "F__6", "F__8", tick_appeared=10)
        building_sstf.create_and_add_passenger("P__1", "F__6", "F__8", tick_appeared=10)

        # Far passenger (distance 5) but appeared earlier (tick 5)
        building_fcfs.create_and_add_passenger("P__2", "F__0", "F__3", tick_appeared=5)
        building_sstf.create_and_add_passenger("P__2", "F__0", "F__3", tick_appeared=5)

        fcfs_logic = FirstComeFirstServedLogic(building_fcfs)
        sstf_logic = ShortestSeekTimeFirstLogic(building_sstf)

        fcfs_logic.assign_elevator_targets()
        sstf_logic.assign_elevator_targets()

        # FCFS should pick floor 0 (earlier time)
        assert building_fcfs.elevators[0].target_floor == building_fcfs.floors[0]

        # SSTF should pick floor 6 (closer distance)
        assert building_sstf.elevators[0].target_floor == building_sstf.floors[6]

    def test_scan_vs_look_boundary_behavior(self):
        """
        Given: Same scenario with no requests ahead but requests behind
        When: SCAN and LOOK assign elevator targets
        Then: SCAN goes to boundary, LOOK reverses immediately
        """
        building_scan = Building(n_floors=10, n_elevators=1)
        building_look = Building(n_floors=10, n_elevators=1)

        # Setup: elevator at floor 5, moving up, no passengers above
        building_scan.elevators[0].current_floor = building_scan.floors[5]
        building_look.elevators[0].current_floor = building_look.floors[5]

        scan_logic = ScanLogic(building_scan)
        look_logic = LookLogic(building_look)

        scan_logic.elevator_directions[building_scan.elevators[0].elevator_id] = 'UP'
        look_logic.elevator_directions[building_look.elevators[0].elevator_id] = 'UP'

        # Add passenger below going down
        building_scan.create_and_add_passenger("P__1", "F__2", "F__0", tick_appeared=0)
        building_look.create_and_add_passenger("P__1", "F__2", "F__0", tick_appeared=0)

        scan_logic.assign_elevator_targets()
        look_logic.assign_elevator_targets()

        # SCAN should go to top boundary (floor 9)
        assert building_scan.elevators[0].target_floor == building_scan.floors[9]

        # LOOK should reverse immediately to floor 2
        assert building_look.elevators[0].target_floor == building_look.floors[2]
