"""Tests for geocoding tools.

Note: These tests require network access to Nominatim.
For CI/CD, consider mocking the HTTP requests.
"""

from unittest.mock import AsyncMock, patch

import pytest

from gis_mcp.config import Config, PeliasConfig
from gis_mcp.tools.geocoding import batch_geocode, geocode_address, reverse_geocode_coords


def _create_pelias_config():
    """Create a mock config with Pelias configured."""
    config = Config()
    config.pelias = PeliasConfig(
        base_url="http://localhost:4000",
        api_key="test-key",
        timeout=10.0
    )
    return config


class TestGeocode:
    """Tests for the geocode tool."""

    @pytest.mark.asyncio
    async def test_geocode_empty_address(self):
        """Test geocoding with empty address."""
        result = await geocode_address("")

        assert result["success"] is False
        assert "empty" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_geocode_whitespace_address(self):
        """Test geocoding with whitespace-only address."""
        result = await geocode_address("   ")

        assert result["success"] is False
        assert "empty" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_geocode_response_structure(self):
        """Test that geocode returns correct structure (mocked)."""
        mock_response = [{
            "lat": "48.8566",
            "lon": "2.3522",
            "display_name": "Paris, Île-de-France, France",
            "type": "city",
            "class": "place",
            "importance": 0.9,
            "osm_type": "relation",
            "osm_id": 7444,
            "boundingbox": ["48.815", "48.902", "2.224", "2.469"],
            "address": {
                "city": "Paris",
                "state": "Île-de-France",
                "country": "France"
            }
        }]

        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.return_value = mock_response

            result = await geocode_address("Paris")

            assert result["success"] is True
            assert result["data"]["lat"] == 48.8566
            assert result["data"]["lon"] == 2.3522
            assert "Paris" in result["data"]["display_name"]
            assert result["metadata"]["source"] == "nominatim"
            assert "confidence" in result["metadata"]

    @pytest.mark.asyncio
    async def test_geocode_no_results(self):
        """Test geocoding when no results found (mocked)."""
        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.return_value = []

            result = await geocode_address("xyznonexistentplace123")

            assert result["success"] is False
            assert "no results" in result["error"].lower()


class TestReverseGeocode:
    """Tests for the reverse geocode tool."""

    @pytest.mark.asyncio
    async def test_reverse_geocode_invalid_lat(self):
        """Test reverse geocoding with invalid latitude."""
        result = await reverse_geocode_coords(100, 2.3522)

        assert result["success"] is False
        assert "latitude" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_reverse_geocode_invalid_lon(self):
        """Test reverse geocoding with invalid longitude."""
        result = await reverse_geocode_coords(48.8566, 200)

        assert result["success"] is False
        assert "longitude" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_reverse_geocode_response_structure(self):
        """Test that reverse geocode returns correct structure (mocked)."""
        mock_response = {
            "display_name": "Eiffel Tower, Paris, France",
            "type": "attraction",
            "class": "tourism",
            "osm_type": "way",
            "osm_id": 123456,
            "boundingbox": ["48.857", "48.859", "2.293", "2.296"],
            "address": {
                "tourism": "Eiffel Tower",
                "road": "Avenue Gustave Eiffel",
                "city": "Paris",
                "state": "Île-de-France",
                "postcode": "75007",
                "country": "France",
                "country_code": "fr"
            }
        }

        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.return_value = mock_response

            result = await reverse_geocode_coords(48.8584, 2.2945)

            assert result["success"] is True
            assert "Eiffel Tower" in result["data"]["display_name"]
            assert result["data"]["structured"]["city"] == "Paris"
            assert result["metadata"]["source"] == "nominatim"

    @pytest.mark.asyncio
    async def test_reverse_geocode_error_response(self):
        """Test reverse geocoding when API returns error (mocked)."""
        mock_response = {
            "error": "Unable to geocode"
        }

        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.return_value = mock_response

            result = await reverse_geocode_coords(0, 0)  # Middle of ocean

            assert result["success"] is False


class TestValidation:
    """Tests for coordinate validation."""

    @pytest.mark.asyncio
    async def test_lat_boundary_min(self):
        """Test latitude at minimum boundary."""
        result = await reverse_geocode_coords(-90, 0)
        # Should not fail validation (may fail for other reasons)
        assert "latitude" not in (result.get("error") or "").lower()

    @pytest.mark.asyncio
    async def test_lat_boundary_max(self):
        """Test latitude at maximum boundary."""
        result = await reverse_geocode_coords(90, 0)
        assert "latitude" not in (result.get("error") or "").lower()

    @pytest.mark.asyncio
    async def test_lon_boundary_min(self):
        """Test longitude at minimum boundary."""
        result = await reverse_geocode_coords(0, -180)
        assert "longitude" not in (result.get("error") or "").lower()

    @pytest.mark.asyncio
    async def test_lon_boundary_max(self):
        """Test longitude at maximum boundary."""
        result = await reverse_geocode_coords(0, 180)
        assert "longitude" not in (result.get("error") or "").lower()


class TestBatchGeocode:
    """Tests for the batch_geocode function."""

    @pytest.mark.asyncio
    async def test_batch_geocode_empty_list(self):
        """Test batch geocoding with empty list."""
        result = await batch_geocode([])

        assert result["success"] is False
        assert "empty" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_batch_geocode_too_many_addresses(self):
        """Test batch geocoding with more than 10 addresses."""
        addresses = [f"Address {i}" for i in range(11)]
        result = await batch_geocode(addresses)

        assert result["success"] is False
        assert "too many" in result["error"].lower()
        assert "10" in result["error"]

    @pytest.mark.asyncio
    async def test_batch_geocode_valid_addresses(self):
        """Test batch geocoding with valid addresses (mocked)."""
        addresses = ["Paris, France", "London, UK", "Berlin, Germany"]

        mock_responses = [
            [{
                "lat": "48.8566",
                "lon": "2.3522",
                "display_name": "Paris, Île-de-France, France",
                "type": "city",
                "class": "place",
                "importance": 0.9,
                "osm_type": "relation",
                "osm_id": 7444,
                "boundingbox": ["48.815", "48.902", "2.224", "2.469"],
                "address": {"city": "Paris", "country": "France"}
            }],
            [{
                "lat": "51.5074",
                "lon": "-0.1278",
                "display_name": "London, England, UK",
                "type": "city",
                "class": "place",
                "importance": 0.9,
                "osm_type": "relation",
                "osm_id": 65606,
                "boundingbox": ["51.38", "51.69", "-0.35", "0.14"],
                "address": {"city": "London", "country": "United Kingdom"}
            }],
            [{
                "lat": "52.5200",
                "lon": "13.4050",
                "display_name": "Berlin, Germany",
                "type": "city",
                "class": "place",
                "importance": 0.9,
                "osm_type": "relation",
                "osm_id": 62422,
                "boundingbox": ["52.33", "52.68", "13.08", "13.76"],
                "address": {"city": "Berlin", "country": "Germany"}
            }]
        ]

        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.side_effect = mock_responses

            result = await batch_geocode(addresses)

            assert result["success"] is True
            assert result["data"]["summary"]["total"] == 3
            assert result["data"]["summary"]["successful"] == 3
            assert result["data"]["summary"]["failed"] == 0
            assert len(result["data"]["results"]) == 3

            # Check first result
            first_result = result["data"]["results"][0]
            assert first_result["index"] == 0
            assert first_result["address"] == "Paris, France"
            assert first_result["result"]["success"] is True
            assert first_result["result"]["data"]["lat"] == 48.8566

    @pytest.mark.asyncio
    async def test_batch_geocode_partial_failures(self):
        """Test batch geocoding with some addresses failing (mocked)."""
        addresses = ["Paris, France", "InvalidAddress123XYZ", "Berlin, Germany"]

        mock_responses = [
            [{
                "lat": "48.8566",
                "lon": "2.3522",
                "display_name": "Paris, Île-de-France, France",
                "type": "city",
                "class": "place",
                "importance": 0.9,
                "osm_type": "relation",
                "osm_id": 7444,
                "boundingbox": ["48.815", "48.902", "2.224", "2.469"],
                "address": {"city": "Paris", "country": "France"}
            }],
            [],  # No results for invalid address
            [{
                "lat": "52.5200",
                "lon": "13.4050",
                "display_name": "Berlin, Germany",
                "type": "city",
                "class": "place",
                "importance": 0.9,
                "osm_type": "relation",
                "osm_id": 62422,
                "boundingbox": ["52.33", "52.68", "13.08", "13.76"],
                "address": {"city": "Berlin", "country": "Germany"}
            }]
        ]

        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.side_effect = mock_responses

            result = await batch_geocode(addresses)

            assert result["success"] is True  # Overall success if at least one succeeded
            assert result["data"]["summary"]["total"] == 3
            assert result["data"]["summary"]["successful"] == 2
            assert result["data"]["summary"]["failed"] == 1

            # Check that the failed address has an error
            second_result = result["data"]["results"][1]
            assert second_result["index"] == 1
            assert second_result["address"] == "InvalidAddress123XYZ"
            assert second_result["result"]["success"] is False
            assert "no results" in second_result["result"]["error"].lower()

    @pytest.mark.asyncio
    async def test_batch_geocode_all_failures(self):
        """Test batch geocoding when all addresses fail (mocked)."""
        addresses = ["Invalid1", "Invalid2"]

        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.return_value = []  # No results for any address

            result = await batch_geocode(addresses)

            assert result["success"] is False
            assert "all addresses failed" in result["error"].lower()
            assert result["metadata"]["batch_size"] == 2

    @pytest.mark.asyncio
    async def test_batch_geocode_not_a_list(self):
        """Test batch geocoding with invalid input type."""
        result = await batch_geocode("not a list")  # type: ignore

        assert result["success"] is False
        assert "list" in result["error"].lower()


class TestPeliasGeocode:
    """Tests for Pelias geocoding provider."""

    @pytest.mark.asyncio
    async def test_pelias_geocode_invalid_provider(self):
        """Test geocoding with invalid provider name."""
        result = await geocode_address("Paris", provider="invalid")

        assert result["success"] is False
        assert "invalid provider" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_pelias_geocode_not_configured(self):
        """Test Pelias geocoding when not configured (falls back to Nominatim)."""
        mock_nominatim_response = [{
            "lat": "48.8566",
            "lon": "2.3522",
            "display_name": "Paris, France",
            "type": "city",
            "class": "place",
            "importance": 0.9,
            "osm_type": "relation",
            "osm_id": 7444,
            "address": {"city": "Paris", "country": "France"}
        }]

        with patch("gis_mcp.tools.geocoding._nominatim_request", new_callable=AsyncMock) as mock:
            mock.return_value = mock_nominatim_response

            result = await geocode_address("Paris", provider="pelias")

            assert result["success"] is True
            assert result["metadata"]["source"] == "nominatim"  # Fell back to Nominatim

    @pytest.mark.asyncio
    async def test_pelias_geocode_success(self):
        """Test successful Pelias geocoding (mocked)."""
        mock_pelias_response = {
            "geocoding": {"version": "0.2"},
            "type": "FeatureCollection",
            "features": [{
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [2.3522, 48.8566]
                },
                "properties": {
                    "gid": "whosonfirst:locality:101751119",
                    "layer": "locality",
                    "source": "whosonfirst",
                    "name": "Paris",
                    "label": "Paris, Île-de-France, France",
                    "confidence": 0.9,
                    "country": "France",
                    "region": "Île-de-France",
                    "locality": "Paris"
                }
            }]
        }

        with patch("gis_mcp.tools.geocoding.get_config") as mock_config:
            mock_config.return_value = _create_pelias_config()
            with patch("gis_mcp.tools.geocoding._pelias_geocode", new_callable=AsyncMock) as mock:
                mock.return_value = mock_pelias_response

                result = await geocode_address("Paris", provider="pelias")

                assert result["success"] is True
                assert result["data"]["lat"] == 48.8566
                assert result["data"]["lon"] == 2.3522
                assert "Paris" in result["data"]["display_name"]
                assert result["metadata"]["source"] == "pelias"
                assert result["metadata"]["confidence"] == 0.9

    @pytest.mark.asyncio
    async def test_pelias_geocode_no_results(self):
        """Test Pelias geocoding with no results (mocked)."""
        mock_pelias_response = {
            "geocoding": {"version": "0.2"},
            "type": "FeatureCollection",
            "features": []
        }

        with patch("gis_mcp.tools.geocoding.get_config") as mock_config:
            mock_config.return_value = _create_pelias_config()
            with patch("gis_mcp.tools.geocoding._pelias_geocode", new_callable=AsyncMock) as mock:
                mock.return_value = mock_pelias_response

                result = await geocode_address("nonexistent123", provider="pelias")

                assert result["success"] is False
                assert "no results" in result["error"].lower()
                assert result["metadata"]["source"] == "pelias"

    @pytest.mark.asyncio
    async def test_pelias_reverse_geocode_success(self):
        """Test successful Pelias reverse geocoding (mocked)."""
        mock_pelias_response = {
            "geocoding": {"version": "0.2"},
            "type": "FeatureCollection",
            "features": [{
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [2.2945, 48.8584]
                },
                "properties": {
                    "gid": "openstreetmap:venue:way/5013364",
                    "layer": "venue",
                    "source": "openstreetmap",
                    "name": "Eiffel Tower",
                    "label": "Eiffel Tower, Paris, France",
                    "street": "Avenue Gustave Eiffel",
                    "locality": "Paris",
                    "region": "Île-de-France",
                    "postalcode": "75007",
                    "country": "France"
                }
            }]
        }

        with patch("gis_mcp.tools.geocoding.get_config") as mock_config:
            mock_config.return_value = _create_pelias_config()
            with patch("gis_mcp.tools.geocoding._pelias_reverse", new_callable=AsyncMock) as mock:
                mock.return_value = mock_pelias_response

                result = await reverse_geocode_coords(48.8584, 2.2945, provider="pelias")

                assert result["success"] is True
                assert "Eiffel Tower" in result["data"]["display_name"]
                assert result["data"]["structured"]["city"] == "Paris"
                assert result["metadata"]["source"] == "pelias"

    @pytest.mark.asyncio
    async def test_pelias_reverse_geocode_no_results(self):
        """Test Pelias reverse geocoding with no results (mocked)."""
        mock_pelias_response = {
            "geocoding": {"version": "0.2"},
            "type": "FeatureCollection",
            "features": []
        }

        with patch("gis_mcp.tools.geocoding.get_config") as mock_config:
            mock_config.return_value = _create_pelias_config()
            with patch("gis_mcp.tools.geocoding._pelias_reverse", new_callable=AsyncMock) as mock:
                mock.return_value = mock_pelias_response

                result = await reverse_geocode_coords(0, 0, provider="pelias")

                assert result["success"] is False
                assert "no address found" in result["error"].lower()
                assert result["metadata"]["source"] == "pelias"

    @pytest.mark.asyncio
    async def test_pelias_reverse_geocode_invalid_provider(self):
        """Test reverse geocoding with invalid provider name."""
        result = await reverse_geocode_coords(48.8566, 2.3522, provider="invalid")

        assert result["success"] is False
        assert "invalid provider" in result["error"].lower()
