import math
import datetime
import numpy as np
import pandas as pd
import pytest
from r_bridge.deserializer import deserialize, register_deserializer, _CUSTOM
from r_bridge.type_hints import TypeHint


# --- Scalars ---

def test_none():
    assert deserialize(None) is None

def test_bool():
    assert deserialize(True) is True

def test_float():
    assert deserialize(3.14) == pytest.approx(3.14)

def test_str():
    assert deserialize("hello") == "hello"

def test_integer_tag():
    assert deserialize({"__type__": "integer", "value": 7}) == 7
    assert isinstance(deserialize({"__type__": "integer", "value": 7}), int)

def test_nan_tag():
    assert math.isnan(deserialize({"__type__": "nan"}))

def test_inf_tag():
    assert deserialize({"__type__": "inf", "sign": 1}) == math.inf
    assert deserialize({"__type__": "inf", "sign": -1}) == -math.inf

def test_complex_tag():
    result = deserialize({"__type__": "complex", "r": 1.0, "i": 2.0})
    assert result == 1 + 2j

def test_posixct():
    result = deserialize({"__type__": "posixct", "iso8601": "2026-03-21T12:00:00"})
    assert isinstance(result, datetime.datetime)
    assert result.year == 2026

def test_date_tag():
    result = deserialize({"__type__": "date", "iso8601": "2026-03-21"})
    assert result == datetime.date(2026, 3, 21)

def test_na_tag_numeric():
    result = deserialize({"__type__": "na", "na_type": "real"})
    assert math.isnan(result)

def test_na_tag_integer():
    result = deserialize({"__type__": "na", "na_type": "integer"})
    assert result is pd.NA

def test_na_tag_character():
    result = deserialize({"__type__": "na", "na_type": "character"})
    assert result is None


# --- Vectors → numpy / list ---

def test_numeric_vector():
    result = deserialize({"__type__": "numeric_vector", "value": [1.0, 2.0, 3.0]})
    assert isinstance(result, np.ndarray)
    assert result.dtype == np.float64
    np.testing.assert_array_equal(result, [1.0, 2.0, 3.0])

def test_integer_vector():
    result = deserialize({"__type__": "integer_vector", "value": [1, 2, 3]})
    assert isinstance(result, np.ndarray)
    assert result.dtype == np.int32

def test_logical_vector():
    result = deserialize({"__type__": "logical_vector", "value": [True, False]})
    assert isinstance(result, np.ndarray)
    assert result.dtype == bool

def test_character_vector():
    result = deserialize({"__type__": "character_vector", "value": ["a", "b"]})
    assert result == ["a", "b"]

def test_scalar_hint_unwraps_length1_vector():
    result = deserialize({"__type__": "numeric_vector", "value": [42.0]}, hint=TypeHint.SCALAR)
    assert result == pytest.approx(42.0)
    assert isinstance(result, float)

def test_list_hint_returns_list():
    result = deserialize({"__type__": "integer_vector", "value": [1, 2]}, hint=TypeHint.LIST)
    assert result == [1, 2]
    assert isinstance(result, list)


# --- Matrix ---

def test_matrix():
    result = deserialize({"__type__": "matrix", "nrow": 2, "ncol": 2, "data": [1.0, 2.0, 3.0, 4.0], "byrow": True})
    assert isinstance(result, np.ndarray)
    assert result.shape == (2, 2)
    np.testing.assert_array_equal(result, [[1.0, 2.0], [3.0, 4.0]])


# --- DataFrame ---

def test_dataframe():
    payload = {
        "__type__": "dataframe",
        "columns": {
            "x": {"__type__": "integer_vector", "value": [1, 2]},
            "y": {"__type__": "numeric_vector", "value": [3.0, 4.0]},
        }
    }
    result = deserialize(payload)
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["x", "y"]
    assert result["x"].tolist() == [1, 2]


# --- Factor ---

def test_factor():
    payload = {"__type__": "factor", "levels": ["a", "b", "c"], "codes": [1, 2, 1], "ordered": False}
    result = deserialize(payload)
    assert isinstance(result, pd.Categorical)
    assert list(result) == ["a", "b", "a"]


# --- Named / unnamed list ---

def test_named_list():
    payload = {"__type__": "named_list", "value": {"a": 1.0, "b": "x"}}
    result = deserialize(payload)
    assert result == {"a": 1.0, "b": "x"}

def test_unnamed_list():
    payload = {"__type__": "list", "value": [1.0, "a"]}
    result = deserialize(payload)
    assert result == [1.0, "a"]


# --- Raw passthrough ---

def test_raw_hint():
    payload = {"__type__": "numeric_vector", "value": [1.0]}
    result = deserialize(payload, hint=TypeHint.RAW)
    assert result == payload


# --- Custom deserializer ---

def test_register_deserializer_custom_dispatch():
    register_deserializer("mytype", lambda d: f"custom:{d['value']}")
    try:
        assert deserialize({"__type__": "mytype", "value": 42}) == "custom:42"
    finally:
        del _CUSTOM["mytype"]


# --- Unknown tag ---

def test_unknown_tag_raises():
    with pytest.raises(ValueError, match="unknown R type tag"):
        deserialize({"__type__": "totally_unknown_xyz"})


# --- Vector hint coverage ---

def test_integer_vector_scalar_hint():
    result = deserialize({"__type__": "integer_vector", "value": [7]}, hint=TypeHint.SCALAR)
    assert result == 7
    assert isinstance(result, int)

def test_logical_vector_scalar_hint():
    result = deserialize({"__type__": "logical_vector", "value": [True]}, hint=TypeHint.SCALAR)
    assert result is True

def test_logical_vector_list_hint():
    result = deserialize({"__type__": "logical_vector", "value": [True, False]}, hint=TypeHint.LIST)
    assert result == [True, False]
    assert isinstance(result, list)

def test_character_vector_scalar_hint():
    result = deserialize({"__type__": "character_vector", "value": ["hello"]}, hint=TypeHint.SCALAR)
    assert result == "hello"
    assert isinstance(result, str)


# --- Complex vector ---

def test_complex_vector():
    payload = {
        "__type__": "complex_vector",
        "value": [
            {"__type__": "complex", "r": 1.0, "i": 2.0},
            {"__type__": "complex", "r": 3.0, "i": 4.0},
        ],
    }
    result = deserialize(payload)
    assert isinstance(result, np.ndarray)
    assert result.dtype == np.complex128
    assert result[0] == 1 + 2j

def test_complex_vector_scalar_hint():
    payload = {"__type__": "complex_vector", "value": [{"__type__": "complex", "r": 1.0, "i": 2.0}]}
    result = deserialize(payload, hint=TypeHint.SCALAR)
    assert result == 1 + 2j


# --- Array (N-D) ---

def test_array_decode():
    payload = {"__type__": "array", "shape": [2, 3], "data": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}
    result = deserialize(payload)
    assert isinstance(result, np.ndarray)
    assert result.shape == (2, 3)
    assert result[0, 0] == 1.0
    assert result[1, 2] == 6.0
