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


class TestBuildingCreation:
    """Tests for Building creation and initialization"""

    def test_building_creation_basic(self):
        """
        Given: Building parameters are provided
        When: A Building object is created
        Then: The building has correct number of floors and elevators with specified capacity
        """
        building = Building(n_floors=5, n_elevators=2, elevator_capacity=10)
        assert len(building.floors) == 5
        assert len(building.elevators) == 2
        assert building.elevator_capacity == 10

    def test_building_creation_single_floor(self):
        """
        Given: Building parameters with single floor
        When: A Building object is created
        Then: The building has exactly one floor and one elevator
        """
        building = Building(n_floors=1, n_elevators=1, elevator_capacity=5)
        assert len(building.floors) == 1
        assert len(building.elevators) == 1

    def test_building_creation_many_floors(self):
        """
        Given: Building parameters with many floors
        When: A Building object is created
        Then: The building has correct number of floors and elevators
        """
        building = Building(n_floors=50, n_elevators=5, elevator_capacity=15)
        assert len(building.floors) == 50
        assert len(building.elevators) == 5
        assert building.elevator_capacity == 15

    def test_building_creation_default_capacity(self):
        """
        Given: Building parameters without explicit capacity
        When: A Building object is created
        Then: The building uses default elevator capacity
        """
        building = Building(n_floors=10, n_elevators=3)
        assert building.elevator_capacity == 10

    def test_building_floors_have_correct_ids(self):
        """
        Given: A building with multiple floors
        When: The building is created
        Then: Floors have sequential IDs starting from 0
        """
        building = Building(n_floors=5, n_elevators=1)
        for i, floor in enumerate(building.floors):
            assert floor.floor_id == f"F__{i}"

    def test_building_elevators_have_correct_ids(self):
        """
        Given: A building with multiple elevators
        When: The building is created
        Then: Elevators have sequential IDs starting from 0
        """
        building = Building(n_floors=5, n_elevators=3)
        for i, elevator in enumerate(building.elevators):
            assert elevator.elevator_id == f"E__{i}"

    def test_building_elevators_reference_building(self):
        """
        Given: A building with elevators
        When: The building is created
        Then: Each elevator has a reference to the building
        """
        building = Building(n_floors=5, n_elevators=2)
        for elevator in building.elevators:
            assert elevator.building is building

    def test_building_elevators_start_at_ground_floor(self):
        """
        Given: A building with multiple elevators
        When: The building is created
        Then: All elevators start at ground floor (floor 0)
        """
        building = Building(n_floors=10, n_elevators=3)
        for elevator in building.elevators:
            assert elevator.current_floor == building.floors[0]


class TestBuildingRepresentation:
    """Tests for Building string representation"""

    def test_repr(self):
        """
        Given: A building with specific parameters
        When: The building's repr is called
        Then: The repr contains building details
        """
        building = Building(n_floors=8, n_elevators=2, elevator_capacity=12)
        repr_str = repr(building)
        assert "Building" in repr_str
        assert "8 floors" in repr_str
        assert "2 elevators" in repr_str
        assert "elevator_capacity=12" in repr_str


class TestBuildingToDictSerialization:
    """Tests for Building to_dict method"""

    def test_to_dict_structure(self):
        """
        Given: A building object
        When: to_dict method is called
        Then: The dictionary has correct structure with type, capacity, floors, and elevators
        """
        building = Building(n_floors=3, n_elevators=1, elevator_capacity=5)
        building_dict = building.to_dict()

        assert building_dict['type'] == 'Building'
        assert building_dict['elevator_capacity'] == 5
        assert 'floors' in building_dict
        assert 'elevators' in building_dict

    def test_to_dict_floors(self):
        """
        Given: A building with multiple floors
        When: to_dict method is called
        Then: All floors are included in the dictionary
        """
        building = Building(n_floors=4, n_elevators=1)
        building_dict = building.to_dict()

        assert len(building_dict['floors']) == 4
        for i in range(4):
            assert f"F__{i}" in building_dict['floors']

    def test_to_dict_elevators(self):
        """
        Given: A building with multiple elevators
        When: to_dict method is called
        Then: All elevators are included in the dictionary
        """
        building = Building(n_floors=5, n_elevators=3)
        building_dict = building.to_dict()

        assert len(building_dict['elevators']) == 3
        for i in range(3):
            assert f"E__{i}" in building_dict['elevators']


class TestBuildingPassengerCreation:
    """Tests for creating and adding passengers"""

    def test_create_and_add_passenger_success(self):
        """
        Given: Valid passenger parameters
        When: create_and_add_passenger is called
        Then: The passenger is created and added to the origin floor
        """
        building = Building(n_floors=10, n_elevators=2)
        success = building.create_and_add_passenger(
            passenger_id="P__1",
            origin_floor_id="F__0",
            destination_floor_id="F__5",
            tick_appeared=0
        )

        assert success is True
        assert len(building.floors[0].waiting_passengers) == 1
        passenger = building.floors[0].waiting_passengers[0]
        assert passenger.passenger_id == "P__1"
        assert passenger.destination_floor == building.floors[5]

    def test_create_and_add_passenger_invalid_origin(self):
        """
        Given: Passenger parameters with invalid origin floor
        When: create_and_add_passenger is called
        Then: The operation returns False
        """
        building = Building(n_floors=5, n_elevators=1)
        success = building.create_and_add_passenger(
            passenger_id="P__1",
            origin_floor_id="F__10",
            destination_floor_id="F__2",
            tick_appeared=0
        )

        assert success is False

    def test_create_and_add_passenger_invalid_destination(self):
        """
        Given: Passenger parameters with invalid destination floor
        When: create_and_add_passenger is called
        Then: The operation returns False
        """
        building = Building(n_floors=5, n_elevators=1)
        success = building.create_and_add_passenger(
            passenger_id="P__1",
            origin_floor_id="F__0",
            destination_floor_id="F__20",
            tick_appeared=0
        )

        assert success is False

    def test_create_and_add_passenger_negative_floor(self):
        """
        Given: Passenger parameters with negative floor number
        When: create_and_add_passenger is called
        Then: The operation returns False
        """
        building = Building(n_floors=10, n_elevators=1)
        success = building.create_and_add_passenger(
            passenger_id="P__1",
            origin_floor_id="F__-1",
            destination_floor_id="F__5",
            tick_appeared=0
        )

        assert success is False

    def test_create_and_add_passenger_malformed_id(self):
        """
        Given: Passenger parameters with malformed floor ID
        When: create_and_add_passenger is called
        Then: The operation returns False
        """
        building = Building(n_floors=10, n_elevators=1)
        success = building.create_and_add_passenger(
            passenger_id="P__1",
            origin_floor_id="invalid",
            destination_floor_id="F__5",
            tick_appeared=0
        )

        assert success is False

    def test_create_multiple_passengers(self):
        """
        Given: Multiple passenger creation requests
        When: create_and_add_passenger is called for each
        Then: All passengers are successfully created and added
        """
        building = Building(n_floors=10, n_elevators=1)

        for i in range(5):
            success = building.create_and_add_passenger(
                passenger_id=f"P__{i}",
                origin_floor_id="F__0",
                destination_floor_id=f"F__{i+1}",
                tick_appeared=i
            )
            assert success is True

        assert len(building.floors[0].waiting_passengers) == 5


class TestBuildingAddWaitingPassenger:
    """Tests for adding waiting passengers"""

    def test_add_waiting_passenger(self):
        """
        Given: A passenger object and a building
        When: add_waiting_passenger is called
        Then: The passenger is added to their origin floor's waiting list
        """
        building = Building(n_floors=5, n_elevators=1)
        origin = building.floors[0]
        destination = building.floors[3]
        passenger = Passenger("P__1", origin, destination, 0)

        building.add_waiting_passenger(passenger)

        assert len(origin.waiting_passengers) == 1
        assert passenger in origin.waiting_passengers


class TestBuildingTickMovement:
    """Tests for elevator movement coordination"""

    def test_tick_movement_idle_elevators(self):
        """
        Given: A building with idle elevators
        When: tick_movement is called
        Then: All elevators remain idle at ground floor
        """
        building = Building(n_floors=5, n_elevators=2)

        # Initially all elevators should be idle
        building.tick_movement()

        for elevator in building.elevators:
            assert elevator.state == "IDLE"
            assert elevator.current_floor == building.floors[0]

    def test_tick_movement_with_target(self):
        """
        Given: An elevator with a target floor set
        When: tick_movement is called multiple times
        Then: The elevator moves towards and reaches the target
        """
        building = Building(n_floors=10, n_elevators=1)
        elevator = building.elevators[0]
        elevator.target_floor = building.floors[5]

        # First tick should start moving (state changes but doesn't move yet)
        building.tick_movement()
        assert elevator.state == "MOVING_UP"
        assert elevator.current_floor == building.floors[0]

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

        # Continue ticking until reached target
        for _ in range(4):
            building.tick_movement()

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


class TestBuildingPassengerHandling:
    """Tests for passenger embark/disembark coordination"""

    def test_embark_waiting_passengers(self):
        """
        Given: Waiting passengers at a floor with an elevator
        When: embark_waiting_passengers is called
        Then: Passengers board the elevator and are removed from waiting list
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=10)
        elevator = building.elevators[0]

        # Add waiting passengers
        building.create_and_add_passenger("P__1", "F__0", "F__3", 0)
        building.create_and_add_passenger("P__2", "F__0", "F__4", 0)

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

        # Embark passengers
        building.embark_waiting_passengers(current_tick=5)

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

    def test_disembark_arrived_passengers(self):
        """
        Given: An elevator with passengers at their destination floor
        When: disembark_arrived_passengers is called
        Then: Passengers leave the elevator and are marked as disembarked
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

        # Create passenger and manually embark
        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 elevator to destination
        elevator.current_floor = building.floors[3]

        # Disembark passengers
        building.disembark_arrived_passengers(current_tick=10)

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

    def test_handle_passengers(self):
        """
        Given: An elevator with passengers arriving and waiting passengers
        When: handle_passengers is called
        Then: Passengers disembark and new passengers embark
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

        # Add waiting passenger at ground floor
        building.create_and_add_passenger("P__1", "F__0", "F__2", 0)

        # Add onboard passenger arriving at ground floor
        passenger2 = building.floors[2].create_passenger("P__2", building.floors[0], 1)
        building.floors[2].remove_waiting_passenger(passenger2)
        passenger2.embark(2)
        elevator.onboard_passengers.append(passenger2)
        elevator.current_floor = building.floors[0]

        # Handle passengers
        building.handle_passengers(current_tick=5)

        # P__2 should disembark, P__1 should embark
        assert len(building.floors[0].dismbarked_passengers) == 1
        assert len(elevator.onboard_passengers) == 1
        assert elevator.onboard_passengers[0].passenger_id == "P__1"


class TestBuildingIntegration:
    """Integration tests for Building"""

    def test_complete_passenger_journey(self):
        """
        Given: A passenger created and added to a floor
        When: The passenger embarks, elevator moves, and passenger disembarks
        Then: The passenger completes the journey from origin to destination
        """
        building = Building(n_floors=5, n_elevators=1)
        elevator = building.elevators[0]

        # Create passenger
        building.create_and_add_passenger("P__1", "F__0", "F__3", 0)
        assert len(building.floors[0].waiting_passengers) == 1

        # Embark passenger
        building.embark_waiting_passengers(current_tick=1)
        assert len(elevator.onboard_passengers) == 1
        assert len(building.floors[0].waiting_passengers) == 0

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

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

        # Disembark passenger
        building.disembark_arrived_passengers(current_tick=5)
        assert len(elevator.onboard_passengers) == 0
        assert len(building.floors[3].dismbarked_passengers) == 1

    def test_multiple_elevators_multiple_passengers(self):
        """
        Given: Multiple elevators and multiple passengers on different floors
        When: embark_waiting_passengers is called
        Then: Passengers are distributed across available elevators
        """
        building = Building(n_floors=10, n_elevators=2, elevator_capacity=5)

        # Create passengers on different floors
        building.create_and_add_passenger("P__1", "F__0", "F__5", 0)
        building.create_and_add_passenger("P__2", "F__0", "F__7", 0)
        building.create_and_add_passenger("P__3", "F__2", "F__8", 0)

        # Embark passengers
        building.embark_waiting_passengers(current_tick=1)

        # Check that at least some passengers embarked
        total_onboard = sum(len(e.onboard_passengers) for e in building.elevators)
        assert total_onboard > 0

    def test_elevator_capacity_limit(self):
        """
        Given: An elevator with limited capacity and many waiting passengers
        When: embark_waiting_passengers is called
        Then: Only capacity-limited number of passengers embark
        """
        building = Building(n_floors=5, n_elevators=1, elevator_capacity=2)
        elevator = building.elevators[0]

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

        assert len(building.floors[0].waiting_passengers) == 5

        # Try to embark - should only take 2
        building.embark_waiting_passengers(current_tick=1)

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