import asyncio

import pytest

import kopf
from kopf._cogs.structs.ephemera import Memo
from kopf._core.engines.indexing import OperatorIndexers
from kopf._core.intents.causes import Reason
from kopf._core.reactor.inventory import ResourceMemories
from kopf._core.reactor.processing import process_resource_event

LAST_SEEN_ANNOTATION = 'kopf.zalando.org/last-handled-configuration'

EVENT_TYPES = [None, 'ADDED', 'MODIFIED', 'DELETED']
EVENT_TYPES_WHEN_EXISTS = [None, 'ADDED', 'MODIFIED']


@pytest.mark.parametrize('event_type', EVENT_TYPES_WHEN_EXISTS)
async def test_create(registry, settings, handlers, resource, cause_mock, event_type,
                      assert_logs, k8s_mocked):
    cause_mock.reason = Reason.CREATE

    settings.posting.loggers = True
    event_queue = asyncio.Queue()
    await process_resource_event(
        lifecycle=kopf.lifecycles.all_at_once,
        registry=registry,
        settings=settings,
        resource=resource,
        indexers=OperatorIndexers(),
        memories=ResourceMemories(),
        memobase=Memo(),
        raw_event={'type': event_type, 'object': {}},
        event_queue=event_queue,
    )

    assert handlers.create_mock.call_count == 1
    assert not handlers.update_mock.called
    assert not handlers.delete_mock.called

    assert k8s_mocked.patch.call_count == 1
    assert not event_queue.empty()

    patch = k8s_mocked.patch.call_args_list[0].kwargs['payload']
    assert 'metadata' in patch
    assert 'annotations' in patch['metadata']
    assert LAST_SEEN_ANNOTATION in patch['metadata']['annotations']

    assert_logs([
        "Creation is in progress:",
        "Handler 'create_fn' is invoked",
        "Handler 'create_fn' succeeded",
        "Creation is processed:",
        "Merge-patching",
    ])


@pytest.mark.parametrize('event_type', EVENT_TYPES_WHEN_EXISTS)
async def test_update(registry, settings, handlers, resource, cause_mock, event_type,
                      assert_logs, k8s_mocked):
    cause_mock.reason = Reason.UPDATE

    settings.posting.loggers = True
    event_queue = asyncio.Queue()
    await process_resource_event(
        lifecycle=kopf.lifecycles.all_at_once,
        registry=registry,
        settings=settings,
        resource=resource,
        indexers=OperatorIndexers(),
        memories=ResourceMemories(),
        memobase=Memo(),
        raw_event={'type': event_type, 'object': {}},
        event_queue=event_queue,
    )

    assert not handlers.create_mock.called
    assert handlers.update_mock.call_count == 1
    assert not handlers.delete_mock.called

    assert k8s_mocked.patch.call_count == 1
    assert not event_queue.empty()

    patch = k8s_mocked.patch.call_args_list[0].kwargs['payload']
    assert 'metadata' in patch
    assert 'annotations' in patch['metadata']
    assert LAST_SEEN_ANNOTATION in patch['metadata']['annotations']

    assert_logs([
        "Updating is in progress:",
        "Handler 'update_fn' is invoked",
        "Handler 'update_fn' succeeded",
        "Updating is processed:",
        "Merge-patching",
    ])


@pytest.mark.parametrize('event_type', EVENT_TYPES_WHEN_EXISTS)
async def test_delete(registry, settings, handlers, resource, cause_mock, event_type,
                      assert_logs, k8s_mocked):
    cause_mock.reason = Reason.DELETE
    finalizer = settings.persistence.finalizer
    event_body = {'metadata': {'deletionTimestamp': '...', 'finalizers': [finalizer]}}

    settings.posting.loggers = True
    event_queue = asyncio.Queue()
    await process_resource_event(
        lifecycle=kopf.lifecycles.all_at_once,
        registry=registry,
        settings=settings,
        resource=resource,
        indexers=OperatorIndexers(),
        memories=ResourceMemories(),
        memobase=Memo(),
        raw_event={'type': event_type, 'object': event_body},
        event_queue=event_queue,
    )

    assert not handlers.create_mock.called
    assert not handlers.update_mock.called
    assert handlers.delete_mock.call_count == 1

    assert k8s_mocked.patch.call_count == 2  # (1) annotations, (2) finalizers.
    assert not event_queue.empty()

    assert_logs([
        "Deletion is in progress:",
        "Handler 'delete_fn' is invoked",
        "Handler 'delete_fn' succeeded",
        "Deletion is processed:",
        "Removing the finalizer",
        "Merge-patching",
    ])


#
# Informational causes: just log, and do nothing else.
#

@pytest.mark.parametrize('consistency_time', [0, 100], ids=['now', 'future'])
@pytest.mark.parametrize('event_type', EVENT_TYPES)
async def test_gone(registry, settings, handlers, resource, cause_mock, event_type,
                    assert_logs, k8s_mocked, consistency_time, looptime):
    cause_mock.reason = Reason.GONE

    settings.posting.loggers = True
    event_queue = asyncio.Queue()
    await process_resource_event(
        lifecycle=kopf.lifecycles.all_at_once,
        registry=registry,
        settings=settings,
        resource=resource,
        indexers=OperatorIndexers(),
        memories=ResourceMemories(),
        memobase=Memo(),
        raw_event={'type': event_type, 'object': {}},
        event_queue=event_queue,
        consistency_time=consistency_time,
    )

    assert not handlers.create_mock.called
    assert not handlers.update_mock.called
    assert not handlers.delete_mock.called

    assert not k8s_mocked.patch.called
    assert event_queue.empty()

    assert looptime == 0
    assert_logs([
        "Deleted, really deleted",
    ])


# Happens on the removal of the very last finalizer on DELETED events (it arrives as not removed).
async def test_gone_with_finalizer_not_removed_on_deleted_event(
        registry, settings, handlers, resource, cause_mock,
        assert_logs, k8s_mocked):
    cause_mock.reason = Reason.GONE
    finalizer = settings.persistence.finalizer
    event_body = {'metadata': {'deletionTimestamp': '...', 'finalizers': [finalizer]}}

    event_queue = asyncio.Queue()
    await process_resource_event(
        lifecycle=kopf.lifecycles.all_at_once,
        registry=registry,
        settings=settings,
        resource=resource,
        indexers=OperatorIndexers(),
        memories=ResourceMemories(),
        memobase=Memo(),
        raw_event={'type': 'DELETED', 'object': event_body},
        event_queue=event_queue,
    )

    assert not handlers.create_mock.called
    assert not handlers.update_mock.called
    assert not handlers.delete_mock.called

    assert not k8s_mocked.patch.called
    assert event_queue.empty()

    assert_logs([
        "Deleted, really deleted",
    ], prohibited=[
        "Removing the finalizer",
    ])


@pytest.mark.parametrize('event_type', EVENT_TYPES)
async def test_free(registry, settings, handlers, resource, cause_mock, event_type,
                    assert_logs, k8s_mocked):
    cause_mock.reason = Reason.FREE

    settings.posting.loggers = True
    event_queue = asyncio.Queue()
    await process_resource_event(
        lifecycle=kopf.lifecycles.all_at_once,
        registry=registry,
        settings=settings,
        resource=resource,
        indexers=OperatorIndexers(),
        memories=ResourceMemories(),
        memobase=Memo(),
        raw_event={'type': event_type, 'object': {}},
        event_queue=event_queue,
    )

    assert not handlers.create_mock.called
    assert not handlers.update_mock.called
    assert not handlers.delete_mock.called
    assert not k8s_mocked.patch.called
    assert event_queue.empty()

    assert_logs([
        "Deletion, but we are done with it",
    ])


@pytest.mark.parametrize('event_type', EVENT_TYPES)
async def test_noop(registry, settings, handlers, resource, cause_mock, event_type,
                    assert_logs, k8s_mocked):
    cause_mock.reason = Reason.NOOP

    settings.posting.loggers = True
    event_queue = asyncio.Queue()
    await process_resource_event(
        lifecycle=kopf.lifecycles.all_at_once,
        registry=registry,
        settings=settings,
        resource=resource,
        indexers=OperatorIndexers(),
        memories=ResourceMemories(),
        memobase=Memo(),
        raw_event={'type': event_type, 'object': {}},
        event_queue=event_queue,
    )

    assert not handlers.create_mock.called
    assert not handlers.update_mock.called
    assert not handlers.delete_mock.called
    assert not k8s_mocked.patch.called
    assert event_queue.empty()

    assert_logs([
        "Something has changed, but we are not interested",
    ])
