import pytest
import os
import tempfile
import yaml
from elevator_traffic_analysis.scenario import Scenario
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 TestScenarioCreation:
    """Tests for Scenario creation and initialization"""

    def test_scenario_creation_from_valid_config(self):
        """
        Given: A valid scenario configuration file
        When: A Scenario object is created
        Then: The scenario is initialized with correct building and logic
        """
        config = {
            'n_floors': 5,
            'n_elevators': 2,
            'elevator_capacity': 10,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            assert scenario.building is not None
            assert len(scenario.building.floors) == 5
            assert len(scenario.building.elevators) == 2
            assert scenario.building.elevator_capacity == 10
            assert isinstance(scenario.logic, FirstComeFirstServedLogic)
            assert scenario.current_tick == 0
        finally:
            os.unlink(config_file)

    def test_scenario_fcfs_logic(self):
        """
        Given: A config specifying first_come_first_served logic
        When: A Scenario is created
        Then: The scenario uses FirstComeFirstServedLogic
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [{'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            assert isinstance(scenario.logic, FirstComeFirstServedLogic)
        finally:
            os.unlink(config_file)

    def test_scenario_sstf_logic(self):
        """
        Given: A config specifying shortest_seek_time_first logic
        When: A Scenario is created
        Then: The scenario uses ShortestSeekTimeFirstLogic
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'shortest_seek_time_first',
            'passenger_schedule': {
                0: [{'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            assert isinstance(scenario.logic, ShortestSeekTimeFirstLogic)
        finally:
            os.unlink(config_file)

    def test_scenario_scan_logic(self):
        """
        Given: A config specifying scan logic
        When: A Scenario is created
        Then: The scenario uses ScanLogic
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'scan',
            'passenger_schedule': {
                0: [{'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            assert isinstance(scenario.logic, ScanLogic)
        finally:
            os.unlink(config_file)

    def test_scenario_look_logic(self):
        """
        Given: A config specifying look logic
        When: A Scenario is created
        Then: The scenario uses LookLogic
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'look',
            'passenger_schedule': {
                0: [{'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            assert isinstance(scenario.logic, LookLogic)
        finally:
            os.unlink(config_file)

    def test_scenario_unknown_logic_raises_error(self):
        """
        Given: A config with an unknown elevator logic algorithm
        When: A Scenario is created
        Then: ValueError is raised
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'unknown_algorithm',
            'passenger_schedule': {
                0: [{'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="Unknown elevator logic"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_scenario_default_elevator_capacity(self):
        """
        Given: A config without elevator_capacity specified
        When: A Scenario is created
        Then: The default capacity of 10 is used
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [{'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            assert scenario.building.elevator_capacity == 10
        finally:
            os.unlink(config_file)


class TestScenarioValidation:
    """Tests for scenario configuration validation"""

    def test_validation_missing_passenger_schedule(self):
        """
        Given: A config without passenger_schedule field
        When: A Scenario is created
        Then: ValueError is raised about missing passenger_schedule
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served'
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="Missing required field 'passenger_schedule'"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_empty_passenger_schedule(self):
        """
        Given: A config with empty passenger_schedule
        When: A Scenario is created
        Then: ValueError is raised about empty passenger_schedule
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {}
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="'passenger_schedule' cannot be empty"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_missing_passenger_id(self):
        """
        Given: A passenger entry without passenger_id field
        When: A Scenario is created
        Then: ValueError is raised about missing passenger_id
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="missing 'passenger_id' field"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_invalid_passenger_id_format(self):
        """
        Given: A passenger_id that doesn't start with 'P__'
        When: A Scenario is created
        Then: ValueError is raised about invalid format
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'INVALID', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="must start with 'P__'"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_duplicate_passenger_id(self):
        """
        Given: Two passengers with the same passenger_id
        When: A Scenario is created
        Then: ValueError is raised about duplicate passenger_id
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ],
                1: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__1', 'destination_floor': 'F__4'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="duplicate passenger_id 'P__0'"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_missing_appear_floor(self):
        """
        Given: A passenger entry without appear_floor field
        When: A Scenario is created
        Then: ValueError is raised about missing appear_floor
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="missing required field 'appear_floor'"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_invalid_appear_floor_format(self):
        """
        Given: An appear_floor that doesn't start with 'F__'
        When: A Scenario is created
        Then: ValueError is raised about invalid format
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'INVALID', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="appear_floor 'INVALID' must start with 'F__'"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_appear_floor_out_of_range(self):
        """
        Given: An appear_floor exceeding the building's floor count
        When: A Scenario is created
        Then: ValueError is raised about out of range floor
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__10', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="appear_floor 'F__10' is out of range"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_missing_destination_floor(self):
        """
        Given: A passenger entry without destination_floor field
        When: A Scenario is created
        Then: ValueError is raised about missing destination_floor
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="missing required field 'destination_floor'"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)

    def test_validation_destination_floor_out_of_range(self):
        """
        Given: A destination_floor exceeding the building's floor count
        When: A Scenario is created
        Then: ValueError is raised about out of range floor
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__10'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            with pytest.raises(ValueError, match="destination_floor 'F__10' is out of range"):
                Scenario(config_file)
        finally:
            os.unlink(config_file)


class TestScenarioRun:
    """Tests for scenario execution"""

    def test_run_simple_scenario(self):
        """
        Given: A valid scenario with one passenger
        When: The scenario is run
        Then: Results include analysis with total_ticks and total_passengers
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_capacity': 10,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            results = scenario.run(max_ticks=50)

            assert 'analysis' in results
            assert 'total_ticks' in results['analysis']
            assert 'total_passengers' in results['analysis']
            assert results['analysis']['total_passengers'] == 1
            assert results['analysis']['total_ticks'] > 0
        finally:
            os.unlink(config_file)

    def test_run_records_tick_states(self):
        """
        Given: A scenario is configured and run
        When: The simulation executes
        Then: State information is recorded for each tick
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__2'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            results = scenario.run(max_ticks=30)

            assert 'ticks' in results['analysis']
            assert len(results['analysis']['ticks']) > 0
            assert 0 in results['analysis']['ticks']
        finally:
            os.unlink(config_file)

    def test_run_completes_when_all_passengers_served(self):
        """
        Given: A scenario with passengers
        When: All passengers are delivered to their destinations
        Then: The simulation completes with elevators idle
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__2'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            results = scenario.run(max_ticks=100)

            # Check that passenger was delivered
            final_tick = results['analysis']['total_ticks']
            final_state = results['analysis']['ticks'][final_tick]

            # Elevator should be idle with no passengers
            elevator_state = final_state['elevators']['E__0']
            assert elevator_state['onboard_passengers_count'] == 0
            assert elevator_state['state'] == 'IDLE'
        finally:
            os.unlink(config_file)

    def test_run_multiple_passengers(self):
        """
        Given: A scenario with multiple passengers at different times
        When: The scenario is run
        Then: All passengers are processed successfully
        """
        config = {
            'n_floors': 10,
            'n_elevators': 2,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__5'},
                    {'passenger_id': 'P__1', 'appear_floor': 'F__1', 'destination_floor': 'F__8'}
                ],
                5: [
                    {'passenger_id': 'P__2', 'appear_floor': 'F__3', 'destination_floor': 'F__7'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            results = scenario.run(max_ticks=100)

            assert results['analysis']['total_passengers'] == 3
            assert results['analysis']['total_ticks'] >= 5
        finally:
            os.unlink(config_file)

    def test_run_respects_max_ticks(self):
        """
        Given: A scenario that would take many ticks to complete
        When: run is called with a low max_ticks limit
        Then: The simulation stops before exceeding max_ticks
        """
        config = {
            'n_floors': 100,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__99'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            results = scenario.run(max_ticks=10)

            assert results['analysis']['total_ticks'] < 10
        finally:
            os.unlink(config_file)


class TestScenarioSave:
    """Tests for saving scenario results"""

    def test_save_to_file(self):
        """
        Given: A scenario that has been run
        When: save is called with an output file path
        Then: Results are saved to the file in YAML format
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        output_file = tempfile.mktemp(suffix='.yaml')

        try:
            scenario = Scenario(config_file)
            scenario.run(max_ticks=50)
            scenario.save(output_file)

            assert os.path.exists(output_file)

            # Verify content
            with open(output_file, 'r') as f:
                saved_data = yaml.safe_load(f)

            assert 'scenario_config' in saved_data
            assert 'analysis' in saved_data
        finally:
            os.unlink(config_file)
            if os.path.exists(output_file):
                os.unlink(output_file)


class TestScenarioSimulationCompletion:
    """Tests for simulation completion detection"""

    def test_is_simulation_complete_with_waiting_passengers(self):
        """
        Given: Passengers are waiting on floors
        When: _is_simulation_complete is checked
        Then: False is returned (simulation not complete)
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            scenario.current_tick = 0
            scenario._add_scheduled_passengers(0)

            # Should not be complete with waiting passengers
            assert scenario._is_simulation_complete() is False
        finally:
            os.unlink(config_file)

    def test_is_simulation_complete_with_onboard_passengers(self):
        """
        Given: Passengers are onboard elevators
        When: _is_simulation_complete is checked
        Then: False is returned (simulation not complete)
        """
        config = {
            'n_floors': 5,
            'n_elevators': 1,
            'elevator_logic': 'first_come_first_served',
            'passenger_schedule': {
                0: [
                    {'passenger_id': 'P__0', 'appear_floor': 'F__0', 'destination_floor': 'F__3'}
                ]
            }
        }

        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            yaml.dump(config, f)
            config_file = f.name

        try:
            scenario = Scenario(config_file)
            scenario.current_tick = 5
            scenario._add_scheduled_passengers(0)
            scenario.building.embark_waiting_passengers(1)

            # Should not be complete with onboard passengers
            assert scenario._is_simulation_complete() is False
        finally:
            os.unlink(config_file)
