import datetime

import pytest

import stix2
import stix2.exceptions
import stix2.properties
import stix2.utils
import stix2.v21
import stix2.versioning

from .constants import CAMPAIGN_MORE_KWARGS


def test_making_new_version():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)

    campaign_v2 = campaign_v1.new_version(name="fred")

    assert campaign_v1.id == campaign_v2.id
    assert campaign_v1.spec_version == campaign_v2.spec_version
    assert campaign_v1.created_by_ref == campaign_v2.created_by_ref
    assert campaign_v1.created == campaign_v2.created
    assert campaign_v1.name != campaign_v2.name
    assert campaign_v2.name == "fred"
    assert campaign_v1.description == campaign_v2.description
    assert campaign_v1.modified < campaign_v2.modified


def test_making_new_version_with_unset():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)

    campaign_v2 = campaign_v1.new_version(description=None)

    assert campaign_v1.id == campaign_v2.id
    assert campaign_v1.spec_version == campaign_v2.spec_version
    assert campaign_v1.created_by_ref == campaign_v2.created_by_ref
    assert campaign_v1.created == campaign_v2.created
    assert campaign_v1.name == campaign_v2.name
    with pytest.raises(AttributeError):
        assert campaign_v2.description
    assert campaign_v1.modified < campaign_v2.modified


def test_making_new_version_with_embedded_object():
    campaign_v1 = stix2.v21.Campaign(
        external_references=[{
            "source_name": "capec",
            "external_id": "CAPEC-163",
        }],
        **CAMPAIGN_MORE_KWARGS
    )

    campaign_v2 = campaign_v1.new_version(
        external_references=[{
                "source_name": "capec",
                "external_id": "CAPEC-164",
        }],
    )

    assert campaign_v1.id == campaign_v2.id
    assert campaign_v1.spec_version == campaign_v2.spec_version
    assert campaign_v1.created_by_ref == campaign_v2.created_by_ref
    assert campaign_v1.created == campaign_v2.created
    assert campaign_v1.name == campaign_v2.name
    assert campaign_v1.description == campaign_v2.description
    assert campaign_v1.modified < campaign_v2.modified
    assert campaign_v1.external_references[0].external_id != campaign_v2.external_references[0].external_id


def test_revoke():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)

    campaign_v2 = campaign_v1.revoke()

    assert campaign_v1.id == campaign_v2.id
    assert campaign_v1.spec_version == campaign_v2.spec_version
    assert campaign_v1.created_by_ref == campaign_v2.created_by_ref
    assert campaign_v1.created == campaign_v2.created
    assert campaign_v1.name == campaign_v2.name
    assert campaign_v1.description == campaign_v2.description
    assert campaign_v1.modified < campaign_v2.modified

    assert campaign_v2.revoked


def test_versioning_error_invalid_property():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)

    with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as excinfo:
        campaign_v1.new_version(type="threat-actor")

    assert str(excinfo.value) == "These properties cannot be changed when making a new version: type."


def test_versioning_error_bad_modified_value():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)

    with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
        campaign_v1.new_version(modified="2015-04-06T20:03:00.000Z")

    assert excinfo.value.cls == stix2.v21.Campaign
    assert excinfo.value.prop_name == "modified"
    assert excinfo.value.reason == (
        "The new modified datetime cannot be before than or equal to the current modified datetime."
        "It cannot be equal, as according to STIX 2 specification, objects that are different "
        "but have the same id and modified timestamp do not have defined consumer behavior."
    )

    msg = "Invalid value for {0} '{1}': {2}"
    msg = msg.format(
        stix2.v21.Campaign.__name__, "modified",
        "The new modified datetime cannot be before than or equal to the current modified datetime."
        "It cannot be equal, as according to STIX 2 specification, objects that are different "
        "but have the same id and modified timestamp do not have defined consumer behavior.",
    )
    assert str(excinfo.value) == msg


def test_versioning_error_usetting_required_property():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)

    with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo:
        campaign_v1.new_version(name=None)

    assert excinfo.value.cls == stix2.v21.Campaign
    assert excinfo.value.properties == ["name"]

    msg = "No values for required properties for {0}: ({1})."
    msg = msg.format(stix2.v21.Campaign.__name__, "name")
    assert str(excinfo.value) == msg


def test_versioning_error_new_version_of_revoked():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)
    campaign_v2 = campaign_v1.revoke()

    with pytest.raises(stix2.exceptions.RevokeError) as excinfo:
        campaign_v2.new_version(name="barney")
    assert str(excinfo.value) == "Cannot create a new version of a revoked object."

    assert excinfo.value.called_by == "new_version"
    assert str(excinfo.value) == "Cannot create a new version of a revoked object."


def test_versioning_error_revoke_of_revoked():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)
    campaign_v2 = campaign_v1.revoke()

    with pytest.raises(stix2.exceptions.RevokeError) as excinfo:
        campaign_v2.revoke()
    assert str(excinfo.value) == "Cannot revoke an already revoked object."

    assert excinfo.value.called_by == "revoke"
    assert str(excinfo.value) == "Cannot revoke an already revoked object."


def test_making_new_version_dict():
    campaign_v1 = CAMPAIGN_MORE_KWARGS
    campaign_v2 = stix2.versioning.new_version(CAMPAIGN_MORE_KWARGS, name="fred")

    assert campaign_v1['id'] == campaign_v2['id']
    assert campaign_v1['spec_version'] == campaign_v2['spec_version']
    assert campaign_v1['created_by_ref'] == campaign_v2['created_by_ref']
    assert campaign_v1['created'] == campaign_v2['created']
    assert campaign_v1['name'] != campaign_v2['name']
    assert campaign_v2['name'] == "fred"
    assert campaign_v1['description'] == campaign_v2['description']
    assert stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified']


def test_versioning_error_dict_bad_modified_value():
    with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
        stix2.versioning.new_version(CAMPAIGN_MORE_KWARGS, modified="2015-04-06T20:03:00.000Z")

    assert excinfo.value.cls == dict
    assert excinfo.value.prop_name == "modified"
    assert excinfo.value.reason == "The new modified datetime cannot be before than or equal to the current modified datetime." \
        "It cannot be equal, as according to STIX 2 specification, objects that are different " \
        "but have the same id and modified timestamp do not have defined consumer behavior."


def test_versioning_dict_unregistered_no_modified():
    d = {
        "type": "not-registered",
        "id": "not-registered--4da54535-47b7-468c-88fa-d13b04033c4b",
        "spec_version": "2.1",
        "created": "1995-04-07T15:37:48.178Z",
    }

    new_d = stix2.versioning.new_version(d)
    assert "modified" in new_d
    assert new_d["modified"] > stix2.utils.parse_into_datetime(d["created"])

    new_d = stix2.versioning.new_version(d, modified="1996-11-20T01:19:29.134Z")
    assert new_d["modified"] == "1996-11-20T01:19:29.134Z"


def test_versioning_dict_unregistered_unversionable():
    d = {
        "type": "not-registered",
        "id": "not-registered--4da54535-47b7-468c-88fa-d13b04033c4b",
        "spec_version": "2.1",
        "modified": "1995-04-07T15:37:48.178Z",
    }

    with pytest.raises(stix2.exceptions.ObjectNotVersionableError):
        stix2.versioning.new_version(d)

    with pytest.raises(stix2.exceptions.ObjectNotVersionableError):
        # should fail even if we provide a "created" kwarg.
        stix2.versioning.new_version(d, created="1985-06-29T06:09:51.157Z")


def test_versioning_custom_object():
    @stix2.v21.CustomObject(
        "x-versionable-all-optional-21", [
            ("created", stix2.properties.TimestampProperty()),
            ("modified", stix2.properties.TimestampProperty()),
            ("revoked", stix2.properties.BooleanProperty()),
        ],
    )
    class CustomSDO:
        pass

    obj = CustomSDO(created="1990-12-18T17:56:11.346234Z")
    new_obj = stix2.versioning.new_version(obj)

    assert "modified" in new_obj
    assert new_obj["modified"] > new_obj["created"]

    obj = CustomSDO()
    with pytest.raises(stix2.exceptions.ObjectNotVersionableError):
        # fails due to insufficient properties on the object, even though its
        # type supports versioning.
        stix2.versioning.new_version(obj)


def test_versioning_error_dict_no_modified_value():
    campaign_v1 = {
        'type': 'campaign',
        'id': "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
        'created': "2016-04-06T20:03:00.000Z",
        'name': "Green Group Attacks Against Finance",
    }
    campaign_v2 = stix2.versioning.new_version(campaign_v1, modified="2017-04-06T20:03:00.000Z")

    assert str(campaign_v2['modified']) == "2017-04-06T20:03:00.000Z"


def test_making_new_version_invalid_cls():
    campaign_v1 = "This is a campaign."
    with pytest.raises(stix2.exceptions.TypeNotVersionableError) as excinfo:
        stix2.versioning.new_version(campaign_v1, name="fred")

    assert excinfo.value.object is campaign_v1


def test_revoke_dict():
    campaign_v1 = CAMPAIGN_MORE_KWARGS
    campaign_v2 = stix2.versioning.revoke(campaign_v1)

    assert campaign_v1['id'] == campaign_v2['id']
    assert campaign_v1['spec_version'] == campaign_v2['spec_version']
    assert campaign_v1['created_by_ref'] == campaign_v2['created_by_ref']
    assert campaign_v1['created'] == campaign_v2['created']
    assert campaign_v1['name'] == campaign_v2['name']
    assert campaign_v1['description'] == campaign_v2['description']
    assert stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified']

    assert campaign_v2['revoked']


def test_revoke_unversionable():
    sco = stix2.v21.File(name="data.txt")
    with pytest.raises(stix2.exceptions.TypeNotVersionableError):
        sco.revoke()


def test_versioning_error_revoke_of_revoked_dict():
    campaign_v1 = CAMPAIGN_MORE_KWARGS
    campaign_v2 = stix2.versioning.revoke(campaign_v1)

    with pytest.raises(stix2.exceptions.RevokeError) as excinfo:
        stix2.versioning.revoke(campaign_v2)

    assert excinfo.value.called_by == "revoke"


def test_revoke_invalid_cls():
    campaign_v1 = "This is a campaign."
    with pytest.raises(ValueError) as excinfo:
        stix2.versioning.revoke(campaign_v1)

    assert 'cannot revoke object of this type' in str(excinfo.value)


def test_remove_custom_stix_property():
    mal = stix2.v21.Malware(
        name="ColePowers",
        malware_types=["rootkit"],
        x_custom="armada",
        allow_custom=True,
        is_family=False,
    )

    mal_nc = stix2.versioning.remove_custom_stix(mal)

    assert "x_custom" not in mal_nc
    assert mal["modified"] < mal_nc["modified"]


def test_remove_custom_stix_object():
    @stix2.v21.CustomObject(
        "x-animal", [
            ("species", stix2.properties.StringProperty(required=True)),
            ("animal_class", stix2.properties.StringProperty()),
        ],
    )
    class Animal(object):
        pass

    animal = Animal(species="lion", animal_class="mammal")

    nc = stix2.versioning.remove_custom_stix(animal)

    assert nc is None


def test_remove_custom_stix_no_custom():
    campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS)
    campaign_v2 = stix2.versioning.remove_custom_stix(campaign_v1)

    assert len(campaign_v1.keys()) == len(campaign_v2.keys())
    assert campaign_v1.id == campaign_v2.id
    assert campaign_v1.description == campaign_v2.description


@pytest.mark.parametrize(
    "old, candidate_new, expected_new, use_stix21", [
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", "1999-08-15T00:19:07.001Z", False),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", False),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.000Z", "1999-08-15T00:19:07.001Z", False),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.999Z", "1999-08-15T00:19:07.001Z", False),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.0001Z", "1999-08-15T00:19:07.001Z", False),
        ("1999-08-15T00:19:07.999Z", "1999-08-15T00:19:07.9999Z", "1999-08-15T00:19:08.000Z", False),

        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", "1999-08-15T00:19:07.001Z", True),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000001Z", True),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.000Z", "1999-08-15T00:19:07.000001Z", True),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.999999Z", "1999-08-15T00:19:07.000001Z", True),
        ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000001Z", "1999-08-15T00:19:07.000001Z", True),
        ("1999-08-15T00:19:07.999Z", "1999-08-15T00:19:07.999999Z", "1999-08-15T00:19:07.999999Z", True),
    ],
)
def test_fudge_modified(old, candidate_new, expected_new, use_stix21):
    old_dt = datetime.datetime.strptime(old, "%Y-%m-%dT%H:%M:%S.%fZ")
    candidate_new_dt = datetime.datetime.strptime(
        candidate_new, "%Y-%m-%dT%H:%M:%S.%fZ",
    )
    expected_new_dt = datetime.datetime.strptime(
        expected_new, "%Y-%m-%dT%H:%M:%S.%fZ",
    )

    fudged = stix2.versioning._fudge_modified(
        old_dt, candidate_new_dt, use_stix21,
    )
    assert fudged == expected_new_dt


def test_version_unversionable_dict():
    f = {
        "type": "file",
        "id": "file--4efb5217-e987-4438-9a1b-c800099401df",
        "name": "data.txt",
    }

    with pytest.raises(stix2.exceptions.TypeNotVersionableError):
        stix2.versioning.new_version(f)


def test_version_sco_with_custom():
    """
    If we add custom properties named like versioning properties to an object
    type which is otherwise unversionable, versioning should start working.
    """

    file_sco_obj = stix2.v21.File(
        name="data.txt",
        created="1973-11-23T02:31:37Z",
        modified="1991-05-13T19:24:57Z",
        revoked=False,
        allow_custom=True,
    )

    new_file_sco_obj = stix2.versioning.new_version(
        file_sco_obj, size=1234,
    )

    assert new_file_sco_obj.size == 1234

    revoked_obj = stix2.versioning.revoke(new_file_sco_obj)
    assert revoked_obj.revoked

    # Same thing with a dict
    d = {
        "type": "file",
        "id": "file--d287f10a-98b4-4a47-8fa0-64b12695ea58",
        "spec_version": "2.1",
        "name": "data.txt",
        "created": "1973-11-23T02:31:37Z",
        "modified": "1991-05-13T19:24:57Z",
        "revoked": False,
    }

    new_d = stix2.versioning.new_version(d, size=1234)
    assert new_d["size"] == 1234

    revoked_d = stix2.versioning.revoke(new_d)
    assert revoked_d["revoked"]


def test_version_sco_id_contributing_properties():
    file_sco_obj = stix2.v21.File(
        name="data.txt",
        created="1973-11-23T02:31:37Z",
        modified="1991-05-13T19:24:57Z",
        revoked=False,
        allow_custom=True,
    )

    with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as e:
        stix2.versioning.new_version(file_sco_obj, name="foo.dat")

    assert e.value.unchangable_properties == {"name"}


def test_version_sco_id_contributing_properties_dict():
    file_sco_dict = {
        "type": "file",
        "id": "file--c27c572c-2e17-5ce1-817e-67bb97629a56",
        "spec_version": "2.1",
        "name": "data.txt",
        "created": "1973-11-23T02:31:37Z",
        "modified": "1991-05-13T19:24:57Z",
        "revoked": False,
    }

    with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as e:
        stix2.versioning.new_version(file_sco_dict, name="foo.dat")

    assert e.value.unchangable_properties == {"name"}


def test_version_marking():
    m = stix2.v21.MarkingDefinition(
        name="a name",
        created="1982-11-29T12:20:13.723Z",
        definition_type="statement",
        definition={"statement": "Copyright (c) 2000-2020 Acme Corp"},
    )

    with pytest.raises(stix2.exceptions.TypeNotVersionableError):
        stix2.versioning.new_version(m)

    m = {
        "type": "marking-definition",
        "id": "marking-definition--2a9f3f6e-5cbd-423b-a40d-02aefd29e612",
        "spec_version": "2.1",
        "name": "a name",
        "created": "1982-11-29T12:20:13.723Z",
        "definition_type": "statement",
        "definition": {
            "statement": "Copyright (c) 2000-2020 Acme Corp",
        },
    }

    with pytest.raises(stix2.exceptions.TypeNotVersionableError):
        stix2.versioning.new_version(m)


def test_version_disable_custom():
    m = stix2.v21.Malware(
        name="foo", description="Steals your identity!", is_family=False,
        x_custom=123, allow_custom=True,
    )

    # Remove the custom property, and disallow custom properties in the
    # resulting object.
    m2 = stix2.versioning.new_version(m, x_custom=None, allow_custom=False)
    assert "x_custom" not in m2

    # Remove a regular property and leave the custom one, disallow custom
    # properties, and make sure we get an error.
    with pytest.raises(stix2.exceptions.ExtraPropertiesError):
        stix2.versioning.new_version(m, description=None, allow_custom=False)


def test_version_enable_custom():
    m = stix2.v21.Malware(
        name="foo", description="Steals your identity!", is_family=False,
    )

    # Add a custom property to an object for which it was previously disallowed
    m2 = stix2.versioning.new_version(m, x_custom=123, allow_custom=True)
    assert "x_custom" in m2

    # Add a custom property without enabling it, make sure we get an error
    with pytest.raises(stix2.exceptions.ExtraPropertiesError):
        stix2.versioning.new_version(m, x_custom=123, allow_custom=False)


def test_version_propagate_custom():
    m = stix2.v21.Malware(
        name="foo", is_family=False,
    )

    # Remember custom-not-allowed setting from original; produce error
    with pytest.raises(stix2.exceptions.ExtraPropertiesError):
        stix2.versioning.new_version(m, x_custom=123)

    m2 = stix2.versioning.new_version(m, description="Steals your identity!")
    assert "description" in m2
    assert m2.description == "Steals your identity!"

    m_custom = stix2.v21.Malware(
        name="foo", is_family=False, x_custom=123, allow_custom=True,
    )

    # Remember custom-allowed setting from original; should work
    m2_custom = stix2.versioning.new_version(m_custom, x_other_custom="abc")
    assert "x_other_custom" in m2_custom
    assert m2_custom.x_other_custom == "abc"
