"""
Unit-Tests für QueryResult und TimeSeriesResult.

Testet die Result-Wrapper-Klassen insbesondere die DataFrame-Konvertierung.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime

import pytest

from questra_data.models import Namespace, Role, Unit
from questra_data.results import QueryResult
from tests.conftest import load_graphql_response


@pytest.mark.unit
class TestQueryResultSequenceBehavior:
    """Tests für QueryResult als Sequence."""

    def test_query_result_iteration(self):
        """Test QueryResult kann iteriert werden."""
        data = [{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]
        result = QueryResult(data)

        items = list(result)
        assert len(items) == 2
        assert items[0] == {"id": 1, "name": "A"}
        assert items[1] == {"id": 2, "name": "B"}

    def test_query_result_length(self):
        """Test QueryResult hat korrekte Länge."""
        data = [{"id": 1}, {"id": 2}, {"id": 3}]
        result = QueryResult(data)

        assert len(result) == 3

    def test_query_result_indexing(self):
        """Test QueryResult unterstützt Indexing."""
        data = [{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]
        result = QueryResult(data)

        assert result[0] == {"id": 1, "name": "A"}
        assert result[1] == {"id": 2, "name": "B"}
        assert result[-1] == {"id": 2, "name": "B"}

    def test_query_result_slicing(self):
        """Test QueryResult unterstützt Slicing."""
        data = [{"id": i} for i in range(5)]
        result = QueryResult(data)

        sliced = result[1:3]
        assert sliced == [{"id": 1}, {"id": 2}]
        assert isinstance(sliced, list)

    def test_query_result_empty(self):
        """Test QueryResult mit leeren Daten."""
        result = QueryResult([])

        assert len(result) == 0
        assert list(result) == []

    def test_query_result_repr(self):
        """Test QueryResult hat aussagekräftige Repr."""
        data = [{"id": 1}]
        result = QueryResult(data)

        repr_str = repr(result)
        assert "QueryResult" in repr_str
        assert "{'id': 1}" in repr_str

    def test_query_result_str(self):
        """Test QueryResult String-Konvertierung."""
        data = [{"id": 1}, {"id": 2}]
        result = QueryResult(data)

        str_repr = str(result)
        assert str_repr == str(data)

    def test_query_result_equality_with_list(self):
        """Test QueryResult kann mit Listen verglichen werden."""
        data = [{"id": 1}, {"id": 2}]
        result = QueryResult(data)

        assert result == data
        assert result == [{"id": 1}, {"id": 2}]
        assert result != [{"id": 3}]

    def test_query_result_equality_with_query_result(self):
        """Test QueryResult kann mit anderen QueryResults verglichen werden."""
        data1 = [{"id": 1}, {"id": 2}]
        data2 = [{"id": 1}, {"id": 2}]
        data3 = [{"id": 3}]

        result1 = QueryResult(data1)
        result2 = QueryResult(data2)
        result3 = QueryResult(data3)

        assert result1 == result2
        assert result1 != result3


@pytest.mark.unit
class TestQueryResultToDf:
    """Tests für QueryResult.to_df() Methode."""

    def test_to_df_with_dict_data(self):
        """Test to_df() mit Dictionary-Daten."""
        pytest.importorskip("pandas")
        import pandas as pd

        data = [
            {"id": 1, "name": "Alice", "age": 30},
            {"id": 2, "name": "Bob", "age": 25},
            {"id": 3, "name": "Charlie", "age": 35},
        ]
        result = QueryResult(data)

        df = result.to_df()

        # DataFrame validieren
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 3
        assert list(df.columns) == ["id", "name", "age"]
        assert df.iloc[0]["name"] == "Alice"
        assert df.iloc[1]["age"] == 25
        assert df.iloc[2]["id"] == 3

    def test_to_df_with_empty_data(self):
        """Test to_df() mit leeren Daten."""
        pytest.importorskip("pandas")
        import pandas as pd

        result = QueryResult([])
        df = result.to_df()

        assert isinstance(df, pd.DataFrame)
        assert len(df) == 0

    def test_to_df_with_namespaces_from_json(self):
        """Test to_df() mit Namespace-Daten aus JSON-Fixture."""
        pytest.importorskip("pandas")
        import pandas as pd

        # Lade Namespace Response aus JSON
        response = load_graphql_response("namespaces_response.json")
        namespaces = [Namespace.from_dict(ns) for ns in response["_namespaces"]]

        result = QueryResult(namespaces)
        df = result.to_df()

        # DataFrame validieren
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 3
        assert "name" in df.columns
        assert "is_system" in df.columns
        assert "description" in df.columns

        # Erste Zeile validieren
        assert df.iloc[0]["name"] == "TestNamespace"
        assert not df.iloc[0]["is_system"]  # False (pandas konvertiert zu np.False_)
        assert pd.isna(df.iloc[0]["description"])  # null in JSON → NaN

        # Zweite Zeile (Default Namespace)
        assert pd.isna(df.iloc[1]["name"])  # null in JSON → NaN
        assert df.iloc[1]["description"] == "The default namespace."
        assert df.iloc[1]["is_system"]  # True (pandas konvertiert zu np.True_)

    def test_to_df_with_roles_from_json(self):
        """Test to_df() mit Role-Daten aus JSON-Fixture."""
        pytest.importorskip("pandas")
        import pandas as pd

        # Lade Roles Response aus JSON
        response = load_graphql_response("roles_response.json")
        roles = [Role.from_dict(role) for role in response["_roles"]]

        result = QueryResult(roles)
        df = result.to_df()

        # DataFrame validieren
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 3
        assert "name" in df.columns
        assert "is_system" in df.columns

        # Validiere _administrators Role
        admin_row = df[df["name"] == "_administrators"].iloc[0]
        assert admin_row["is_system"]  # True
        assert "all permissons" in admin_row["description"]

        # Validiere Role ohne Description
        no_desc_row = df[df["name"] == "NoDescription"].iloc[0]
        assert not no_desc_row["is_system"]  # False
        assert pd.isna(no_desc_row["description"])

    def test_to_df_with_units_from_json(self):
        """Test to_df() mit Unit-Daten aus JSON-Fixture."""
        pytest.importorskip("pandas")
        import pandas as pd

        # Lade Units Response aus JSON
        response = load_graphql_response("units_response.json")
        units = [Unit.from_dict(unit) for unit in response["_units"]]

        result = QueryResult(units)
        df = result.to_df()

        # DataFrame validieren
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 3
        assert list(df.columns) == ["symbol", "aggregation"]

        # Werte validieren
        assert df.iloc[0]["symbol"] == "kWh"
        assert df.iloc[0]["aggregation"] == "SUM"
        assert df.iloc[1]["symbol"] == "°C"
        assert df.iloc[1]["aggregation"] == "AVERAGE"

    def test_to_df_with_dataclass_objects(self):
        """Test to_df() mit generischen Dataclass-Objekten."""
        pytest.importorskip("pandas")
        import pandas as pd

        @dataclass
        class Person:
            name: str
            age: int
            city: str | None = None

        people = [
            Person("Alice", 30, "Berlin"),
            Person("Bob", 25, None),
            Person("Charlie", 35, "Munich"),
        ]

        result = QueryResult(people)
        df = result.to_df()

        # DataFrame validieren
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 3
        assert list(df.columns) == ["name", "age", "city"]
        assert df.iloc[0]["name"] == "Alice"
        assert df.iloc[1]["age"] == 25
        assert pd.isna(df.iloc[1]["city"])

    def test_to_df_with_pydantic_models(self):
        """Test to_df() mit Pydantic-Model-Objekten (model_dump)."""
        pytest.importorskip("pandas")
        pydantic = pytest.importorskip("pydantic")
        import pandas as pd

        BaseModel = pydantic.BaseModel

        class User(BaseModel):
            id: int
            username: str
            email: str

        users = [
            User(id=1, username="alice", email="alice@example.com"),
            User(id=2, username="bob", email="bob@example.com"),
        ]

        result = QueryResult(users)
        df = result.to_df()

        # DataFrame validieren
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 2
        assert list(df.columns) == ["id", "username", "email"]
        assert df.iloc[0]["username"] == "alice"
        assert df.iloc[1]["email"] == "bob@example.com"

    def test_to_df_with_primitive_types(self):
        """Test to_df() mit primitiven Datentypen (Fallback)."""
        pytest.importorskip("pandas")
        import pandas as pd

        data = [1, 2, 3, 4, 5]
        result = QueryResult(data)

        df = result.to_df()

        # DataFrame validieren - Primitive Werte in "value" Spalte
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 5
        assert list(df.columns) == ["value"]
        assert df.iloc[0]["value"] == 1
        assert df.iloc[4]["value"] == 5

    def test_to_df_with_nested_dict_data(self):
        """Test to_df() mit verschachtelten Dictionary-Daten."""
        pytest.importorskip("pandas")
        import pandas as pd

        data = [
            {
                "_id": "1",
                "_rowVersion": "1",
                "name": "Alice",
                "address": {"city": "Berlin", "zip": "10115"},
            },
            {
                "_id": "2",
                "_rowVersion": "1",
                "name": "Bob",
                "address": {"city": "Munich", "zip": "80331"},
            },
        ]
        result = QueryResult(data)

        df = result.to_df()

        # DataFrame validieren - verschachtelte Dicts bleiben als dict
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 2
        assert df.iloc[0]["name"] == "Alice"
        assert df.iloc[0]["address"] == {"city": "Berlin", "zip": "10115"}
        assert isinstance(df.iloc[0]["address"], dict)

    def test_to_df_with_datetime_columns(self):
        """Test to_df() mit datetime-Spalten."""
        pytest.importorskip("pandas")
        import pandas as pd

        # Lade Namespace Response aus JSON (enthält datetime-Felder)
        response = load_graphql_response("namespaces_response.json")
        namespaces = [Namespace.from_dict(ns) for ns in response["_namespaces"]]

        result = QueryResult(namespaces)
        df = result.to_df()

        # DateTime-Spalten validieren
        assert "created_at" in df.columns
        assert "altered_at" in df.columns
        assert isinstance(df.iloc[0]["created_at"], datetime)
        assert isinstance(df.iloc[0]["altered_at"], datetime)

    def test_to_df_with_mixed_nullable_fields(self):
        """Test to_df() mit gemischten nullable Feldern."""
        pytest.importorskip("pandas")
        import pandas as pd

        data = [
            {"id": 1, "name": "Alice", "email": "alice@example.com", "phone": None},
            {"id": 2, "name": "Bob", "email": None, "phone": "+49123456"},
            {"id": 3, "name": "Charlie", "email": "c@example.com", "phone": None},
        ]
        result = QueryResult(data)

        df = result.to_df()

        # Nullable Felder validieren
        assert len(df) == 3
        assert df.iloc[0]["name"] == "Alice"
        assert pd.isna(df.iloc[0]["phone"])
        assert pd.isna(df.iloc[1]["email"])
        assert df.iloc[1]["phone"] == "+49123456"

    def test_to_df_without_pandas_raises_import_error(self, monkeypatch):
        """Test to_df() wirft ImportError wenn pandas nicht verfügbar ist."""
        # Mock: pandas ist nicht verfügbar
        import questra_data.results

        monkeypatch.setattr(questra_data.results, "_PANDAS_AVAILABLE", False)

        data = [{"id": 1, "name": "Test"}]
        result = QueryResult(data)

        with pytest.raises(ImportError) as exc_info:
            result.to_df()

        error_msg = str(exc_info.value)
        assert "pandas ist nicht installiert" in error_msg
        assert "pip install pandas" in error_msg

    def test_to_df_preserves_column_order(self):
        """Test to_df() erhält die Spalten-Reihenfolge bei dict-Daten."""
        pytest.importorskip("pandas")

        # Seit Python 3.7 sind Dicts ordered - Reihenfolge sollte erhalten bleiben
        data = [
            {"_id": "1", "_rowVersion": "1", "name": "Alice", "age": 30, "city": "Berlin"},
            {"_id": "2", "_rowVersion": "2", "name": "Bob", "age": 25, "city": "Munich"},
        ]
        result = QueryResult(data)

        df = result.to_df()

        # Spalten-Reihenfolge validieren
        expected_columns = ["_id", "_rowVersion", "name", "age", "city"]
        assert list(df.columns) == expected_columns

    def test_to_df_with_inventory_items(self):
        """Test to_df() mit Item-Daten (typischer Use-Case)."""
        pytest.importorskip("pandas")
        import pandas as pd

        # Simuliere list_items Response
        items = [
            {
                "_id": "638301262349418496",
                "_rowVersion": "1",
                "name": "Device A",
                "location": "Building 1",
                "status": "active",
            },
            {
                "_id": "638301262349418497",
                "_rowVersion": "2",
                "name": "Device B",
                "location": "Building 2",
                "status": "inactive",
            },
            {
                "_id": "638301262349418498",
                "_rowVersion": "1",
                "name": "Device C",
                "location": None,
                "status": "active",
            },
        ]

        result = QueryResult(items)
        df = result.to_df()

        # DataFrame validieren
        assert isinstance(df, pd.DataFrame)
        assert len(df) == 3
        assert "_id" in df.columns
        assert "_rowVersion" in df.columns
        assert df.iloc[0]["name"] == "Device A"
        assert df.iloc[1]["status"] == "inactive"
        assert pd.isna(df.iloc[2]["location"])
