"""Tests for STAC engine abstraction layer.

Tests both rustac and stac-geoparquet engines to ensure consistent behavior.
"""

import tempfile
from pathlib import Path

import geopandas as gpd
import pytest

from earthcatalog.engines import (
    STACEngine,
    get_engine,
)
from earthcatalog.storage_backends import LocalStorage

# Sample STAC items for testing
SAMPLE_ITEMS = [
    {
        "type": "Feature",
        "id": "test-item-1",
        "stac_version": "1.0.0",
        "geometry": {"type": "Point", "coordinates": [-122.4, 37.8]},
        "bbox": [-122.5, 37.7, -122.3, 37.9],
        "properties": {"datetime": "2024-01-01T00:00:00Z"},
        "links": [{"rel": "self", "href": "https://example.com/item1.json"}],
        "assets": {"data": {"href": "https://example.com/data1.tif"}},
    },
    {
        "type": "Feature",
        "id": "test-item-2",
        "stac_version": "1.0.0",
        "geometry": {
            "type": "Polygon",
            "coordinates": [[[-122.0, 37.0], [-121.0, 37.0], [-121.0, 38.0], [-122.0, 38.0], [-122.0, 37.0]]],
        },
        "bbox": [-122.0, 37.0, -121.0, 38.0],
        "properties": {"datetime": "2024-06-15T12:00:00Z"},
        "links": [{"rel": "self", "href": "https://example.com/item2.json"}],
        "assets": {"data": {"href": "https://example.com/data2.tif"}},
    },
]


@pytest.fixture(params=["rustac", "stac-geoparquet"])
def engine(request) -> STACEngine:
    """Parameterized fixture that yields both engine types."""
    return get_engine(request.param)


class TestEngineFactory:
    """Tests for the engine factory function."""

    def test_get_rustac_engine(self):
        """Test getting rustac engine explicitly."""
        engine = get_engine("rustac")
        assert engine.name == "rustac"

    def test_get_stac_geoparquet_engine(self):
        """Test getting stac-geoparquet engine explicitly."""
        engine = get_engine("stac-geoparquet")
        assert engine.name == "stac-geoparquet"

    def test_get_auto_engine(self):
        """Test auto engine selection (should prefer rustac)."""
        engine = get_engine("auto")
        # Should return one of the available engines
        assert engine.name in ("rustac", "stac-geoparquet")

    def test_invalid_engine_type(self):
        """Test that invalid engine type raises ValueError."""
        with pytest.raises(ValueError, match="Unknown engine type"):
            get_engine("invalid-engine")  # type: ignore

    def test_engine_is_stac_engine(self):
        """Test that returned engines implement STACEngine."""
        engine = get_engine("rustac")
        assert isinstance(engine, STACEngine)


class TestItemsToGeoDataFrame:
    """Tests for converting STAC items to GeoDataFrame."""

    def test_single_item_conversion(self, engine: STACEngine):
        """Test converting a single STAC item."""
        gdf = engine.items_to_geodataframe([SAMPLE_ITEMS[0]])

        assert isinstance(gdf, gpd.GeoDataFrame)
        assert len(gdf) == 1
        assert "geometry" in gdf.columns
        assert gdf.iloc[0]["id"] == "test-item-1"

    def test_multiple_items_conversion(self, engine: STACEngine):
        """Test converting multiple STAC items."""
        gdf = engine.items_to_geodataframe(SAMPLE_ITEMS)

        assert isinstance(gdf, gpd.GeoDataFrame)
        assert len(gdf) == 2
        assert set(gdf["id"].tolist()) == {"test-item-1", "test-item-2"}

    def test_empty_items_list(self, engine: STACEngine):
        """Test converting empty list returns empty GeoDataFrame."""
        gdf = engine.items_to_geodataframe([])

        assert isinstance(gdf, gpd.GeoDataFrame)
        assert len(gdf) == 0

    def test_geometry_preserved(self, engine: STACEngine):
        """Test that geometry is correctly preserved."""
        gdf = engine.items_to_geodataframe([SAMPLE_ITEMS[0]])

        geom = gdf.iloc[0].geometry
        assert geom is not None
        assert geom.geom_type == "Point"


class TestGeoDataFrameToItems:
    """Tests for converting GeoDataFrame back to STAC items."""

    def test_roundtrip_conversion(self, engine: STACEngine):
        """Test that items survive a roundtrip conversion."""
        # Convert to GeoDataFrame
        gdf = engine.items_to_geodataframe(SAMPLE_ITEMS)

        # Convert back to items
        items = engine.geodataframe_to_items(gdf)

        assert len(items) == 2
        item_ids = {item["id"] for item in items}
        assert item_ids == {"test-item-1", "test-item-2"}

    def test_empty_geodataframe(self, engine: STACEngine):
        """Test converting empty GeoDataFrame returns empty list."""
        gdf = gpd.GeoDataFrame()
        items = engine.geodataframe_to_items(gdf)

        assert items == []

    def test_geometry_preserved_in_roundtrip(self, engine: STACEngine):
        """Test that geometry is preserved in roundtrip."""
        original = [SAMPLE_ITEMS[0]]
        gdf = engine.items_to_geodataframe(original)
        items = engine.geodataframe_to_items(gdf)

        assert len(items) == 1
        item = items[0]
        assert "geometry" in item
        assert item["geometry"]["type"] == "Point"


class TestGeoParquetIO:
    """Tests for GeoParquet read/write operations."""

    def test_write_and_read_geoparquet(self, engine: STACEngine):
        """Test writing and reading GeoParquet files."""
        with tempfile.TemporaryDirectory() as tmpdir:
            path = f"{tmpdir}/test.parquet"
            storage = LocalStorage(tmpdir)

            # Convert items to GeoDataFrame
            gdf = engine.items_to_geodataframe(SAMPLE_ITEMS)

            # Write to GeoParquet
            engine.write_geoparquet_sync(gdf, path, storage)

            # Verify file was created
            assert Path(path).exists()

            # Read back
            gdf_read = engine.read_geoparquet_sync(path, storage)

            # Verify data
            assert isinstance(gdf_read, gpd.GeoDataFrame)
            assert len(gdf_read) == 2

    def test_write_empty_geodataframe(self, engine: STACEngine):
        """Test that writing empty GeoDataFrame doesn't create file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            path = f"{tmpdir}/empty.parquet"
            storage = LocalStorage(tmpdir)

            gdf = gpd.GeoDataFrame()
            engine.write_geoparquet_sync(gdf, path, storage)

            # Empty GeoDataFrame should not create a file
            assert not Path(path).exists()

    def test_read_nonexistent_file(self, engine: STACEngine):
        """Test reading nonexistent file raises appropriate error."""
        with tempfile.TemporaryDirectory() as tmpdir:
            path = f"{tmpdir}/nonexistent.parquet"
            storage = LocalStorage(tmpdir)

            with pytest.raises((FileNotFoundError, IOError)):
                engine.read_geoparquet_sync(path, storage)


class TestEngineConsistency:
    """Tests to verify both engines produce consistent results."""

    def test_geodataframe_columns_present(self):
        """Test that both engines include essential columns."""
        rustac_engine = get_engine("rustac")
        sgp_engine = get_engine("stac-geoparquet")

        gdf_rustac = rustac_engine.items_to_geodataframe(SAMPLE_ITEMS)
        gdf_sgp = sgp_engine.items_to_geodataframe(SAMPLE_ITEMS)

        # Both should have geometry column
        assert "geometry" in gdf_rustac.columns
        assert "geometry" in gdf_sgp.columns

        # Both should have id column
        assert "id" in gdf_rustac.columns
        assert "id" in gdf_sgp.columns

    def test_item_ids_preserved(self):
        """Test that both engines preserve item IDs."""
        rustac_engine = get_engine("rustac")
        sgp_engine = get_engine("stac-geoparquet")

        gdf_rustac = rustac_engine.items_to_geodataframe(SAMPLE_ITEMS)
        gdf_sgp = sgp_engine.items_to_geodataframe(SAMPLE_ITEMS)

        ids_rustac = set(gdf_rustac["id"].tolist())
        ids_sgp = set(gdf_sgp["id"].tolist())

        expected_ids = {"test-item-1", "test-item-2"}
        assert ids_rustac == expected_ids
        assert ids_sgp == expected_ids

    def test_roundtrip_preserves_ids(self):
        """Test that roundtrip conversion preserves IDs in both engines."""
        rustac_engine = get_engine("rustac")
        sgp_engine = get_engine("stac-geoparquet")

        for engine in [rustac_engine, sgp_engine]:
            gdf = engine.items_to_geodataframe(SAMPLE_ITEMS)
            items = engine.geodataframe_to_items(gdf)

            item_ids = {item["id"] for item in items}
            assert item_ids == {"test-item-1", "test-item-2"}, f"Failed for {engine.name}"


class TestEnginePerformance:
    """Basic performance sanity tests."""

    def test_handles_larger_datasets(self, engine: STACEngine):
        """Test that engines can handle larger datasets."""
        # Create 100 items
        items = []
        for i in range(100):
            item = {
                "type": "Feature",
                "id": f"test-item-{i}",
                "stac_version": "1.0.0",
                "geometry": {"type": "Point", "coordinates": [-122.0 + (i * 0.01), 37.0 + (i * 0.01)]},
                "bbox": [-122.1, 36.9, -121.9, 37.1],
                "properties": {"datetime": "2024-01-01T00:00:00Z"},
                "links": [],
                "assets": {},
            }
            items.append(item)

        # Convert to GeoDataFrame
        gdf = engine.items_to_geodataframe(items)
        assert len(gdf) == 100

        # Convert back
        converted = engine.geodataframe_to_items(gdf)
        assert len(converted) == 100
