#
#  Tests for owid.catalog.api module
#
import pytest

from owid.catalog import Client
from owid.catalog.api import (
    ChartNotFoundError,
    ChartResult,
    IndicatorResult,
    LicenseError,
    PageSearchResult,
    ResponseSet,
    TableResult,
)


class TestClient:
    """Test the unified Client class."""

    def test_client_has_all_apis(self):
        client = Client()
        assert hasattr(client, "charts")
        assert hasattr(client, "_site_search")
        assert hasattr(client, "indicators")
        assert hasattr(client, "tables")

    def test_client_repr(self):
        client = Client()
        assert "Client" in repr(client)


class TestChartsAPI:
    """Test the Charts API."""

    def test_get_chart_data(self):
        client = Client()
        df = client.charts.get_data("life-expectancy")

        assert df is not None
        assert len(df) > 0
        assert "entities" in df.columns
        assert "years" in df.columns

    def test_get_chart_data_by_url(self):
        client = Client()
        df = client.charts.get_data("https://ourworldindata.org/grapher/life-expectancy")

        assert df is not None
        assert len(df) > 0

    def test_get_chart_metadata(self):
        client = Client()
        meta = client.charts.get_metadata("life-expectancy")

        assert meta is not None
        assert isinstance(meta, dict)
        assert "columns" in meta

    def test_get_chart_config(self):
        client = Client()
        config = client.charts.get_config("life-expectancy")

        assert config is not None
        assert isinstance(config, dict)

    def test_fetch_chart(self):
        client = Client()
        chart = client.charts.fetch("life-expectancy")

        assert isinstance(chart, ChartResult)
        assert chart.slug == "life-expectancy"
        assert chart.title
        assert chart.url == "https://ourworldindata.org/grapher/life-expectancy"

    def test_chart_not_found(self):
        client = Client()
        with pytest.raises(ChartNotFoundError):
            client.charts.get_data("this-chart-does-not-exist")

    def test_non_redistributable_chart(self):
        client = Client()
        with pytest.raises(LicenseError):
            client.charts.get_data("test-scores-ai-capabilities-relative-human-performance")

    def test_invalid_url(self):
        client = Client()
        with pytest.raises(ValueError):
            client.charts.get_data("https://example.com/not-a-grapher-url")


class TestChartsAPISearch:
    """Test the Charts API search functionality."""

    def test_charts_search(self):
        """Test searching for charts via ChartsAPI."""
        client = Client()
        results = client.charts.search("life expectancy")

        assert isinstance(results, ResponseSet)
        assert len(results) > 0
        assert all(isinstance(r, ChartResult) for r in results)
        assert results[0].slug
        assert results[0].title

    def test_charts_search_with_countries(self):
        """Test searching with country filters."""
        client = Client()
        results = client.charts.search("gdp", countries=["France"])

        assert isinstance(results, ResponseSet)
        # Results should be filtered by country

    def test_search_results_to_frame(self):
        """Test converting search results to DataFrame."""
        client = Client()
        results = client.charts.search("population")

        df = results.to_frame()
        assert "slug" in df.columns
        assert "title" in df.columns
        assert len(df) == len(results)


class TestSiteSearchAPI:
    """Test the internal SiteSearchAPI (advanced usage)."""

    def test_site_search_pages(self):
        """Test searching for pages via _site_search."""
        client = Client()
        results = client._site_search.pages("climate change")

        assert isinstance(results, ResponseSet)
        assert len(results) > 0
        assert all(isinstance(r, PageSearchResult) for r in results)
        assert results[0].slug
        assert results[0].title

    def test_site_search_charts(self):
        """Test accessing charts via _site_search (should work but not recommended)."""
        client = Client()
        results = client._site_search.charts("gdp")

        assert isinstance(results, ResponseSet)
        assert len(results) > 0

    def test_site_search_charts_limit_warning(self):
        """Test that warning is raised when limit > 100 for charts."""
        import warnings

        client = Client()
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            results = client._site_search.charts("gdp", limit=150)

            # Check warning was raised
            assert len(w) == 1
            assert issubclass(w[0].category, UserWarning)
            assert "Max allowed of result items is 100" in str(w[0].message)

            # Check that limit was clamped to 100
            assert isinstance(results, ResponseSet)

    def test_site_search_pages_limit_warning(self):
        """Test that warning is raised when limit > 100 for pages."""
        import warnings

        client = Client()
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            results = client._site_search.pages("climate", limit=200)

            # Check warning was raised
            assert len(w) == 1
            assert issubclass(w[0].category, UserWarning)
            assert "Max allowed of result items is 100" in str(w[0].message)

            # Check that limit was clamped to 100
            assert isinstance(results, ResponseSet)


class TestIndicatorsAPI:
    """Test the Indicators API."""

    def test_search_indicators(self):
        client = Client()
        results = client.indicators.search("solar power generation")

        assert isinstance(results, ResponseSet)
        assert len(results) > 0
        assert all(isinstance(r, IndicatorResult) for r in results)

        # Check result fields
        first = results[0]
        assert first.indicator_id is not None and first.indicator_id > 0
        assert first.title
        assert 0 <= first.score <= 1
        assert first.path

    def test_indicator_results_to_catalog_frame(self):
        client = Client()
        results = client.indicators.search("co2 emissions")

        frame = results.to_catalog_frame()
        assert "indicator_title" in frame.columns
        assert "score" in frame.columns
        assert "path" in frame.columns

    def test_fetch_indicator(self):
        """Test fetching a specific indicator by URI."""
        client = Client()
        # First search to find an indicator URI
        results = client.indicators.search("solar power")
        if len(results) > 0:
            # Fetch by URI (path)
            assert results[0].path
            indicator = client.indicators.fetch(results[0].path)
            assert indicator.column_name
            assert indicator.title
            assert indicator.path == results[0].path
            # Test lazy loading - table should not be loaded yet
            assert indicator._table is None
            # Test data access - this triggers lazy loading
            variable = indicator.data
            assert variable is not None
            # After accessing data, table should be cached
            assert indicator._table is not None

    def test_fetch_indicator_invalid_format(self):
        """Test that invalid path format raises error."""
        client = Client()
        with pytest.raises(ValueError, match="Invalid indicator path format"):
            # Missing # separator
            client.indicators.fetch("grapher/un/2024/pop/population")

    def test_fetch_indicator_missing_column(self):
        """Test that fetching non-existent column raises error."""
        client = Client()
        # Search to get a valid table path
        results = client.indicators.search("solar power")
        if len(results) > 0:
            # Get table path from path
            assert results[0].path
            table_path = results[0].path.partition("#")[0]
            # Try to fetch with a non-existent column
            with pytest.raises(ValueError, match="Column 'nonexistent_column_12345' not found"):
                client.indicators.fetch(f"{table_path}#nonexistent_column_12345")

    def test_get_data(self):
        """Test get_data convenience method."""
        client = Client()
        # Search to get an indicator path
        results = client.indicators.search("solar power")
        if len(results) > 0:
            path = results[0].path
            assert path is not None
            # Use get_data - should return Variable directly
            variable = client.indicators.get_data(path)
            assert variable is not None
            # Should be equivalent to fetch().data
            variable2 = client.indicators.fetch(path).data
            assert variable.name == variable2.name


class TestTablesAPI:
    """Test the Tables API."""

    def test_search_tables(self):
        client = Client()
        results = client.tables.search(table="population")

        assert isinstance(results, ResponseSet)
        assert len(results) > 0
        assert all(isinstance(r, TableResult) for r in results)

    def test_search_tables_by_namespace(self):
        client = Client()
        results = client.tables.search(table="population", namespace="un")

        assert isinstance(results, ResponseSet)
        assert len(results) > 0
        assert all(r.namespace == "un" for r in results)

    def test_table_results_to_catalog_frame(self):
        client = Client()
        results = client.tables.search(table="population", namespace="un")

        frame = results.to_catalog_frame()
        assert "table" in frame.columns
        assert "namespace" in frame.columns
        assert "path" in frame.columns

        # Should be loadable
        assert hasattr(frame, "load")

    def test_fetch_table(self):
        """Test fetching table metadata by path."""
        client = Client()
        # First search to find a path
        results = client.tables.search(table="population", namespace="un")
        if len(results) > 0:
            path = results[0].path
            # Now fetch by path (should return same metadata)
            table_result = client.tables.fetch(path)
            assert isinstance(table_result, TableResult)
            assert table_result.path == path
            assert table_result.table
            assert table_result.dataset
            assert table_result.namespace == "un"

    def test_fetch_invalid_path(self):
        """Test that fetching with invalid path format raises error."""
        client = Client()
        with pytest.raises(ValueError, match="Invalid catalog path"):
            client.tables.fetch("invalid/path")

    def test_fetch_nonexistent_table(self):
        """Test that fetching non-existent table raises error."""
        client = Client()
        with pytest.raises(ValueError, match="not found"):
            client.tables.fetch("garden/fake/2024-01-01/fake/fake")

    def test_get_data(self):
        """Test get_data convenience method."""
        client = Client()
        # Search to get a table path
        results = client.tables.search(table="population", namespace="un")
        if len(results) > 0:
            path = results[0].path
            # Use get_data - should return Table directly
            table = client.tables.get_data(path)
            assert table is not None
            assert len(table) > 0
            # Should be equivalent to fetch().data
            table2 = client.tables.fetch(path).data
            assert table.m.short_name == table2.m.short_name

    def test_backwards_compatibility_datasets(self):
        """Test that client.datasets still works (backwards compatibility)."""
        client = Client()

        # Should work via datasets attribute
        results = client.datasets.search(table="population")
        assert len(results) > 0

        # Verify it's the same as tables
        assert client.datasets is client.tables


class TestResponseSet:
    """Test the ResponseSet container."""

    def test_iteration(self):
        results = ResponseSet(results=[1, 2, 3], query="test")

        items = list(results)
        assert items == [1, 2, 3]

    def test_indexing(self):
        results = ResponseSet(results=["a", "b", "c"], query="test")

        assert results[0] == "a"
        assert results[1] == "b"
        assert results[2] == "c"

    def test_len(self):
        results = ResponseSet(results=[1, 2, 3, 4, 5], query="test")
        assert len(results) == 5

    def test_limit_auto_set(self):
        results = ResponseSet(results=[1, 2, 3], query="test")
        assert results.limit == 3

    def test_limit_explicit(self):
        results = ResponseSet(results=[1, 2, 3], query="test", limit=100)
        assert results.limit == 100

    def test_filter(self):
        """Test filtering results with a predicate."""
        # Create mock result objects with version attribute
        from pydantic import BaseModel

        class MockResult(BaseModel):
            version: str
            name: str

        items = [
            MockResult(version="2024-01-01", name="a"),
            MockResult(version="2024-06-01", name="b"),
            MockResult(version="2023-12-01", name="c"),
            MockResult(version="2024-03-01", name="d"),
        ]
        results = ResponseSet(results=items, query="test")

        # Filter by version (>= "2024-03-01" matches 2024-03-01 and 2024-06-01)
        filtered = results.filter(lambda r: r.version >= "2024-03-01")
        assert len(filtered) == 2
        assert all(r.version >= "2024-03-01" for r in filtered)
        assert {r.name for r in filtered} == {"b", "d"}

        # Filter by name
        filtered = results.filter(lambda r: r.name in ["a", "c"])
        assert len(filtered) == 2
        assert {r.name for r in filtered} == {"a", "c"}

        # Chain filters
        filtered = results.filter(lambda r: r.version >= "2024-01-01").filter(lambda r: r.name != "d")
        assert len(filtered) == 2

    def test_sort_by_string(self):
        """Test sorting by attribute name."""
        from pydantic import BaseModel

        class MockResult(BaseModel):
            version: str
            score: float

        items = [
            MockResult(version="2024-03-01", score=0.8),
            MockResult(version="2024-01-01", score=0.9),
            MockResult(version="2024-06-01", score=0.7),
        ]
        results = ResponseSet(results=items, query="test")

        # Sort by version ascending
        sorted_results = results.sort_by("version")
        versions = [r.version for r in sorted_results]
        assert versions == ["2024-01-01", "2024-03-01", "2024-06-01"]

        # Sort by version descending
        sorted_results = results.sort_by("version", reverse=True)
        versions = [r.version for r in sorted_results]
        assert versions == ["2024-06-01", "2024-03-01", "2024-01-01"]

        # Sort by score
        sorted_results = results.sort_by("score", reverse=True)
        scores = [r.score for r in sorted_results]
        assert scores == [0.9, 0.8, 0.7]

    def test_sort_by_function(self):
        """Test sorting by key function."""
        from pydantic import BaseModel

        class MockResult(BaseModel):
            name: str
            value: int

        items = [
            MockResult(name="c", value=3),
            MockResult(name="a", value=1),
            MockResult(name="b", value=2),
        ]
        results = ResponseSet(results=items, query="test")

        # Sort by value using lambda
        sorted_results = results.sort_by(lambda r: r.value)
        values = [r.value for r in sorted_results]
        assert values == [1, 2, 3]

        # Sort by name using lambda
        sorted_results = results.sort_by(lambda r: r.name)
        names = [r.name for r in sorted_results]
        assert names == ["a", "b", "c"]

    def test_latest(self):
        """Test getting the latest result."""
        from pydantic import BaseModel

        class MockResult(BaseModel):
            version: str
            published_at: str
            score: float

        items = [
            MockResult(version="2024-01-01", published_at="2024-01-15", score=0.8),
            MockResult(version="2024-06-01", published_at="2024-06-15", score=0.9),
            MockResult(version="2024-03-01", published_at="2024-03-15", score=0.7),
        ]
        results = ResponseSet(results=items, query="test")

        # Get latest by version
        latest = results.latest(by="version")
        assert latest.version == "2024-06-01"

        # Get latest by published_at
        latest = results.latest(by="published_at")
        assert latest.published_at == "2024-06-15"

        # Get latest by score
        latest = results.latest(by="score")
        assert latest.score == 0.9

    def test_latest_empty_results(self):
        """Test that latest() raises ValueError on empty results."""
        results = ResponseSet(results=[], query="test")

        with pytest.raises(ValueError, match="No results available"):
            results.latest(by="version")

    def test_first_single(self):
        """Test getting first single result."""
        from pydantic import BaseModel

        class MockResult(BaseModel):
            name: str

        items = [
            MockResult(name="a"),
            MockResult(name="b"),
            MockResult(name="c"),
        ]
        results = ResponseSet(results=items, query="test")

        # Get first item
        first = results.first()
        assert first.name == "a"

        # First of empty results
        empty_results = ResponseSet(results=[], query="test")
        assert empty_results.first() is None

    def test_first_multiple(self):
        """Test getting first n results."""
        from pydantic import BaseModel

        class MockResult(BaseModel):
            name: str

        items = [
            MockResult(name="a"),
            MockResult(name="b"),
            MockResult(name="c"),
            MockResult(name="d"),
        ]
        results = ResponseSet(results=items, query="test")

        # Get first 2
        first_two = results.first(2)
        assert isinstance(first_two, ResponseSet)
        assert len(first_two) == 2
        assert [r.name for r in first_two] == ["a", "b"]

        # Get first 10 (more than available)
        first_ten = results.first(10)
        assert isinstance(first_ten, ResponseSet)
        assert len(first_ten) == 4

    def test_chaining(self):
        """Test chaining multiple convenience methods."""
        from pydantic import BaseModel

        class MockResult(BaseModel):
            version: str
            namespace: str
            score: float

        items = [
            MockResult(version="2024-01-01", namespace="un", score=0.8),
            MockResult(version="2024-06-01", namespace="worldbank", score=0.9),
            MockResult(version="2024-03-01", namespace="un", score=0.7),
            MockResult(version="2024-02-01", namespace="worldbank", score=0.6),
        ]
        results = ResponseSet(results=items, query="test")

        # Chain filter -> sort -> first
        filtered = results.filter(lambda r: r.namespace == "un").sort_by("version", reverse=True).first(1)
        assert filtered.version == "2024-03-01"
        assert filtered.namespace == "un"

        # Chain filter -> sort -> first multiple
        top_two = results.filter(lambda r: r.score > 0.6).sort_by("score", reverse=True).first(2)
        assert isinstance(top_two, ResponseSet)
        assert len(top_two) == 2
        assert [r.score for r in top_two] == [0.9, 0.8]


class TestDataclassModels:
    """Test the dataclass model objects."""

    def test_chart_result(self):
        result = ChartResult(
            slug="test-chart",
            title="Test Chart",
            url="https://ourworldindata.org/grapher/test-chart",
        )

        assert result.slug == "test-chart"
        assert result.title == "Test Chart"

    def test_indicator_result(self):
        result = IndicatorResult(
            indicator_id=123,
            title="Test Indicator",
            score=0.95,
            path="grapher/test/2024/dataset/table#column",
        )

        assert result.indicator_id == 123
        assert result.score == 0.95

    def test_table_result(self):
        result = TableResult(
            table="population",
            dataset="un_wpp",
            version="2024-07-11",
            namespace="un",
            channel="garden",
            path="garden/un/2024-07-11/un_wpp/population",
        )

        assert result.table == "population"
        assert result.namespace == "un"
