import pytest
from elevator_traffic_analysis.building import Building
from elevator_traffic_analysis.elevator import Elevator
from elevator_traffic_analysis.floor import Floor
from elevator_traffic_analysis.passenger import Passenger


class TestElevatorCreation:
    """Tests for Elevator creation and initialization"""

    def test_elevator_creation_basic(self):
        """
        Given: A building with specified configuration
        When: An elevator is created
        Then: The elevator has correct properties and starts at ground floor in IDLE state
        """
        building = Building(n_floors=10, n_elevators=1, elevator_capacity=8)
        elevator = building.elevators[0]

        assert elevator.elevator_id == "E__0"
        assert elevator.building is building
        assert elevator.capacity == 8
        assert elevator.current_floor == building.floors[0]
        assert elevator.target_floor is None
        assert len(elevator.onboard_passengers) == 0
        assert elevator.state == "IDLE"

    def test_elevator_initial_state(self):
        """
        Given: A new elevator is created
        When: The elevator is initialized
        Then: The elevator state is IDLE
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]
        assert elevator.state == "IDLE"

    def test_elevator_starts_at_ground_floor(self):
        """
        Given: A new elevator is created in a building
        When: The elevator is initialized
        Then: The elevator is positioned at ground floor (floor 0)
        """
        building = Building(n_floors=20, n_elevators=1)
        elevator = building.elevators[0]
        assert elevator.current_floor == building.floors[0]
        assert int(elevator.current_floor) == 0


class TestElevatorRepresentation:
    """Tests for Elevator string representation"""

    def test_repr_idle_empty(self):
        """
        Given: An idle elevator with no passengers
        When: The elevator's repr is called
        Then: The representation includes ID, floor, state, and passenger count
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=10)
        elevator = building.elevators[0]
        repr_str = repr(elevator)

        assert "Elevator" in repr_str
        assert "E__0" in repr_str
        assert "F__0" in repr_str
        assert "IDLE" in repr_str
        assert "0/10" in repr_str

    def test_repr_with_passengers(self):
        """
        Given: An elevator with onboard passengers
        When: The elevator's repr is called
        Then: The representation includes the passenger count
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=5)
        elevator = building.elevators[0]

        # Add passengers
        for i in range(3):
            passenger = Passenger(f"P__{i}", building.floors[0], building.floors[3], 0)
            elevator.onboard_passengers.append(passenger)

        repr_str = repr(elevator)
        assert "3/5" in repr_str


class TestElevatorToDictSerialization:
    """Tests for Elevator to_dict method"""

    def test_to_dict_structure(self):
        """
        Given: An elevator instance
        When: to_dict is called
        Then: A dictionary with correct structure and elevator properties is returned
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=8)
        elevator = building.elevators[0]
        elevator_dict = elevator.to_dict()

        assert elevator_dict['type'] == 'Elevator'
        assert elevator_dict['id'] == 'E__0'
        assert elevator_dict['capacity'] == 8
        assert elevator_dict['current_floor'] == 'F__0'
        assert elevator_dict['target_floor'] is None
        assert elevator_dict['state'] == 'IDLE'
        assert elevator_dict['onboard_passengers_count'] == 0
        assert len(elevator_dict['onboard_passengers']) == 0

    def test_to_dict_with_target(self):
        """
        Given: An elevator with a target floor set
        When: to_dict is called
        Then: The dictionary includes the target floor ID
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[5]

        elevator_dict = elevator.to_dict()
        assert elevator_dict['target_floor'] == 'F__5'

    def test_to_dict_with_passengers(self):
        """
        Given: An elevator with onboard passengers
        When: to_dict is called
        Then: The dictionary includes passenger count and passenger data
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]
        passenger = Passenger("P__1", building.floors[0], building.floors[3], 0)
        elevator.onboard_passengers.append(passenger)

        elevator_dict = elevator.to_dict()
        assert elevator_dict['onboard_passengers_count'] == 1
        assert len(elevator_dict['onboard_passengers']) == 1
        assert elevator_dict['onboard_passengers'][0]['id'] == 'P__1'


class TestElevatorCapacity:
    """Tests for elevator capacity management"""

    def test_is_at_capacity_empty(self):
        """
        Given: An empty elevator
        When: is_at_capacity is called
        Then: False is returned
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=5)
        elevator = building.elevators[0]
        assert elevator.is_at_capacity() is False

    def test_is_at_capacity_partial(self):
        """
        Given: An elevator that is partially filled with passengers
        When: is_at_capacity is called
        Then: False is returned
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=5)
        elevator = building.elevators[0]

        for i in range(3):
            passenger = Passenger(f"P__{i}", building.floors[0], building.floors[2], 0)
            elevator.onboard_passengers.append(passenger)

        assert elevator.is_at_capacity() is False

    def test_is_at_capacity_full(self):
        """
        Given: An elevator that is completely filled to capacity
        When: is_at_capacity is called
        Then: True is returned
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=3)
        elevator = building.elevators[0]

        for i in range(3):
            passenger = Passenger(f"P__{i}", building.floors[0], building.floors[2], 0)
            elevator.onboard_passengers.append(passenger)

        assert elevator.is_at_capacity() is True

    def test_available_capacity_empty(self):
        """
        Given: An empty elevator with capacity 10
        When: available_capacity is called
        Then: The full capacity (10) is returned
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=10)
        elevator = building.elevators[0]
        assert elevator.available_capacity() == 10

    def test_available_capacity_partial(self):
        """
        Given: An elevator with 4 passengers out of 10 capacity
        When: available_capacity is called
        Then: The remaining capacity (6) is returned
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=10)
        elevator = building.elevators[0]

        for i in range(4):
            passenger = Passenger(f"P__{i}", building.floors[0], building.floors[2], 0)
            elevator.onboard_passengers.append(passenger)

        assert elevator.available_capacity() == 6

    def test_available_capacity_full(self):
        """
        Given: An elevator filled to capacity
        When: available_capacity is called
        Then: Zero is returned
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=5)
        elevator = building.elevators[0]

        for i in range(5):
            passenger = Passenger(f"P__{i}", building.floors[0], building.floors[2], 0)
            elevator.onboard_passengers.append(passenger)

        assert elevator.available_capacity() == 0


class TestElevatorMovement:
    """Tests for elevator movement"""

    def test_move_up(self):
        """
        Given: An elevator with a target floor above current position
        When: move_up is called
        Then: The elevator state changes to MOVING_UP
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[5]

        elevator.move_up()
        assert elevator.state == "MOVING_UP"

    def test_move_up_invalid(self):
        """
        Given: An elevator with a target floor below current position
        When: move_up is called
        Then: ValueError is raised
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        elevator.target_floor = building.floors[3]

        with pytest.raises(ValueError, match="Target floor must be above"):
            elevator.move_up()

    def test_move_down(self):
        """
        Given: An elevator with a target floor below current position
        When: move_down is called
        Then: The elevator state changes to MOVING_DOWN
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        elevator.target_floor = building.floors[2]

        elevator.move_down()
        assert elevator.state == "MOVING_DOWN"

    def test_move_down_invalid(self):
        """
        Given: An elevator with a target floor above current position
        When: move_down is called
        Then: ValueError is raised
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[2]
        elevator.target_floor = building.floors[5]

        with pytest.raises(ValueError, match="Target floor must be below"):
            elevator.move_down()

    def test_stop(self):
        """
        Given: An elevator that is moving
        When: stop is called
        Then: The elevator state changes to IDLE
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[5]
        elevator.move_up()

        assert elevator.state == "MOVING_UP"
        elevator.stop()
        assert elevator.state == "IDLE"

    def test_move_to_higher_floor(self):
        """
        Given: An elevator with a target floor above current position
        When: move is called
        Then: The elevator begins moving upward
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[5]

        elevator.move()
        assert elevator.state == "MOVING_UP"

    def test_move_to_lower_floor(self):
        """
        Given: An elevator with a target floor below current position
        When: move is called
        Then: The elevator begins moving downward
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[7]
        elevator.target_floor = building.floors[3]

        elevator.move()
        assert elevator.state == "MOVING_DOWN"

    def test_move_same_floor_raises_error(self):
        """
        Given: An elevator already at the target floor
        When: move is called
        Then: ValueError is raised
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[0]

        with pytest.raises(ValueError, match="Current floor is the same as target floor"):
            elevator.move()


class TestElevatorTickMovement:
    """Tests for tick_movement method"""

    def test_tick_movement_no_target(self):
        """
        Given: An idle elevator with no target floor
        When: tick_movement is called
        Then: The elevator remains idle at current floor
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

        elevator.tick_movement()

        assert elevator.state == "IDLE"
        assert elevator.current_floor == building.floors[0]

    def test_tick_movement_start_moving_up(self):
        """
        Given: An idle elevator with a target floor above
        When: tick_movement is called
        Then: The elevator changes state to MOVING_UP and begins moving
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[5]

        # First tick changes state to MOVING_UP but doesn't move yet
        elevator.tick_movement()

        assert elevator.state == "MOVING_UP"
        assert elevator.current_floor == building.floors[0]

        # Second tick actually moves
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[1]

    def test_tick_movement_start_moving_down(self):
        """
        Given: An idle elevator with a target floor below
        When: tick_movement is called
        Then: The elevator changes state to MOVING_DOWN and begins moving
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[7]
        elevator.target_floor = building.floors[3]

        # First tick changes state to MOVING_DOWN but doesn't move yet
        elevator.tick_movement()

        assert elevator.state == "MOVING_DOWN"
        assert elevator.current_floor == building.floors[7]

        # Second tick actually moves
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[6]

    def test_tick_movement_continue_up(self):
        """
        Given: An elevator that is moving up
        When: tick_movement is called multiple times
        Then: The elevator continues moving up one floor per tick
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[5]

        # Start moving (first tick changes state)
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[0]
        assert elevator.state == "MOVING_UP"

        # Second tick moves to floor 1
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[1]

        # Third tick continues moving
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[2]
        assert elevator.state == "MOVING_UP"

    def test_tick_movement_continue_down(self):
        """
        Given: An elevator that is moving down
        When: tick_movement is called multiple times
        Then: The elevator continues moving down one floor per tick
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[7]
        elevator.target_floor = building.floors[3]

        # Start moving (first tick changes state)
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[7]
        assert elevator.state == "MOVING_DOWN"

        # Second tick moves to floor 6
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[6]

        # Third tick continues moving
        elevator.tick_movement()
        assert elevator.current_floor == building.floors[5]
        assert elevator.state == "MOVING_DOWN"

    def test_tick_movement_reach_target_up(self):
        """
        Given: An elevator moving up toward its target floor
        When: The elevator reaches the target floor
        Then: The elevator stops and state changes to IDLE
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[3]

        # Move to target (first tick changes state, next 3 ticks move)
        for _ in range(4):
            elevator.tick_movement()

        assert elevator.current_floor == building.floors[3]
        assert elevator.state == "IDLE"

    def test_tick_movement_reach_target_down(self):
        """
        Given: An elevator moving down toward its target floor
        When: The elevator reaches the target floor
        Then: The elevator stops and state changes to IDLE
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.current_floor = building.floors[5]
        elevator.target_floor = building.floors[2]

        # Move to target (first tick changes state, next 3 ticks move)
        for _ in range(4):
            elevator.tick_movement()

        assert elevator.current_floor == building.floors[2]
        assert elevator.state == "IDLE"


class TestElevatorPassengerOperations:
    """Tests for passenger embark and disembark operations"""

    def test_embark_passengers_single(self):
        """
        Given: A passenger waiting on the elevator's current floor
        When: embark_passengers is called
        Then: The passenger boards the elevator
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]
        building.create_and_add_passenger("P__1", "F__0", "F__3", 0)

        elevator.embark_passengers(current_tick=1)

        assert len(elevator.onboard_passengers) == 1
        assert len(building.floors[0].waiting_passengers) == 0
        assert elevator.onboard_passengers[0].state == "EMBARKED"
        assert elevator.onboard_passengers[0].tick_embarked == 1

    def test_embark_passengers_multiple(self):
        """
        Given: Multiple passengers waiting on the elevator's current floor
        When: embark_passengers is called
        Then: All passengers board the elevator
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=10)
        elevator = building.elevators[0]

        for i in range(3):
            building.create_and_add_passenger(f"P__{i}", "F__0", "F__3", 0)

        elevator.embark_passengers(current_tick=2)

        assert len(elevator.onboard_passengers) == 3
        assert len(building.floors[0].waiting_passengers) == 0

    def test_embark_passengers_respects_capacity(self):
        """
        Given: More waiting passengers than elevator capacity
        When: embark_passengers is called
        Then: Only passengers up to capacity board, remaining passengers wait
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=2)
        elevator = building.elevators[0]

        for i in range(5):
            building.create_and_add_passenger(f"P__{i}", "F__0", "F__3", 0)

        elevator.embark_passengers(current_tick=1)

        assert len(elevator.onboard_passengers) == 2
        assert len(building.floors[0].waiting_passengers) == 3

    def test_disembark_passengers_at_destination(self):
        """
        Given: An elevator with passengers at their destination floor
        When: disembark_passengers is called
        Then: The passengers exit the elevator
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

        # Create and embark passenger
        passenger = building.floors[0].create_passenger("P__1", building.floors[3], 0)
        building.floors[0].remove_waiting_passenger(passenger)
        passenger.embark(1)
        elevator.onboard_passengers.append(passenger)

        # Move to destination
        elevator.current_floor = building.floors[3]

        # Disembark
        elevator.disembark_passengers(current_tick=5)

        assert len(elevator.onboard_passengers) == 0
        assert len(building.floors[3].dismbarked_passengers) == 1
        assert passenger.state == "DISEMBARKED"
        assert passenger.tick_disembarked == 5

    def test_disembark_passengers_not_at_destination(self):
        """
        Given: An elevator with passengers not at their destination
        When: disembark_passengers is called
        Then: The passengers remain onboard
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

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

        # Move to different floor
        elevator.current_floor = building.floors[2]

        # Try to disembark
        elevator.disembark_passengers(current_tick=5)

        # Passenger should still be onboard
        assert len(elevator.onboard_passengers) == 1
        assert passenger.state == "EMBARKED"

    def test_disembark_multiple_passengers(self):
        """
        Given: Multiple passengers at their destination floor
        When: disembark_passengers is called
        Then: All passengers at destination exit the elevator
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

        # Create passengers going to same floor
        for i in range(3):
            passenger = building.floors[0].create_passenger(f"P__{i}", building.floors[3], 0)
            building.floors[0].remove_waiting_passenger(passenger)
            passenger.embark(1)
            elevator.onboard_passengers.append(passenger)

        # Move to destination
        elevator.current_floor = building.floors[3]
        elevator.disembark_passengers(current_tick=5)

        assert len(elevator.onboard_passengers) == 0
        assert len(building.floors[3].dismbarked_passengers) == 3

    def test_embark_waiting_passengers_when_idle(self):
        """
        Given: An idle elevator with waiting passengers on current floor
        When: embark_waiting_passengers is called
        Then: Passengers board the elevator
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]
        building.create_and_add_passenger("P__1", "F__0", "F__3", 0)

        # Should embark when idle
        elevator.embark_waiting_passengers(current_tick=1)
        assert len(elevator.onboard_passengers) == 1

    def test_embark_waiting_passengers_when_moving(self):
        """
        Given: A moving elevator with waiting passengers on current floor
        When: embark_waiting_passengers is called
        Then: Passengers do not board (elevator must be idle)
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[3]
        elevator.move_up()

        building.create_and_add_passenger("P__1", "F__0", "F__3", 0)

        # Should not embark when moving
        elevator.embark_waiting_passengers(current_tick=1)
        assert len(elevator.onboard_passengers) == 0

    def test_disembark_arrived_passengers(self):
        """
        Given: An elevator with passengers at their destination
        When: disembark_arrived_passengers is called
        Then: The passengers exit at their destination floor
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

        passenger = building.floors[0].create_passenger("P__1", building.floors[0], 0)
        building.floors[0].remove_waiting_passenger(passenger)
        passenger.embark(1)
        elevator.onboard_passengers.append(passenger)

        elevator.disembark_arrived_passengers(current_tick=2)

        assert len(elevator.onboard_passengers) == 0
        assert len(building.floors[0].dismbarked_passengers) == 1


class TestElevatorIntegration:
    """Integration tests for Elevator"""

    def test_complete_journey(self):
        """
        Given: A passenger waiting to travel from floor 0 to floor 5
        When: The passenger embarks, elevator moves, and passenger disembarks
        Then: The complete journey is successful
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]

        # Create passenger
        building.create_and_add_passenger("P__1", "F__0", "F__5", 0)

        # Embark
        elevator.embark_passengers(current_tick=1)
        assert len(elevator.onboard_passengers) == 1

        # Move to destination (1 tick to change state + 5 ticks to move 5 floors)
        elevator.target_floor = building.floors[5]
        for _ in range(6):
            elevator.tick_movement()

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

        # Disembark
        elevator.disembark_passengers(current_tick=10)
        assert len(elevator.onboard_passengers) == 0
        assert len(building.floors[5].dismbarked_passengers) == 1

    def test_multiple_stops(self):
        """
        Given: Multiple passengers going to different floors
        When: The elevator transports all passengers
        Then: The elevator makes multiple stops and delivers all passengers
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]

        # Create passengers going to different floors
        p1 = building.floors[0].create_passenger("P__1", building.floors[3], 0)
        p2 = building.floors[0].create_passenger("P__2", building.floors[5], 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])

        # Move to floor 3 (1 tick to change state + 3 ticks to move)
        elevator.target_floor = building.floors[3]
        for _ in range(4):
            elevator.tick_movement()

        # Disembark first passenger
        elevator.disembark_passengers(current_tick=5)
        assert len(elevator.onboard_passengers) == 1

        # Move to floor 5 (1 tick to change state + 2 ticks to move)
        elevator.target_floor = building.floors[5]
        for _ in range(3):
            elevator.tick_movement()

        # Disembark second passenger
        elevator.disembark_passengers(current_tick=10)
        assert len(elevator.onboard_passengers) == 0
