# services/external/tests/unit/test_services.py
"""
Unit tests for External Service Services
"""

import pytest
from datetime import datetime, timezone, timedelta
from unittest.mock import AsyncMock, patch
from uuid import uuid4

from app.services.weather_service import WeatherService
from app.services.traffic_service import TrafficService


@pytest.mark.asyncio
class TestWeatherService:
    """Test Weather Service business logic"""

    @pytest.fixture
    def weather_service(self):
        """Create weather service instance"""
        return WeatherService()

    async def test_get_current_weather_from_cache(self, weather_service):
        """Test getting current weather from cache"""
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_weather = AsyncMock()
            mock_weather.date = datetime.now(timezone.utc) - timedelta(minutes=30)  # Fresh data
            mock_weather.to_dict.return_value = {"temperature": 18.5, "city": "madrid"}
            mock_repository.get_current_weather.return_value = mock_weather
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                result = await weather_service.get_current_weather("madrid")
                
                assert result is not None
                assert result["temperature"] == 18.5
                assert result["city"] == "madrid"

    async def test_get_current_weather_fetch_from_api(self, weather_service, mock_aemet_response):
        """Test getting current weather from API when cache is stale"""
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            # No cached data or stale data
            mock_repository.get_current_weather.return_value = None
            mock_stored = AsyncMock()
            mock_stored.to_dict.return_value = {"temperature": 20.0}
            mock_repository.create_weather_data.return_value = mock_stored
            
            # Mock AEMET client
            mock_client = AsyncMock()
            mock_client.get_current_weather.return_value = mock_aemet_response
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                weather_service.aemet_client = mock_client
                
                result = await weather_service.get_current_weather("madrid")
                
                assert result is not None
                assert result["temperature"] == 20.0
                mock_client.get_current_weather.assert_called_once()

    async def test_get_weather_forecast_from_cache(self, weather_service):
        """Test getting weather forecast from cache"""
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_forecast = [AsyncMock(), AsyncMock()]
            for item in mock_forecast:
                item.created_at = datetime.now(timezone.utc) - timedelta(hours=1)  # Fresh
                item.to_dict.return_value = {"temperature": 22.0}
            mock_repository.get_weather_forecast.return_value = mock_forecast
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                result = await weather_service.get_weather_forecast("madrid", 7)
                
                assert len(result) == 2
                assert all(item["temperature"] == 22.0 for item in result)

    async def test_get_weather_forecast_fetch_from_api(self, weather_service):
        """Test getting weather forecast from API when cache is stale"""
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            # No cached data
            mock_repository.get_weather_forecast.return_value = []
            mock_stored = AsyncMock()
            mock_stored.to_dict.return_value = {"temperature": 25.0}
            mock_repository.create_weather_data.return_value = mock_stored
            
            # Mock AEMET client
            mock_client = AsyncMock()
            mock_client.get_forecast.return_value = [
                {"forecast_date": datetime.now(), "temperature": 25.0}
            ]
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                weather_service.aemet_client = mock_client
                
                result = await weather_service.get_weather_forecast("madrid", 7)
                
                assert len(result) == 1
                assert result[0]["temperature"] == 25.0
                mock_client.get_forecast.assert_called_once()

    async def test_get_historical_weather(self, weather_service, sample_tenant_id):
        """Test getting historical weather data"""
        start_date = datetime.now(timezone.utc) - timedelta(days=7)
        end_date = datetime.now(timezone.utc)
        
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_historical = [AsyncMock(), AsyncMock()]
            for item in mock_historical:
                item.to_dict.return_value = {"temperature": 18.0}
            mock_repository.get_historical_weather.return_value = mock_historical
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                result = await weather_service.get_historical_weather(
                    "madrid", start_date, end_date, sample_tenant_id
                )
                
                assert len(result) == 2
                assert all(item["temperature"] == 18.0 for item in result)

    async def test_get_weather_stations(self, weather_service):
        """Test getting weather stations"""
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_stations = [AsyncMock()]
            mock_stations[0].to_dict.return_value = {"station_id": "TEST_001"}
            mock_repository.get_weather_stations.return_value = mock_stations
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                result = await weather_service.get_weather_stations("madrid")
                
                assert len(result) == 1
                assert result[0]["station_id"] == "TEST_001"

    async def test_trigger_weather_collection(self, weather_service, sample_tenant_id):
        """Test triggering weather data collection"""
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_job = AsyncMock()
            mock_job.id = uuid4()
            mock_job.to_dict.return_value = {"id": str(mock_job.id), "status": "pending"}
            mock_repository.create_weather_job.return_value = mock_job
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                result = await weather_service.trigger_weather_collection(
                    "madrid", "current", sample_tenant_id
                )
                
                assert result["status"] == "pending"
                mock_repository.create_weather_job.assert_called_once()

    async def test_process_weather_collection_job(self, weather_service):
        """Test processing weather collection job"""
        job_id = uuid4()
        
        with patch('app.services.weather_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            
            # Mock job
            mock_job = AsyncMock()
            mock_job.id = job_id
            mock_job.job_type = "current"
            mock_job.city = "madrid"
            
            mock_repository.get_weather_jobs.return_value = [mock_job]
            mock_repository.update_weather_job.return_value = True
            
            # Mock updated job after completion
            mock_updated_job = AsyncMock()
            mock_updated_job.to_dict.return_value = {"id": str(job_id), "status": "completed"}
            
            # Mock methods for different calls
            def mock_get_jobs_side_effect():
                return [mock_updated_job]  # Return completed job
            
            mock_repository.get_weather_jobs.side_effect = [
                [mock_job],  # First call returns pending job
                [mock_updated_job]  # Second call returns completed job
            ]
            
            with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository):
                with patch.object(weather_service, '_collect_current_weather', return_value=1):
                    result = await weather_service.process_weather_collection_job(job_id)
                    
                    assert result["status"] == "completed"

    async def test_map_weather_condition(self, weather_service):
        """Test weather condition mapping"""
        test_cases = [
            ("Soleado", "clear"),
            ("Nublado", "cloudy"),
            ("Parcialmente nublado", "partly_cloudy"),
            ("Lluvioso", "rainy"),
            ("Nevando", "snowy"),
            ("Tormenta", "stormy"),
            ("Desconocido", "unknown")
        ]
        
        for description, expected in test_cases:
            result = weather_service._map_weather_condition(description)
            assert result == expected


@pytest.mark.asyncio
class TestTrafficService:
    """Test Traffic Service business logic"""

    @pytest.fixture
    def traffic_service(self):
        """Create traffic service instance"""
        return TrafficService()

    async def test_get_current_traffic_from_cache(self, traffic_service):
        """Test getting current traffic from cache"""
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_traffic = [AsyncMock()]
            mock_traffic[0].date = datetime.now(timezone.utc) - timedelta(minutes=5)  # Fresh
            mock_traffic[0].to_dict.return_value = {"traffic_volume": 850}
            mock_repository.get_current_traffic.return_value = mock_traffic
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                result = await traffic_service.get_current_traffic("madrid")
                
                assert len(result) == 1
                assert result[0]["traffic_volume"] == 850

    async def test_get_current_traffic_fetch_from_api(self, traffic_service, mock_madrid_traffic_xml):
        """Test getting current traffic from API when cache is stale"""
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            # No cached data
            mock_repository.get_current_traffic.return_value = []
            mock_repository.bulk_create_traffic_data.return_value = 2
            
            # Mock clients
            mock_client = AsyncMock()
            mock_client.fetch_current_traffic_xml.return_value = mock_madrid_traffic_xml
            
            mock_processor = AsyncMock()
            mock_processor.process_current_traffic_xml.return_value = [
                {"traffic_volume": 850, "measurement_point_id": "PM_M30_001"},
                {"traffic_volume": 320, "measurement_point_id": "PM_URB_002"}
            ]
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                traffic_service.madrid_client = mock_client
                traffic_service.madrid_processor = mock_processor
                
                result = await traffic_service.get_current_traffic("madrid")
                
                assert len(result) == 2
                assert result[0]["traffic_volume"] == 850
                mock_client.fetch_current_traffic_xml.assert_called_once()

    async def test_get_historical_traffic(self, traffic_service, sample_tenant_id):
        """Test getting historical traffic data"""
        start_date = datetime.now(timezone.utc) - timedelta(days=7)
        end_date = datetime.now(timezone.utc)
        
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_historical = [AsyncMock(), AsyncMock()]
            for item in mock_historical:
                item.to_dict.return_value = {"traffic_volume": 500}
            mock_repository.get_historical_traffic.return_value = mock_historical
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                result = await traffic_service.get_historical_traffic(
                    "madrid", start_date, end_date, tenant_id=sample_tenant_id
                )
                
                assert len(result) == 2
                assert all(item["traffic_volume"] == 500 for item in result)

    async def test_get_measurement_points(self, traffic_service):
        """Test getting measurement points"""
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_points = [AsyncMock()]
            mock_points[0].to_dict.return_value = {"point_id": "PM_TEST_001"}
            mock_repository.get_measurement_points.return_value = mock_points
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                result = await traffic_service.get_measurement_points("madrid")
                
                assert len(result) == 1
                assert result[0]["point_id"] == "PM_TEST_001"

    async def test_get_traffic_analytics(self, traffic_service):
        """Test getting traffic analytics"""
        start_date = datetime.now(timezone.utc) - timedelta(days=30)
        end_date = datetime.now(timezone.utc)
        
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_analytics = {
                "total_measurements": 1000,
                "average_volume": 650.5,
                "peak_hour": "08:00"
            }
            mock_repository.get_traffic_analytics.return_value = mock_analytics
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                result = await traffic_service.get_traffic_analytics(
                    "madrid", start_date, end_date
                )
                
                assert result["total_measurements"] == 1000
                assert result["average_volume"] == 650.5
                assert "generated_at" in result

    async def test_trigger_traffic_collection(self, traffic_service, sample_tenant_id):
        """Test triggering traffic data collection"""
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_job = AsyncMock()
            mock_job.id = uuid4()
            mock_job.to_dict.return_value = {"id": str(mock_job.id), "status": "pending"}
            mock_repository.create_traffic_job.return_value = mock_job
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                result = await traffic_service.trigger_traffic_collection(
                    "madrid", "current", user_id=sample_tenant_id
                )
                
                assert result["status"] == "pending"
                mock_repository.create_traffic_job.assert_called_once()

    async def test_process_traffic_collection_job(self, traffic_service):
        """Test processing traffic collection job"""
        job_id = uuid4()
        
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            
            # Mock job
            mock_job = AsyncMock()
            mock_job.id = job_id
            mock_job.job_type = "current"
            mock_job.city = "madrid"
            mock_job.location_pattern = None
            
            mock_repository.get_traffic_jobs.return_value = [mock_job]
            mock_repository.update_traffic_job.return_value = True
            
            # Mock updated job after completion
            mock_updated_job = AsyncMock()
            mock_updated_job.to_dict.return_value = {"id": str(job_id), "status": "completed"}
            
            mock_repository.get_traffic_jobs.side_effect = [
                [mock_job],  # First call returns pending job
                [mock_updated_job]  # Second call returns completed job
            ]
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                with patch.object(traffic_service, '_collect_current_traffic', return_value=125):
                    result = await traffic_service.process_traffic_collection_job(job_id)
                    
                    assert result["status"] == "completed"

    async def test_is_traffic_data_fresh(self, traffic_service):
        """Test traffic data freshness check"""
        from app.models.traffic import TrafficData
        
        # Fresh data (5 minutes old)
        fresh_data = [AsyncMock()]
        fresh_data[0].date = datetime.utcnow() - timedelta(minutes=5)
        
        result = traffic_service._is_traffic_data_fresh(fresh_data)
        assert result is True
        
        # Stale data (15 minutes old)
        stale_data = [AsyncMock()]
        stale_data[0].date = datetime.utcnow() - timedelta(minutes=15)
        
        result = traffic_service._is_traffic_data_fresh(stale_data)
        assert result is False
        
        # Empty data
        result = traffic_service._is_traffic_data_fresh([])
        assert result is False

    async def test_collect_current_traffic(self, traffic_service):
        """Test current traffic collection"""
        with patch('app.services.traffic_service.get_db_transaction') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            
            mock_repository = AsyncMock()
            mock_repository.bulk_create_traffic_data.return_value = 10
            
            with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository):
                with patch.object(traffic_service, '_fetch_current_traffic_from_api', return_value=[{} for _ in range(10)]):
                    result = await traffic_service._collect_current_traffic("madrid", None)
                    
                    assert result == 10