import pytest
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
from unittest.mock import patch, MagicMock
from pathlib import Path

from ecoenv.env_request import autenticateEE, get_environment_data
from ecoenv.sentinel import (
    chunk_gdf,
    generate_cache_key as sentinel_cache_key,
    generate_point_key,
    get_index_from_sentinel_batch,
    get_sentinel_properties,
    add_index,
)
from ecoenv.worldclim import (
    chunk_gdf as worldclim_chunk_gdf,
    generate_cache_key as worldclim_cache_key,
    add_worldclim_properties,
    get_average_temperature_and_precipation,
)


@pytest.fixture
def sample_gdf():
    """Create a sample GeoDataFrame for testing."""
    data = {
        'latitude': [10.0, 20.0, 30.0],
        'longitude': [50.0, 60.0, 70.0],
        'eventDate': ['2020-01-01', '2020-06-15', '2021-01-01'],
    }
    gdf = gpd.GeoDataFrame(
        data,
        geometry=[Point(50, 10), Point(60, 20), Point(70, 30)],
        crs='EPSG:4326'
    )
    return gdf


@pytest.fixture
def sample_gdf_with_dates():
    """Create a GeoDataFrame with proper date formatting."""
    data = {
        'latitude': [10.0, 20.0],
        'longitude': [50.0, 60.0],
        'eventDate': pd.to_datetime(['2020-01-01', '2020-06-15']),
    }
    gdf = gpd.GeoDataFrame(
        data,
        geometry=[Point(50, 10), Point(60, 20)],
        crs='EPSG:4326'
    )
    return gdf


class TestEnvRequest:
    """Tests for env_request.py"""

    @patch('ecoenv.env_request.ee')
    def test_autenticateEE(self, mock_ee):
        """Test authentication with Earth Engine."""
        autenticateEE("test-project")
        mock_ee.Authenticate.assert_called_once()
        mock_ee.Initialize.assert_called_once_with(project="test-project")

    @patch('ecoenv.env_request.get_sentinel_properties')
    @patch('ecoenv.env_request.get_average_temperature_and_precipation')
    def test_get_environment_data_all_true(self, mock_temp, mock_sentinel, sample_gdf):
        """Test get_environment_data with all parameters True."""
        mock_sentinel.return_value = sample_gdf
        mock_temp.return_value = sample_gdf
        
        result = get_environment_data(
            sample_gdf,
            ndvi=True,
            ndwi=True,
            temperature=True,
            precipitation=True
        )
        
        assert mock_sentinel.called
        assert mock_temp.called

    @patch('ecoenv.env_request.get_sentinel_properties')
    def test_get_environment_data_ndvi_only(self, mock_sentinel, sample_gdf):
        """Test get_environment_data with only NDVI."""
        mock_sentinel.return_value = sample_gdf
        
        result = get_environment_data(
            sample_gdf,
            ndvi=True,
            ndwi=False,
            temperature=False,
            precipitation=False
        )
        
        mock_sentinel.assert_called_once()

    def test_get_environment_data_all_false(self, sample_gdf):
        """Test that ValueError is raised when all parameters are False."""
        with pytest.raises(ValueError):
            get_environment_data(
                sample_gdf,
                ndvi=False,
                ndwi=False,
                temperature=False,
                precipitation=False
            )

    @patch('ecoenv.env_request.get_sentinel_properties')
    @patch('ecoenv.env_request.get_average_temperature_and_precipation')
    def test_get_environment_data_exception(self, mock_temp, mock_sentinel, sample_gdf):
        """Test exception handling in get_environment_data."""
        mock_sentinel.side_effect = Exception("API Error")
        
        result = get_environment_data(sample_gdf)
        
        assert isinstance(result, pd.DataFrame)
        assert result.empty


class TestSentinel:
    """Tests for sentinel.py"""

    def test_chunk_gdf(self, sample_gdf):
        """Test chunking of GeoDataFrame."""
        chunks = list(chunk_gdf(sample_gdf, size=1))
        assert len(chunks) == 3
        assert len(chunks[0]) == 1

    def test_chunk_gdf_size_larger_than_gdf(self, sample_gdf):
        """Test chunking when chunk size is larger than GeoDataFrame."""
        chunks = list(chunk_gdf(sample_gdf, size=10))
        assert len(chunks) == 1
        assert len(chunks[0]) == 3

    def test_generate_point_key(self, sample_gdf_with_dates):
        """Test generation of cache key for a point."""
        row = sample_gdf_with_dates.iloc[0]
        key = generate_point_key(row, ndvi=True, ndwi=True)
        
        assert isinstance(key, str)
        assert len(key) == 32  # MD5 hash length

    def test_generate_point_key_consistency(self, sample_gdf_with_dates):
        """Test that same input generates same key."""
        row = sample_gdf_with_dates.iloc[0]
        key1 = generate_point_key(row, ndvi=True, ndwi=False)
        key2 = generate_point_key(row, ndvi=True, ndwi=False)
        
        assert key1 == key2

    def test_generate_point_key_different_params(self, sample_gdf_with_dates):
        """Test that different parameters generate different keys."""
        row = sample_gdf_with_dates.iloc[0]
        key1 = generate_point_key(row, ndvi=True, ndwi=False)
        key2 = generate_point_key(row, ndvi=False, ndwi=True)
        
        assert key1 != key2

    @patch('ecoenv.sentinel.ee')
    def test_add_index(self, mock_ee):
        """Test add_index function."""
        mock_img = MagicMock()
        mock_ndvi = MagicMock()
        mock_ndwi = MagicMock()
        
        mock_img.normalizedDifference.side_effect = [mock_ndvi, mock_ndwi]
        mock_ndvi.rename.return_value = mock_ndvi
        mock_ndwi.rename.return_value = mock_ndwi
        mock_img.addBands.return_value = mock_img
        
        result = add_index(mock_img)
        
        assert mock_img.normalizedDifference.called
        assert mock_img.addBands.called

    def test_sentinel_cache_key_generation(self, sample_gdf_with_dates):
        """Test cache key generation for sentinel data."""
        key = sentinel_cache_key(sample_gdf_with_dates, ndvi=True, ndwi=True)
        
        assert isinstance(key, str)
        assert len(key) == 32

    def test_sentinel_cache_key_different_dates(self, sample_gdf_with_dates):
        """Test that different dates generate different cache keys."""
        gdf2 = sample_gdf_with_dates.copy()
        gdf2['eventDate'] = pd.to_datetime(['2019-01-01', '2019-06-15'])
        
        key1 = sentinel_cache_key(sample_gdf_with_dates, ndvi=True, ndwi=True)
        key2 = sentinel_cache_key(gdf2, ndvi=True, ndwi=True)
        
        assert key1 != key2

    def test_get_sentinel_properties_ndvi_ndwi_both_false(self, sample_gdf_with_dates):
        """Test ValueError when both NDVI and NDWI are False."""
        with pytest.raises(ValueError):
            get_sentinel_properties(sample_gdf_with_dates, ndvi=False, ndwi=False)

    @patch('ecoenv.sentinel.get_index_from_sentinel_batch')
    def test_get_sentinel_properties_single_batch(self, mock_batch, sample_gdf_with_dates):
        """Test get_sentinel_properties with single batch."""
        mock_batch.return_value = sample_gdf_with_dates
        
        result = get_sentinel_properties(
            sample_gdf_with_dates,
            batch_size=150,
            ndvi=True,
            ndwi=True
        )
        
        assert isinstance(result, pd.DataFrame)


class TestWorldclim:
    """Tests for worldclim.py"""

    def test_worldclim_chunk_gdf(self, sample_gdf):
        """Test chunking of worldclim GeoDataFrame."""
        chunks = list(worldclim_chunk_gdf(sample_gdf, size=2))
        assert len(chunks) == 2

    def test_worldclim_generate_cache_key_with_geodataframe(self, sample_gdf_with_dates):
        """Test cache key generation for worldclim with GeoDataFrame."""
        key = worldclim_cache_key(sample_gdf_with_dates, ["tavg"], 1000)
        
        assert isinstance(key, str)
        assert len(key) == 32

    def test_worldclim_generate_cache_key_with_dataframe(self):
        """Test cache key generation with regular DataFrame."""
        data = {
            'latitude': [10.0, 20.0],
            'longitude': [50.0, 60.0],
            'eventDate': pd.to_datetime(['2020-01-01', '2020-06-15']),
        }
        df = pd.DataFrame(data)
        
        key = worldclim_cache_key(df, ["tavg"], 1000)
        
        assert isinstance(key, str)
        assert len(key) == 32

    def test_worldclim_generate_cache_key_invalid_type(self):
        """Test that invalid type raises TypeError."""
        with pytest.raises(TypeError):
            worldclim_cache_key([1, 2, 3], ["tavg"], 1000)

    def test_worldclim_generate_cache_key_empty_gdf(self, sample_gdf_with_dates):
        """Test that empty GeoDataFrame raises ValueError."""
        empty_gdf = sample_gdf_with_dates.iloc[0:0]
        
        with pytest.raises(ValueError):
            worldclim_cache_key(empty_gdf, ["tavg"], 1000)

    @patch('ecoenv.worldclim.ee')
    def test_add_worldclim_properties(self, mock_ee, sample_gdf_with_dates):
        """Test adding worldclim properties to GeoDataFrame."""
        mock_img = MagicMock()
        mock_fc = MagicMock()
        mock_ee.FeatureCollection.return_value = mock_fc
        
        mock_sampled = MagicMock()
        mock_img.reduceRegions.return_value = mock_sampled
        mock_sampled.getInfo.return_value = {
            "features": [
                {
                    "type": "Feature",
                    "geometry": {"type": "Point", "coordinates": [50, 10]},
                    "properties": {"tavg": 25.0}
                }
            ]
        }
        
        result = add_worldclim_properties(sample_gdf_with_dates, mock_img, 1000)
        
        assert isinstance(result, gpd.GeoDataFrame)

    def test_get_average_temperature_and_precipation_no_index(self, sample_gdf_with_dates):
        """Test ValueError when both temperature and precipitation are False."""
        with pytest.raises(ValueError):
            get_average_temperature_and_precipation(
                sample_gdf_with_dates,
                temperature=False,
                precipitation=False
            )

    @patch('ecoenv.worldclim.add_worldclim_properties')
    @patch('ecoenv.worldclim.ee')
    def test_get_average_temperature_small_batch(
        self,
        mock_ee,
        mock_add,
        sample_gdf_with_dates
    ):
        """Test temperature and precipitation retrieval for small batch."""
        mock_add.return_value = sample_gdf_with_dates
        mock_collection = MagicMock()
        mock_ee.ImageCollection.return_value = mock_collection
        mock_collection.select.return_value = mock_collection
        mock_collection.mean.return_value = mock_collection
        mock_collection.multiply.return_value = MagicMock()
        
        result = get_average_temperature_and_precipation(
            sample_gdf_with_dates,
            batch_size=5000,
            temperature=True,
            precipitation=True
        )
        
        assert isinstance(result, gpd.GeoDataFrame)

    @patch('ecoenv.worldclim.add_worldclim_properties')
    @patch('ecoenv.worldclim.ee')
    def test_get_average_temperature_large_batch(
        self,
        mock_ee,
        mock_add,
        sample_gdf_with_dates
    ):
        """Test temperature retrieval with multiple batches."""
        large_gdf = pd.concat(
            [sample_gdf_with_dates] * 3000,
            ignore_index=True
        )
        large_gdf = gpd.GeoDataFrame(
            large_gdf,
            geometry=sample_gdf_with_dates.geometry,
            crs='EPSG:4326'
        )
        
        mock_add.return_value = large_gdf.iloc[:1000]
        mock_collection = MagicMock()
        mock_ee.ImageCollection.return_value = mock_collection
        mock_collection.select.return_value = mock_collection
        mock_collection.mean.return_value = mock_collection
        mock_collection.multiply.return_value = MagicMock()
        
        result = get_average_temperature_and_precipation(
            large_gdf,
            batch_size=1000,
            temperature=True,
            precipitation=False
        )
        
        assert isinstance(result, gpd.GeoDataFrame)

    @patch('ecoenv.worldclim.ee')
    def test_get_average_temperature_only(self, mock_ee, sample_gdf_with_dates):
        """Test retrieving only temperature data."""
        mock_collection = MagicMock()
        mock_ee.ImageCollection.return_value = mock_collection
        mock_collection.select.return_value = mock_collection
        mock_collection.mean.return_value = mock_collection
        mock_collection.multiply.return_value = MagicMock()
        
        with patch('ecoenv.worldclim.add_worldclim_properties') as mock_add:
            mock_add.return_value = sample_gdf_with_dates
            
            result = get_average_temperature_and_precipation(
                sample_gdf_with_dates,
                temperature=True,
                precipitation=False
            )
            
            assert isinstance(result, gpd.GeoDataFrame)

    @patch('ecoenv.worldclim.ee')
    def test_get_average_precipitation_only(self, mock_ee, sample_gdf_with_dates):
        """Test retrieving only precipitation data."""
        mock_collection = MagicMock()
        mock_ee.ImageCollection.return_value = mock_collection
        mock_collection.select.return_value = mock_collection
        mock_collection.mean.return_value = mock_collection
        mock_collection.multiply.return_value = MagicMock()
        
        with patch('ecoenv.worldclim.add_worldclim_properties') as mock_add:
            mock_add.return_value = sample_gdf_with_dates
            
            result = get_average_temperature_and_precipation(
                sample_gdf_with_dates,
                temperature=False,
                precipitation=True
            )
            
            assert isinstance(result, gpd.GeoDataFrame)