import datetime as dt
from copy import copy, deepcopy
from functools import partial
from typing import NamedTuple

import pytest

from marshmallow import Schema, fields, utils
from tests.base import assert_date_equal, assert_time_equal, central


def test_missing_singleton_copy():
    assert copy(utils.missing) is utils.missing
    assert deepcopy(utils.missing) is utils.missing


class PointNT(NamedTuple):
    x: int
    y: int


class PointClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y


class PointDict(dict):
    def __init__(self, x, y):
        super().__init__({"x": x})
        self.y = y


@pytest.mark.parametrize(
    "obj", [PointNT(24, 42), PointClass(24, 42), PointDict(24, 42), {"x": 24, "y": 42}]
)
def test_get_value_from_object(obj):
    assert utils.get_value(obj, "x") == 24
    assert utils.get_value(obj, "y") == 42


def test_get_value_from_namedtuple_with_default():
    p = PointNT(x=42, y=None)
    # Default is only returned if key is not found
    assert utils.get_value(p, "z", default=123) == 123
    # since 'y' is an attribute, None is returned instead of the default
    assert utils.get_value(p, "y", default=123) is None


class Triangle:
    def __init__(self, p1, p2, p3):
        self.p1 = p1
        self.p2 = p2
        self.p3 = p3
        self.points = [p1, p2, p3]


def test_get_value_for_nested_object():
    tri = Triangle(p1=PointClass(1, 2), p2=PointNT(3, 4), p3={"x": 5, "y": 6})
    assert utils.get_value(tri, "p1.x") == 1
    assert utils.get_value(tri, "p2.x") == 3
    assert utils.get_value(tri, "p3.x") == 5


# regression test for https://github.com/marshmallow-code/marshmallow/issues/62
def test_get_value_from_dict():
    d = dict(items=["foo", "bar"], keys=["baz", "quux"])
    assert utils.get_value(d, "items") == ["foo", "bar"]
    assert utils.get_value(d, "keys") == ["baz", "quux"]


def test_get_value():
    lst = [1, 2, 3]
    assert utils.get_value(lst, 1) == 2

    class MyInt(int):
        pass

    assert utils.get_value(lst, MyInt(1)) == 2


def test_set_value():
    d = {}
    utils.set_value(d, "foo", 42)
    assert d == {"foo": 42}

    d = {}
    utils.set_value(d, "foo.bar", 42)
    assert d == {"foo": {"bar": 42}}

    d = {"foo": {}}
    utils.set_value(d, "foo.bar", 42)
    assert d == {"foo": {"bar": 42}}

    d = {"foo": 42}
    with pytest.raises(ValueError):
        utils.set_value(d, "foo.bar", 42)


def test_is_keyed_tuple():
    p = PointNT(24, 42)
    assert utils.is_keyed_tuple(p) is True
    t = (24, 42)
    assert utils.is_keyed_tuple(t) is False
    d = {"x": 42, "y": 24}
    assert utils.is_keyed_tuple(d) is False
    s = "xy"
    assert utils.is_keyed_tuple(s) is False
    lst = [24, 42]
    assert utils.is_keyed_tuple(lst) is False


def test_is_collection():
    assert utils.is_collection([1, "foo", {}]) is True
    assert utils.is_collection(("foo", 2.3)) is True
    assert utils.is_collection({"foo": "bar"}) is False


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        (dt.datetime(2013, 11, 10, 1, 23, 45), "Sun, 10 Nov 2013 01:23:45 -0000"),
        (
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=dt.timezone.utc),
            "Sun, 10 Nov 2013 01:23:45 +0000",
        ),
        (
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
            "Sun, 10 Nov 2013 01:23:45 -0600",
        ),
    ],
)
def test_rfc_format(value, expected):
    assert utils.rfcformat(value) == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        (dt.datetime(2013, 11, 10, 1, 23, 45), "2013-11-10T01:23:45"),
        (
            dt.datetime(2013, 11, 10, 1, 23, 45, 123456, tzinfo=dt.timezone.utc),
            "2013-11-10T01:23:45.123456+00:00",
        ),
        (
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=dt.timezone.utc),
            "2013-11-10T01:23:45+00:00",
        ),
        (
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
            "2013-11-10T01:23:45-06:00",
        ),
    ],
)
def test_isoformat(value, expected):
    assert utils.isoformat(value) == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("Sun, 10 Nov 2013 01:23:45 -0000", dt.datetime(2013, 11, 10, 1, 23, 45)),
        (
            "Sun, 10 Nov 2013 01:23:45 +0000",
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=dt.timezone.utc),
        ),
        (
            "Sun, 10 Nov 2013 01:23:45 -0600",
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
        ),
    ],
)
def test_from_rfc(value, expected):
    result = utils.from_rfc(value)
    assert type(result) is dt.datetime
    assert result == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("2013-11-10T01:23:45", dt.datetime(2013, 11, 10, 1, 23, 45)),
        (
            "2013-11-10T01:23:45+00:00",
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=dt.timezone.utc),
        ),
        (
            # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1251
            "2013-11-10T01:23:45.123+00:00",
            dt.datetime(2013, 11, 10, 1, 23, 45, 123000, tzinfo=dt.timezone.utc),
        ),
        (
            "2013-11-10T01:23:45.123456+00:00",
            dt.datetime(2013, 11, 10, 1, 23, 45, 123456, tzinfo=dt.timezone.utc),
        ),
        (
            "2013-11-10T01:23:45-06:00",
            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
        ),
    ],
)
def test_from_iso_datetime(value, expected):
    result = utils.from_iso_datetime(value)
    assert type(result) is dt.datetime
    assert result == expected


def test_from_iso_time_with_microseconds():
    t = dt.time(1, 23, 45, 6789)
    formatted = t.isoformat()
    result = utils.from_iso_time(formatted)
    assert type(result) is dt.time
    assert_time_equal(result, t)


def test_from_iso_time_without_microseconds():
    t = dt.time(1, 23, 45)
    formatted = t.isoformat()
    result = utils.from_iso_time(formatted)
    assert type(result) is dt.time
    assert_time_equal(result, t)


def test_from_iso_date():
    d = dt.date(2014, 8, 21)
    iso_date = d.isoformat()
    result = utils.from_iso_date(iso_date)
    assert type(result) is dt.date
    assert_date_equal(result, d)


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        (1676386740, dt.datetime(2023, 2, 14, 14, 59, 00)),
        (1676386740.58, dt.datetime(2023, 2, 14, 14, 59, 00, 580000)),
    ],
)
def test_from_timestamp(value, expected):
    result = utils.from_timestamp(value)
    assert type(result) is dt.datetime
    assert result == expected


def test_from_timestamp_with_negative_value():
    value = -10
    with pytest.raises(ValueError, match=r"Not a valid POSIX timestamp"):
        utils.from_timestamp(value)


def test_from_timestamp_with_overflow_value():
    value = 9223372036854775
    with pytest.raises(ValueError, match="out of range"):
        utils.from_timestamp(value)


def test_get_func_args():
    def f1(foo, bar):
        pass

    f2 = partial(f1, "baz")

    class F3:
        def __call__(self, foo, bar):
            pass

    f3 = F3()

    for func in [f1, f2, f3]:
        assert utils.get_func_args(func) == ["foo", "bar"]


# Regression test for https://github.com/marshmallow-code/marshmallow/issues/540
def test_function_field_using_type_annotation():
    def get_split_words(value: str):
        return value.split(";")

    class MySchema(Schema):
        friends = fields.Function(deserialize=get_split_words)

    data = {"friends": "Clark;Alfred;Robin"}
    result = MySchema().load(data)
    assert result == {"friends": ["Clark", "Alfred", "Robin"]}
