import datetime as dt

import pytest
import pytz

from misp_stix2.base import _STIXBase
from misp_stix2.exceptions import (
    CustomContentError, ExtraPropertiesError, STIXError,
)
from misp_stix2.properties import (
    BinaryProperty, BooleanProperty, EmbeddedObjectProperty, EnumProperty,
    FloatProperty, HashesProperty, HexProperty, IntegerProperty, ListProperty,
    OpenVocabProperty, Property, StringProperty, TimestampProperty,
    TypeProperty,
)


def test_property():
    p = Property()

    assert p.required is False
    assert p.clean('foo') == ('foo', False)
    assert p.clean(3) == (3, False)


def test_basic_clean():
    class Prop(Property):

        def clean(self, value):
            if value == 42:
                return value
            else:
                raise ValueError("Must be 42")

    p = Prop()

    assert p.clean(42) == 42
    with pytest.raises(ValueError):
        p.clean(41)


def test_property_default():
    class Prop(Property):

        def default(self):
            return 77

    p = Prop()

    assert p.default() == 77


def test_fixed_property():
    p = Property(fixed="2.0")

    assert p.clean("2.0")
    with pytest.raises(ValueError):
        assert p.clean("x") is False
    with pytest.raises(ValueError):
        assert p.clean(2.0) is False

    assert p.default() == "2.0"
    assert p.clean(p.default())


def test_property_fixed_and_required():
    with pytest.raises(STIXError):
        Property(default=lambda: 3, required=True)


def test_list_property_property_type():
    p = ListProperty(StringProperty)

    result = p.clean(['abc', 'xyz'], False)
    assert result == (['abc', 'xyz'], False)

    with pytest.raises(ValueError):
        p.clean([], False)


def test_list_property_property_type_custom():
    class TestObj(_STIXBase):
        _type = "test"
        _properties = {
            "foo": StringProperty(),
        }
    p = ListProperty(EmbeddedObjectProperty(type=TestObj))

    objs_custom = [
        TestObj(foo="abc", bar=123, allow_custom=True),
        TestObj(foo="xyz"),
    ]

    result = p.clean(objs_custom, True)
    assert result == (objs_custom, True)

    with pytest.raises(CustomContentError):
        p.clean(objs_custom, False)

    dicts_custom = [
        {"foo": "abc", "bar": 123},
        {"foo": "xyz"},
    ]

    result = p.clean(dicts_custom, True)
    assert result == (objs_custom, True)

    with pytest.raises(ExtraPropertiesError):
        p.clean(dicts_custom, False)


def test_list_property_object_type():
    class TestObj(_STIXBase):
        _type = "test"
        _properties = {
            "foo": StringProperty(),
        }
    p = ListProperty(TestObj)

    objs = [TestObj(foo="abc"), TestObj(foo="xyz")]
    result = p.clean(objs, False)
    assert result == (objs, False)

    dicts = [{"foo": "abc"}, {"foo": "xyz"}]
    result = p.clean(dicts, False)
    assert result == (objs, False)


def test_list_property_object_type_custom():
    class TestObj(_STIXBase):
        _type = "test"
        _properties = {
            "foo": StringProperty(),
        }
    p = ListProperty(TestObj)

    objs_custom = [
        TestObj(foo="abc", bar=123, allow_custom=True),
        TestObj(foo="xyz"),
    ]

    result = p.clean(objs_custom, True)
    assert result == (objs_custom, True)

    with pytest.raises(CustomContentError):
        p.clean(objs_custom, False)

    dicts_custom = [
        {"foo": "abc", "bar": 123},
        {"foo": "xyz"},
    ]

    result = p.clean(dicts_custom, True)
    assert result == (objs_custom, True)

    with pytest.raises(ExtraPropertiesError):
        p.clean(dicts_custom, False)


def test_list_property_bad_element_type():
    with pytest.raises(TypeError):
        ListProperty(1)


def test_list_property_bad_value_type():
    class TestObj(_STIXBase):
        _type = "test"
        _properties = {
            "foo": StringProperty(),
        }

    list_prop = ListProperty(TestObj)
    with pytest.raises(ValueError):
        list_prop.clean([1], False)


def test_string_property():
    prop = StringProperty()

    assert prop.clean('foobar')
    assert prop.clean(1)
    assert prop.clean([1, 2, 3])


def test_type_property():
    prop = TypeProperty('my-type')

    assert prop.clean('my-type')
    with pytest.raises(ValueError):
        prop.clean('not-my-type')
    assert prop.clean(prop.default())


@pytest.mark.parametrize(
    "value", [
        2,
        -1,
        3.14,
        False,
    ],
)
def test_integer_property_valid(value):
    int_prop = IntegerProperty()
    assert int_prop.clean(value) is not None


@pytest.mark.parametrize(
    "value", [
        -1,
        -100,
        -50 * 6,
    ],
)
def test_integer_property_invalid_min_with_constraints(value):
    int_prop = IntegerProperty(min=0, max=180)
    with pytest.raises(ValueError) as excinfo:
        int_prop.clean(value)
    assert "minimum value is" in str(excinfo.value)


@pytest.mark.parametrize(
    "value", [
        181,
        200,
        50 * 6,
    ],
)
def test_integer_property_invalid_max_with_constraints(value):
    int_prop = IntegerProperty(min=0, max=180)
    with pytest.raises(ValueError) as excinfo:
        int_prop.clean(value)
    assert "maximum value is" in str(excinfo.value)


@pytest.mark.parametrize(
    "value", [
        "something",
        StringProperty(),
    ],
)
def test_integer_property_invalid(value):
    int_prop = IntegerProperty()
    with pytest.raises(ValueError):
        int_prop.clean(value)


@pytest.mark.parametrize(
    "value", [
        2,
        -1,
        3.14,
        False,
    ],
)
def test_float_property_valid(value):
    int_prop = FloatProperty()
    assert int_prop.clean(value) is not None


@pytest.mark.parametrize(
    "value", [
        "something",
        StringProperty(),
    ],
)
def test_float_property_invalid(value):
    int_prop = FloatProperty()
    with pytest.raises(ValueError):
        int_prop.clean(value)


@pytest.mark.parametrize(
    "value", [
        True,
        False,
        'True',
        'False',
        'true',
        'false',
        'TRUE',
        'FALSE',
        'T',
        'F',
        't',
        'f',
        1,
        0,
    ],
)
def test_boolean_property_valid(value):
    bool_prop = BooleanProperty()

    assert bool_prop.clean(value) is not None


@pytest.mark.parametrize(
    "value", [
        'abc',
        ['false'],
        {'true': 'true'},
        2,
        -1,
    ],
)
def test_boolean_property_invalid(value):
    bool_prop = BooleanProperty()
    with pytest.raises(ValueError):
        bool_prop.clean(value)


@pytest.mark.parametrize(
    "value", [
        '2017-01-01T12:34:56Z',
    ],
)
def test_timestamp_property_valid(value):
    ts_prop = TimestampProperty()
    assert ts_prop.clean(value) == (dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc), False)


def test_timestamp_property_invalid():
    ts_prop = TimestampProperty()
    with pytest.raises(TypeError):
        ts_prop.clean(1)
    with pytest.raises(ValueError):
        ts_prop.clean("someday sometime")


def test_binary_property():
    bin_prop = BinaryProperty()

    assert bin_prop.clean("TG9yZW0gSXBzdW0=")
    with pytest.raises(ValueError):
        bin_prop.clean("foobar")


def test_hex_property():
    hex_prop = HexProperty()

    assert hex_prop.clean("4c6f72656d20497073756d")
    with pytest.raises(ValueError):
        hex_prop.clean("foobar")


@pytest.mark.parametrize(
    "value", [
        ['a', 'b', 'c'],
        ('a', 'b', 'c'),
        'b',
    ],
)
def test_enum_property_valid(value):
    enum_prop = EnumProperty(value)
    assert enum_prop.clean('b', False)


def test_enum_property_clean():
    enum_prop = EnumProperty(['1'])
    assert enum_prop.clean(1, False) == ('1', False)


def test_enum_property_invalid():
    enum_prop = EnumProperty(['a', 'b', 'c'])
    with pytest.raises(ValueError):
        enum_prop.clean('z', False)

    with pytest.raises(ValueError):
        enum_prop.clean('z', True)


@pytest.mark.xfail(
    reason="Temporarily disabled custom open vocab enforcement",
    strict=True,
)
@pytest.mark.parametrize(
    "vocab", [
        ['a', 'b', 'c'],
        ('a', 'b', 'c'),
        'b',
    ],
)
def test_openvocab_property(vocab):
    ov_prop = OpenVocabProperty(vocab)

    assert ov_prop.clean("b", False) == ("b", False)
    assert ov_prop.clean("b", True) == ("b", False)

    with pytest.raises(CustomContentError):
        ov_prop.clean("d", False)

    assert ov_prop.clean("d", True) == ("d", True)


@pytest.mark.parametrize(
    "value", [
        {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"},
        [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')],
    ],
)
def test_hashes_property_valid(value):
    hash_prop = HashesProperty(["sha256", "md5", "ripemd160"])
    _, has_custom = hash_prop.clean(value, False)
    assert not has_custom


@pytest.mark.parametrize(
    "value", [
        {"MD5": "a"},
        {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"},
    ],
)
def test_hashes_property_invalid(value):
    hash_prop = HashesProperty(["sha256", "md5"])

    with pytest.raises(ValueError):
        hash_prop.clean(value, False)


def test_hashes_property_custom():
    value = {
        "sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b",
        "abc-123": "aaaaaaaaaaaaaaaaaaaaa",
    }
    expected_cleaned_value = {
        # cleaning transforms recognized hash algorithm names to the spec-
        # mandated name.
        "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b",
        "abc-123": "aaaaaaaaaaaaaaaaaaaaa",
    }

    hash_prop = HashesProperty(["SHA-256"])
    result = hash_prop.clean(value, True)
    assert result == (expected_cleaned_value, True)

    with pytest.raises(CustomContentError):
        hash_prop.clean(value, False)


def test_hashes_no_library_support():
    prop = HashesProperty(["foo"])

    result = prop.clean({"foo": "bar"}, False)
    assert result == ({"foo": "bar"}, False)

    result = prop.clean({"foo": "bar"}, True)
    assert result == ({"foo": "bar"}, False)

    with pytest.raises(CustomContentError):
        # require exact name match for unsupported hash algorithms
        prop.clean({"FOO": "bar"}, False)

    result = prop.clean({"FOO": "bar"}, True)
    assert result == ({"FOO": "bar"}, True)
