import copy
from unittest.mock import call

import pytest

import kopf
from kopf._cogs.structs.bodies import Body, RawBody, RawMeta

OWNER_API_VERSION = 'owner-api-version'
OWNER_NAMESPACE = 'owner-namespace'
OWNER_KIND = 'OwnerKind'
OWNER_NAME = 'owner-name'
OWNER_UID = 'owner-uid'
OWNER_LABELS = {'label-1': 'value-1', 'label-2': 'value-2'}
OWNER = RawBody(
    apiVersion=OWNER_API_VERSION,
    kind=OWNER_KIND,
    metadata=RawMeta(
        namespace=OWNER_NAMESPACE,
        name=OWNER_NAME,
        uid=OWNER_UID,
        labels=OWNER_LABELS,
    ),
)


def test_appending_to_dict():
    obj = {}

    kopf.append_owner_reference(
        obj, owner=Body(OWNER), controller=False, block_owner_deletion=False
    )

    assert 'metadata' in obj
    assert 'ownerReferences' in obj['metadata']
    assert isinstance(obj['metadata']['ownerReferences'], list)
    assert len(obj['metadata']['ownerReferences']) == 1
    assert isinstance(obj['metadata']['ownerReferences'][0], dict)
    assert obj['metadata']['ownerReferences'][0]['apiVersion'] == OWNER_API_VERSION
    assert obj['metadata']['ownerReferences'][0]['kind'] == OWNER_KIND
    assert obj['metadata']['ownerReferences'][0]['name'] == OWNER_NAME
    assert obj['metadata']['ownerReferences'][0]['uid'] == OWNER_UID
    assert obj['metadata']['ownerReferences'][0]['controller'] == False
    assert obj['metadata']['ownerReferences'][0]['blockOwnerDeletion'] == False


def test_appending_to_dicts(multicls):
    obj1 = {}
    obj2 = {}
    objs = multicls([obj1, obj2])

    kopf.append_owner_reference(objs, owner=Body(OWNER))

    assert isinstance(obj1['metadata']['ownerReferences'], list)
    assert len(obj1['metadata']['ownerReferences']) == 1
    assert obj1['metadata']['ownerReferences'][0]['apiVersion'] == OWNER_API_VERSION
    assert obj1['metadata']['ownerReferences'][0]['kind'] == OWNER_KIND
    assert obj1['metadata']['ownerReferences'][0]['name'] == OWNER_NAME
    assert obj1['metadata']['ownerReferences'][0]['uid'] == OWNER_UID

    assert isinstance(obj2['metadata']['ownerReferences'], list)
    assert len(obj2['metadata']['ownerReferences']) == 1
    assert obj2['metadata']['ownerReferences'][0]['apiVersion'] == OWNER_API_VERSION
    assert obj2['metadata']['ownerReferences'][0]['kind'] == OWNER_KIND
    assert obj2['metadata']['ownerReferences'][0]['name'] == OWNER_NAME
    assert obj2['metadata']['ownerReferences'][0]['uid'] == OWNER_UID


def test_appending_to_pykube_object(pykube_object):
    del pykube_object.obj['metadata']
    kopf.append_owner_reference(pykube_object, owner=Body(OWNER))
    assert 'metadata' in pykube_object.obj
    assert 'ownerReferences' in pykube_object.obj['metadata']
    assert isinstance(pykube_object.obj['metadata']['ownerReferences'], list)
    assert len(pykube_object.obj['metadata']['ownerReferences']) == 1
    assert isinstance(pykube_object.obj['metadata']['ownerReferences'][0], dict)
    assert pykube_object.obj['metadata']['ownerReferences'][0]['apiVersion'] == OWNER_API_VERSION
    assert pykube_object.obj['metadata']['ownerReferences'][0]['kind'] == OWNER_KIND
    assert pykube_object.obj['metadata']['ownerReferences'][0]['name'] == OWNER_NAME
    assert pykube_object.obj['metadata']['ownerReferences'][0]['uid'] == OWNER_UID


def test_appending_to_kubernetes_model(kubernetes_model):
    kubernetes_model.metadata = None
    kopf.append_owner_reference(kubernetes_model, owner=Body(OWNER))
    assert kubernetes_model.metadata is not None
    assert kubernetes_model.metadata.owner_references is not None
    assert isinstance(kubernetes_model.metadata.owner_references, list)
    assert len(kubernetes_model.metadata.owner_references) == 1
    assert kubernetes_model.metadata.owner_references[0].api_version == OWNER_API_VERSION
    assert kubernetes_model.metadata.owner_references[0].kind == OWNER_KIND
    assert kubernetes_model.metadata.owner_references[0].name == OWNER_NAME
    assert kubernetes_model.metadata.owner_references[0].uid == OWNER_UID


def test_appending_deduplicates_by_uid():
    """
    The uid is the only necessary criterion to identify same objects.
    No matter how we change the irrelevant non-id fields, they must be ignored.
    """
    owner1 = copy.deepcopy(OWNER)
    owner2 = copy.deepcopy(OWNER)
    owner3 = copy.deepcopy(OWNER)
    owner1['kind'] = 'KindA'
    owner2['kind'] = 'KindA'
    owner3['kind'] = 'KindB'
    owner1['metadata']['name'] = 'name-a'
    owner2['metadata']['name'] = 'name-b'
    owner3['metadata']['name'] = 'name-b'
    owner1['metadata']['uid'] = 'uid-0'
    owner2['metadata']['uid'] = 'uid-0'
    owner3['metadata']['uid'] = 'uid-0'
    obj = {}

    kopf.append_owner_reference(obj, owner=Body(owner1))
    kopf.append_owner_reference(obj, owner=Body(owner2))
    kopf.append_owner_reference(obj, owner=Body(owner3))

    assert len(obj['metadata']['ownerReferences']) == 1
    assert obj['metadata']['ownerReferences'][0]['uid'] == 'uid-0'


def test_appending_distinguishes_by_uid():
    """
    Changing only the uid should be sufficient to consider a new owner.
    Here, all other non-id fields are the same, and must be ignored.
    """
    owner1 = copy.deepcopy(OWNER)
    owner2 = copy.deepcopy(OWNER)
    owner1['metadata']['uid'] = 'uid-a'
    owner2['metadata']['uid'] = 'uid-b'
    obj = {}

    kopf.append_owner_reference(obj, owner=Body(owner1))
    kopf.append_owner_reference(obj, owner=Body(owner2))

    uids = {ref['uid'] for ref in obj['metadata']['ownerReferences']}
    assert uids == {'uid-a', 'uid-b'}


def test_removal_from_dict():
    obj = {}

    kopf.append_owner_reference(obj, owner=Body(OWNER))  # assumed to work, tested above
    kopf.remove_owner_reference(obj, owner=Body(OWNER))  # this one is being tested here

    assert 'metadata' in obj
    assert 'ownerReferences' in obj['metadata']
    assert isinstance(obj['metadata']['ownerReferences'], list)
    assert len(obj['metadata']['ownerReferences']) == 0


def test_removal_from_dicts(multicls):
    obj1 = {}
    obj2 = {}
    objs = multicls([obj1, obj2])

    kopf.append_owner_reference(objs, owner=Body(OWNER))  # assumed to work, tested above
    kopf.remove_owner_reference(objs, owner=Body(OWNER))  # this one is being tested here

    assert 'metadata' in obj1
    assert 'ownerReferences' in obj1['metadata']
    assert isinstance(obj1['metadata']['ownerReferences'], list)
    assert len(obj1['metadata']['ownerReferences']) == 0

    assert 'metadata' in obj2
    assert 'ownerReferences' in obj2['metadata']
    assert isinstance(obj2['metadata']['ownerReferences'], list)
    assert len(obj2['metadata']['ownerReferences']) == 0


def test_removal_from_pykube_object(pykube_object):
    del pykube_object.obj['metadata']
    kopf.append_owner_reference(pykube_object, owner=Body(OWNER))
    kopf.remove_owner_reference(pykube_object, owner=Body(OWNER))
    assert 'metadata' in pykube_object.obj
    assert 'ownerReferences' in pykube_object.obj['metadata']
    assert isinstance(pykube_object.obj['metadata']['ownerReferences'], list)
    assert len(pykube_object.obj['metadata']['ownerReferences']) == 0


def test_removal_from_kubernetes_model(kubernetes_model):
    kubernetes_model.metadata = None
    kopf.append_owner_reference(kubernetes_model, owner=Body(OWNER))
    kopf.remove_owner_reference(kubernetes_model, owner=Body(OWNER))
    assert kubernetes_model.metadata is not None
    assert kubernetes_model.metadata.owner_references is not None
    assert isinstance(kubernetes_model.metadata.owner_references, list)
    assert len(kubernetes_model.metadata.owner_references) == 0


def test_removal_identifies_by_uid():
    owner1 = copy.deepcopy(OWNER)
    owner2 = copy.deepcopy(OWNER)
    owner3 = copy.deepcopy(OWNER)
    owner1['kind'] = 'KindA'
    owner2['kind'] = 'KindA'
    owner3['kind'] = 'KindB'
    owner1['metadata']['name'] = 'name-a'
    owner2['metadata']['name'] = 'name-b'
    owner3['metadata']['name'] = 'name-b'
    owner1['metadata']['uid'] = 'uid-0'
    owner2['metadata']['uid'] = 'uid-0'
    owner3['metadata']['uid'] = 'uid-0'
    obj = {}

    # Three different owners added, but all have the same uid.
    # One is removed and only once, all must be gone (due to same uids).
    kopf.append_owner_reference(obj, owner=Body(owner1))  # assumed to work, tested above
    kopf.append_owner_reference(obj, owner=Body(owner2))  # assumed to work, tested above
    kopf.append_owner_reference(obj, owner=Body(owner3))  # assumed to work, tested above
    kopf.remove_owner_reference(obj, owner=Body(owner1))  # this one is being tested here

    assert len(obj['metadata']['ownerReferences']) == 0


def test_removal_distinguishes_by_uid():
    owner1 = copy.deepcopy(OWNER)
    owner2 = copy.deepcopy(OWNER)
    owner3 = copy.deepcopy(OWNER)
    owner1['metadata']['uid'] = 'uid-a'
    owner2['metadata']['uid'] = 'uid-b'
    owner3['metadata']['uid'] = 'uid-c'
    obj = {}

    # Three very similar owners added, different only by uid.
    # One is removed, others must stay (even if kinds/names are the same).
    kopf.append_owner_reference(obj, owner=Body(owner1))  # assumed to work, tested above
    kopf.append_owner_reference(obj, owner=Body(owner2))  # assumed to work, tested above
    kopf.append_owner_reference(obj, owner=Body(owner3))  # assumed to work, tested above
    kopf.remove_owner_reference(obj, owner=Body(owner1))  # this one is being tested here

    uids = {ref['uid'] for ref in obj['metadata']['ownerReferences']}
    assert uids == {'uid-b', 'uid-c'}


# Not related to owner references only, but uses the OWNER constants.
@pytest.mark.parametrize('nested', [
    pytest.param(('spec.template',), id='tuple'),
    pytest.param(['spec.template'], id='list'),
    pytest.param({'spec.template'}, id='set'),
    pytest.param('spec.template', id='string'),
])
@pytest.mark.parametrize('strict', [True, False])
@pytest.mark.parametrize('forced', [True, False])
def test_adopting(mocker, forced, strict, nested):
    # These methods are tested in their own tests.
    # We just check that they are called at all.
    append_owner_ref = mocker.patch('kopf._kits.hierarchies.append_owner_reference')
    harmonize_naming = mocker.patch('kopf._kits.hierarchies.harmonize_naming')
    adjust_namespace = mocker.patch('kopf._kits.hierarchies.adjust_namespace')
    label = mocker.patch('kopf._kits.hierarchies.label')

    obj = {}
    kopf.adopt(obj, owner=Body(OWNER), forced=forced, strict=strict, nested=nested)

    assert append_owner_ref.called
    assert harmonize_naming.called
    assert adjust_namespace.called
    assert label.called

    assert append_owner_ref.call_args == call(obj, owner=Body(OWNER))
    assert harmonize_naming.call_args == call(obj, name=OWNER_NAME, forced=forced, strict=strict)
    assert adjust_namespace.call_args == call(obj, namespace=OWNER_NAMESPACE, forced=forced)
    assert label.call_args == call(obj, labels=OWNER_LABELS, nested=nested, forced=forced)
