"""Unit tests for tool functions using mock context and cache."""
from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pandas as pd
import pytest

from ontario_data.cache import CacheManager, InvalidQueryError
from ontario_data.portals import PORTALS
from ontario_data.utils import ResourceNotCachedError


def make_mock_context(cache: CacheManager, ckan=None):
    """Create a mock MCP context with cache and optional CKAN client."""
    mock_ckan = ckan or AsyncMock()
    ctx = MagicMock()
    ctx.lifespan_context = {
        "cache": cache,
        "http_client": MagicMock(),
        "portal_configs": PORTALS,
        "portal_clients": {"ontario": mock_ckan},
    }
    ctx.report_progress = AsyncMock()
    return ctx


@pytest.fixture
def cache(tmp_path):
    """Create a fresh CacheManager for testing."""
    c = CacheManager(db_path=str(tmp_path / "test.duckdb"))
    c.initialize()
    yield c


@pytest.fixture
def populated_cache(cache):
    """Cache with a test dataset stored."""
    df = pd.DataFrame({
        "name": ["Alice", "Bob", "Charlie", "Diana"],
        "age": [30, 25, 35, 28],
        "salary": [50000, 60000, 70000, 55000],
    })
    cache.store_resource(
        resource_id="test-r1",
        dataset_id="test-ds1",
        table_name="ds_test_data_test_r1",
        df=df,
        source_url="http://example.com/data.csv",
    )
    return cache


class TestQueryCached:
    @pytest.mark.asyncio
    async def test_valid_select(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        ctx = make_mock_context(populated_cache)
        result = await query_cached(
            sql='SELECT * FROM "ds_test_data_test_r1" LIMIT 2',
            ctx=ctx,
        )
        text = result.content[0].text
        assert "**2 rows**" in text
        assert "Alice" in text
        assert "Bob" in text

    @pytest.mark.asyncio
    async def test_rejects_drop(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        ctx = make_mock_context(populated_cache)
        with pytest.raises(InvalidQueryError):
            await query_cached(sql='DROP TABLE "ds_test_data_test_r1"', ctx=ctx)

    @pytest.mark.asyncio
    async def test_error_includes_available_tables(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        ctx = make_mock_context(populated_cache)
        with pytest.raises(Exception, match="Available tables"):
            await query_cached(sql='SELECT * FROM "nonexistent_table"', ctx=ctx)

    @pytest.mark.asyncio
    async def test_query_cached_returns_structured_content(self, cache):
        """query_cached returns ToolResult with structured_content alongside text."""
        from fastmcp.tools.tool import ToolResult
        from ontario_data.tools.querying import query_cached

        df = pd.DataFrame([{"city": "Toronto", "pop": 2930000}])
        cache.store_resource("r1", "ds1", "test_cities", df, "http://example.com")

        ctx = make_mock_context(cache)
        result = await query_cached('SELECT * FROM "test_cities"', ctx=ctx)

        assert isinstance(result, ToolResult)
        assert result.structured_content is not None
        assert "rows" in result.structured_content
        assert result.structured_content["rows"][0]["city"] == "Toronto"


class TestCacheInfo:
    @pytest.mark.asyncio
    async def test_returns_stats_and_list(self, populated_cache):
        from ontario_data.tools.retrieval import cache_info

        ctx = make_mock_context(populated_cache)
        result = await cache_info(ctx=ctx)
        assert "table_count" in result
        assert "total_rows" in result
        assert "ds_test_data_test_r1" in result


class TestCacheManage:
    @pytest.mark.asyncio
    async def test_remove(self, populated_cache):
        from ontario_data.tools.retrieval import cache_manage

        ctx = make_mock_context(populated_cache)
        result = await cache_manage(action="remove", resource_id="test-r1", ctx=ctx)
        assert "removed" in result
        assert not populated_cache.is_cached("test-r1")

    @pytest.mark.asyncio
    async def test_clear(self, populated_cache):
        from ontario_data.tools.retrieval import cache_manage

        ctx = make_mock_context(populated_cache)
        result = await cache_manage(action="clear", ctx=ctx)
        assert "cleared" in result
        assert "removed_count" in result

    @pytest.mark.asyncio
    async def test_invalid_action(self, cache):
        from ontario_data.tools.retrieval import cache_manage

        ctx = make_mock_context(cache)
        with pytest.raises(ValueError, match="Invalid action"):
            await cache_manage(action="invalid", ctx=ctx)

    @pytest.mark.asyncio
    async def test_refresh_action_rejected(self, cache):
        from ontario_data.tools.retrieval import cache_manage

        ctx = make_mock_context(cache)
        with pytest.raises(ValueError, match="Invalid action"):
            await cache_manage(action="refresh", ctx=ctx)

    @pytest.mark.asyncio
    async def test_remove_requires_resource_id(self, cache):
        from ontario_data.tools.retrieval import cache_manage

        ctx = make_mock_context(cache)
        with pytest.raises(ValueError, match="resource_id is required"):
            await cache_manage(action="remove", ctx=ctx)


class TestProfileData:
    @pytest.mark.asyncio
    async def test_profile_uses_summarize(self, populated_cache):
        from ontario_data.tools.quality import profile_data

        ctx = make_mock_context(populated_cache)
        result = await profile_data(resource_id="test-r1", ctx=ctx)
        assert "row_count" in result
        assert "ds_test_data_test_r1" in result

    @pytest.mark.asyncio
    async def test_profile_not_cached(self, cache):
        from ontario_data.tools.quality import profile_data

        ctx = make_mock_context(cache)
        with pytest.raises(ResourceNotCachedError):
            await profile_data(resource_id="nonexistent", ctx=ctx)


class TestProfileDataQuality:
    """Tests for merged profile_data (formerly check_data_quality + profile_data)."""

    @pytest.mark.asyncio
    async def test_includes_duplicate_count(self, populated_cache):
        from ontario_data.tools.quality import profile_data

        ctx = make_mock_context(populated_cache)
        result = await profile_data(resource_id="test-r1", ctx=ctx)
        assert "duplicate_rows" in result
        assert "row_count" in result


class TestQueryCachedColumnTypes:
    """Tests for column types in query_cached — numeric VARCHARs are auto-cast."""

    @pytest.mark.asyncio
    async def test_includes_column_types(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        ctx = make_mock_context(populated_cache)
        result = await query_cached(
            sql='SELECT * FROM "ds_test_data_test_r1" LIMIT 2',
            ctx=ctx,
        )
        text = result.content[0].text
        # Should include column type info
        assert "VARCHAR" in text or "BIGINT" in text or "INTEGER" in text

    @pytest.mark.asyncio
    async def test_numeric_varchars_auto_cast(self, cache):
        """VARCHAR columns with numeric strings should be auto-cast to DOUBLE."""
        from ontario_data.tools.querying import query_cached

        df = pd.DataFrame({
            "year": ["2020", "2021", "2022", "2023"],
            "value": ["100", "200", "300", "400"],
            "name": ["Alice", "Bob", "Charlie", "Diana"],
        })
        cache.store_resource(
            resource_id="test-varchar",
            dataset_id="test-ds",
            table_name="ds_varchar_test",
            df=df,
            source_url="http://example.com",
        )
        ctx = make_mock_context(cache)
        result = await query_cached(
            sql='SELECT * FROM "ds_varchar_test"',
            ctx=ctx,
        )
        text = result.content[0].text
        # Numeric columns should be DOUBLE, not VARCHAR
        assert "year (DOUBLE)" in text
        assert "value (DOUBLE)" in text
        # String column stays VARCHAR
        assert "name (VARCHAR)" in text
        # No type warnings surfaced to user
        assert "TRY_CAST" not in text


class TestQueryCachedTruncation:
    """Tests for Item 2: row limit with truncation warning."""

    @pytest.mark.asyncio
    async def test_truncation_warning(self, cache):
        from ontario_data.tools.querying import query_cached

        df = pd.DataFrame({
            "id": range(5000),
            "value": range(5000),
        })
        cache.store_resource(
            resource_id="test-big",
            dataset_id="test-ds",
            table_name="ds_big_table",
            df=df,
            source_url="http://example.com",
        )
        ctx = make_mock_context(cache)
        result = await query_cached(
            sql='SELECT * FROM "ds_big_table"',
            ctx=ctx,
        )
        text = result.content[0].text
        assert "truncated" in text.lower()
        assert "2,000" in text
        assert "5,000" in text

    @pytest.mark.asyncio
    async def test_no_truncation_with_limit(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        ctx = make_mock_context(populated_cache)
        result = await query_cached(
            sql='SELECT * FROM "ds_test_data_test_r1" LIMIT 2',
            ctx=ctx,
        )
        text = result.content[0].text
        assert "truncated" not in text.lower()


class TestQueryCachedEchoSQL:
    """Tests for Item 3: echo executed SQL in results."""

    @pytest.mark.asyncio
    async def test_sql_echoed(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        sql = 'SELECT * FROM "ds_test_data_test_r1" LIMIT 2'
        ctx = make_mock_context(populated_cache)
        result = await query_cached(sql=sql, ctx=ctx)
        text = result.content[0].text
        assert sql in text
        assert "**Query:**" in text


class TestQueryCachedHeuristicWarnings:
    """Tests for Item 5: post-query heuristic warnings."""

    @pytest.mark.asyncio
    async def test_count_star_warning(self, cache):
        from ontario_data.tools.querying import query_cached

        df = pd.DataFrame({
            "region": ["A", "B", "C"],
            "No_of_Exceedances": [10, 20, 30],
        })
        cache.store_resource(
            resource_id="test-count",
            dataset_id="test-ds",
            table_name="ds_count_test",
            df=df,
            source_url="http://example.com",
        )
        ctx = make_mock_context(cache)
        result = await query_cached(
            sql='SELECT COUNT(*) FROM "ds_count_test"',
            ctx=ctx,
        )
        text = result.content[0].text
        assert "SUM" in text
        assert "No_of_Exceedances" in text

    @pytest.mark.asyncio
    async def test_zero_rows_warning(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        ctx = make_mock_context(populated_cache)
        result = await query_cached(
            sql='SELECT * FROM "ds_test_data_test_r1" WHERE name = \'Nonexistent\'',
            ctx=ctx,
        )
        text = result.content[0].text
        assert "0 rows returned but table has" in text

    @pytest.mark.asyncio
    async def test_few_groups_warning(self, cache):
        from ontario_data.tools.querying import query_cached

        df = pd.DataFrame({
            "category": ["A"] * 2000 + ["B"] * 1000,
            "value": range(3000),
        })
        cache.store_resource(
            resource_id="test-groups",
            dataset_id="test-ds",
            table_name="ds_groups_test",
            df=df,
            source_url="http://example.com",
        )
        ctx = make_mock_context(cache)
        result = await query_cached(
            sql='SELECT category, COUNT(*) FROM "ds_groups_test" GROUP BY category',
            ctx=ctx,
        )
        text = result.content[0].text
        assert "Only 2 groups from" in text


class TestSpatialQueryValidation:
    """Tests for Item 8: parameterized spatial queries + coordinate validation."""

    @pytest.mark.asyncio
    async def test_invalid_latitude(self, populated_cache):
        from ontario_data.tools.geospatial import spatial_query

        ctx = make_mock_context(populated_cache)
        with pytest.raises(ValueError, match="Latitude .* out of range"):
            await spatial_query(
                resource_id="test-r1",
                operation="contains_point",
                latitude=9999,
                longitude=-79.0,
                ctx=ctx,
            )

    @pytest.mark.asyncio
    async def test_invalid_longitude(self, populated_cache):
        from ontario_data.tools.geospatial import spatial_query

        ctx = make_mock_context(populated_cache)
        with pytest.raises(ValueError, match="Longitude .* out of range"):
            await spatial_query(
                resource_id="test-r1",
                operation="contains_point",
                latitude=43.0,
                longitude=999,
                ctx=ctx,
            )

    @pytest.mark.asyncio
    async def test_invalid_radius(self, populated_cache):
        from ontario_data.tools.geospatial import spatial_query

        ctx = make_mock_context(populated_cache)
        with pytest.raises(ValueError, match="Radius must be positive"):
            await spatial_query(
                resource_id="test-r1",
                operation="within_radius",
                latitude=43.0,
                longitude=-79.0,
                radius_km=-5,
                ctx=ctx,
            )


class TestQueryCachedProvenance:
    """Tests for Item 10: data provenance in query_cached results."""

    @pytest.mark.asyncio
    async def test_provenance_in_response(self, populated_cache):
        from ontario_data.tools.querying import query_cached

        ctx = make_mock_context(populated_cache)
        result = await query_cached(
            sql='SELECT * FROM "ds_test_data_test_r1" LIMIT 2',
            ctx=ctx,
        )
        text = result.content[0].text
        assert "**Source:**" in text
        assert "test-r1" in text
        assert "Downloaded:" in text


class TestProfileDataAutoCast:
    """Tests that profile_data works correctly with auto-cast numeric columns."""

    @pytest.mark.asyncio
    async def test_auto_cast_columns_profiled_as_double(self, cache):
        from ontario_data.tools.quality import profile_data

        df = pd.DataFrame({
            "year": ["2020", "2021", "2022", "2023"],
            "name": ["Alice", "Bob", "Charlie", "Diana"],
        })
        cache.store_resource(
            resource_id="test-qv",
            dataset_id="test-ds",
            table_name="ds_quality_varchar_test",
            df=df,
            source_url="http://example.com",
        )
        ctx = make_mock_context(cache)
        result = await profile_data(resource_id="test-qv", ctx=ctx)
        # No type warnings or TRY_CAST hints in output
        assert "type_warnings" not in result
        assert "TRY_CAST" not in result
        assert "year" in result

    @pytest.mark.asyncio
    async def test_auto_cast_stats_in_summarize(self, cache):
        """SUMMARIZE gets real numeric stats since columns are auto-cast to DOUBLE."""
        from ontario_data.tools.quality import profile_data

        df = pd.DataFrame({
            "amount": ["10", "20", "30", "40"],
            "label": ["a", "b", "c", "d"],
        })
        cache.store_resource(
            resource_id="test-numstats",
            dataset_id="test-ds",
            table_name="ds_quality_numstats_test",
            df=df,
            source_url="http://example.com",
        )
        ctx = make_mock_context(cache)
        result = await profile_data(resource_id="test-numstats", ctx=ctx)
        # No separate numeric_varchar_stats needed — SUMMARIZE handles DOUBLE columns
        assert "numeric_varchar_stats" not in result
        assert "amount" in result

    @pytest.mark.asyncio
    async def test_mixed_values_auto_cast_with_nulls(self, cache):
        """Mixed numeric/non-numeric values: non-numeric become NULL after auto-cast."""
        from ontario_data.tools.quality import profile_data

        # >80% numeric to pass detection threshold (9/10 = 90%)
        df = pd.DataFrame({
            "value": ["100", "200", "300", "400", "500", "600", "700", "800", "900", "N/A"],
            "name": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
        })
        cache.store_resource(
            resource_id="test-mixed",
            dataset_id="test-ds",
            table_name="ds_quality_mixed_test",
            df=df,
            source_url="http://example.com",
        )
        ctx = make_mock_context(cache)
        result = await profile_data(resource_id="test-mixed", ctx=ctx)
        # No type warnings — column was auto-cast
        assert "type_warnings" not in result


class TestNumericVarcharAutoCast:
    """Tests that numeric VARCHAR columns are auto-cast to DOUBLE at download time."""

    def test_numeric_columns_cast_to_double(self, cache):
        df = pd.DataFrame({
            "year": ["2020", "2021", "2022"],
            "name": ["Alice", "Bob", "Charlie"],
            "amount": ["100.5", "200.3", "300.1"],
        })
        cache.store_resource(
            resource_id="test-tw",
            dataset_id="test-ds",
            table_name="ds_type_warn_test",
            df=df,
            source_url="http://example.com",
        )
        # Columns should now be DOUBLE in the table
        columns = cache.execute_sql('DESCRIBE "ds_type_warn_test"')
        col_types = {c[0]: str(c[1]) for c in columns}
        assert col_types["year"] == "DOUBLE"
        assert col_types["amount"] == "DOUBLE"
        assert "VARCHAR" in col_types["name"]

    def test_comma_formatted_numbers_cast(self, cache):
        df = pd.DataFrame({
            "award": ["1,234,567", "2,345,678", "3,456,789"],
            "name": ["Project A", "Project B", "Project C"],
        })
        cache.store_resource(
            resource_id="test-comma",
            dataset_id="test-ds",
            table_name="ds_comma_test",
            df=df,
            source_url="http://example.com",
        )
        columns = cache.execute_sql('DESCRIBE "ds_comma_test"')
        col_types = {c[0]: str(c[1]) for c in columns}
        assert col_types["award"] == "DOUBLE"
        # Verify actual values
        vals = cache.execute_sql('SELECT "award" FROM "ds_comma_test" ORDER BY "award"')
        assert vals[0][0] == 1234567.0


class TestDownloadResourceAlreadyCached:
    @pytest.mark.asyncio
    async def test_returns_staleness_info(self, populated_cache):
        from ontario_data.tools.retrieval import download_resource

        ctx = make_mock_context(populated_cache)
        result = await download_resource(resource_id="test-r1", ctx=ctx)
        assert "already_cached" in result
        assert "ds_test_data_test_r1" in result
