from unittest import mock

import pytest

from app.objects.c_adversary import Adversary
from app.objects.c_source import Source
from app.objects.secondclass.c_link import Link
from app.objects.secondclass.c_fact import Fact
from app.objects.secondclass.c_fact import OriginType
from app.objects.secondclass.c_relationship import Relationship
from app.service.interfaces.i_event_svc import EventServiceInterface
from app.utility.base_service import BaseService


@pytest.fixture
def fake_event_svc(event_loop):
    class FakeEventService(BaseService, EventServiceInterface):
        def __init__(self):
            self.fired = {}

        def reset(self):
            self.fired = {}

        async def observe_event(self, callback, exchange=None, queue=None):
            pass

        async def fire_event(self, exchange=None, queue=None, timestamp=True, **callback_kwargs):
            self.fired[exchange, queue] = callback_kwargs

    service = FakeEventService()
    service.add_service('event_svc', service)

    yield service

    service.remove_service('event_svc')


class TestLink:

    def test_link_eq(self, ability, executor):
        test_executor = executor(name='psh', platform='windows')
        test_ability = ability(ability_id='123', executors=[test_executor])
        fact = Fact(trait='remote.host.fqdn', value='dc')
        test_link = Link(command='sc.exe \\dc create sandsvc binpath= "s4ndc4t.exe -originLinkID 111111"',
                         paw='123456', ability=test_ability, id=111111, executor=test_executor)
        test_link.used = [fact]
        test_link2 = Link(command='sc.exe \\dc create sandsvc binpath= "s4ndc4t.exe -originLinkID 222222"',
                          paw='123456', ability=test_ability, id=222222, executor=test_executor)
        test_link2.used = [fact]
        assert test_link == test_link2

    def test_link_neq(self, ability, executor):
        test_executor = executor(name='psh', platform='windows')
        test_ability = ability(ability_id='123', executors=[test_executor])
        fact_a = Fact(trait='host.user.name', value='a')
        fact_b = Fact(trait='host.user.name', value='b')
        test_link_a = Link(command='net user a', paw='123456', ability=test_ability, id=111111, executor=test_executor)
        test_link_a.used = [fact_a]
        test_link_b = Link(command='net user b', paw='123456', ability=test_ability, id=222222, executor=test_executor)
        test_link_b.used = [fact_b]
        assert test_link_a != test_link_b

    @mock.patch.object(Link, '_emit_status_change_event')
    def test_no_status_change_event_on_instantiation(self, mock_emit_status_change_method, ability, executor):
        executor = executor('psh', 'windows')
        ability = ability(executor=executor)
        Link(command='net user a', paw='123456', ability=ability, executor=executor)
        mock_emit_status_change_method.assert_not_called()

    @mock.patch.object(Link, '_emit_status_change_event')
    def test_status_change_event_fired_on_status_change(self, mock_emit_status_change_method, ability, executor):
        executor = executor('psh', 'windows')
        ability = ability(executor=executor)
        link = Link(command='net user a', paw='123456', ability=ability, executor=executor, status=-3)
        link.status = -5
        mock_emit_status_change_method.assert_called_with(from_status=-3, to_status=-5)

    def test_emit_status_change_event(self, event_loop, fake_event_svc, ability, executor):
        executor = executor('psh', 'windows')
        ability = ability(executor=executor)
        link = Link(command='net user a', paw='123456', ability=ability, executor=executor, status=-3)
        fake_event_svc.reset()

        event_loop.run_until_complete(
            link._emit_status_change_event(
                from_status=-3,
                to_status=-5
            )
        )

        expected_key = (Link.EVENT_EXCHANGE, Link.EVENT_QUEUE_STATUS_CHANGED)
        assert expected_key in fake_event_svc.fired

        event_kwargs = fake_event_svc.fired[expected_key]
        assert event_kwargs['link'] == link.id
        assert event_kwargs['from_status'] == -3
        assert event_kwargs['to_status'] == -5

    def test_link_agent_reported_time_not_present_when_none_roundtrip(self, ability, executor):
        test_executor = executor(name='psh', platform='windows')
        test_ability = ability(ability_id='123')
        test_link = Link(command='sc.exe \\dc create sandsvc binpath= "s4ndc4t.exe -originLinkID 111111"',
                         paw='123456', ability=test_ability, executor=test_executor, id=111111)
        serialized_link = test_link.display
        loaded_link = Link.load(serialized_link)

        assert 'agent_reported_time' not in serialized_link
        assert loaded_link.agent_reported_time is None

    def test_link_agent_reported_time_present_when_set_roundtrip(self, ability, executor):
        agent_reported_time = '2021-02-23T11:50:16Z'
        test_executor = executor(name='psh', platform='windows')
        test_ability = ability(ability_id='123')
        test_link = Link(command='sc.exe \\dc create sandsvc binpath= "s4ndc4t.exe -originLinkID 111111"',
                         paw='123456', ability=test_ability, executor=test_executor, id=111111,
                         agent_reported_time=BaseService.get_timestamp_from_string(agent_reported_time))
        serialized_link = test_link.display
        loaded_link = Link.load(serialized_link)

        assert serialized_link['agent_reported_time'] == agent_reported_time
        assert loaded_link.agent_reported_time == BaseService.get_timestamp_from_string(agent_reported_time)

    def test_link_knowledge_svc_synchronization(self, event_loop, executor, ability, knowledge_svc, fire_event_mock):
        test_executor = executor(name='psh', platform='windows')
        test_ability = ability(ability_id='123', executors=[test_executor])
        fact = Fact(trait='remote.host.fqdn', value='dc')
        fact2 = Fact(trait='domain.user.name', value='Bob')
        relationship = Relationship(source=fact, edge='has_admin', target=fact2)
        test_link = Link(command='echo "this was a triumph"',
                         paw='123456', ability=test_ability, id=111111, executor=test_executor)
        event_loop.run_until_complete(test_link.create_relationships([relationship], None))
        checkable = [(x.trait, x.value) for x in test_link.facts]
        assert (fact.trait, fact.value) in checkable
        assert (fact2.trait, fact2.value) in checkable
        knowledge_base_f = event_loop.run_until_complete(knowledge_svc.get_facts(dict(source=test_link.id)))
        assert len(knowledge_base_f) == 2
        assert test_link.id in knowledge_base_f[0].links
        knowledge_base_r = event_loop.run_until_complete(knowledge_svc.get_relationships(dict(edge='has_admin')))
        assert len(knowledge_base_r) == 1

    def test_create_relationship_source_fact(self, event_loop, ability, executor, operation, knowledge_svc, fire_event_mock):
        test_executor = executor(name='psh', platform='windows')
        test_ability = ability(ability_id='123', executors=[test_executor])
        fact1 = Fact(trait='remote.host.fqdn', value='dc')
        fact2 = Fact(trait='domain.user.name', value='Bob')
        relationship = Relationship(source=fact1, edge='has_admin', target=fact2)
        link1 = Link(command='echo "Bob"', paw='123456', ability=test_ability, id='111111', executor=test_executor)
        operation = operation(name='test-op', agents=[],
                              adversary=Adversary(name='sample', adversary_id='XYZ', atomic_ordering=[],
                                                  description='test'),
                              source=Source(id='test-source', facts=[fact1]))
        event_loop.run_until_complete(operation._init_source())
        event_loop.run_until_complete(link1.create_relationships([relationship], operation))

        link2 = Link(command='echo "Bob"', paw='789100', ability=test_ability, id='222222', executor=test_executor)
        event_loop.run_until_complete(link2.create_relationships([relationship], operation))

        fact_store_operation_source = event_loop.run_until_complete(knowledge_svc.get_facts(dict(source=operation.source.id)))
        fact_store_operation = event_loop.run_until_complete(knowledge_svc.get_facts(dict(source=operation.id)))
        assert len(fact_store_operation_source) == 1
        assert len(fact_store_operation) == 1
        assert len(fact_store_operation_source[0].collected_by) == 2

    def test_save_discover_seeded_fact_not_in_command(self, event_loop, ability, executor, operation, knowledge_svc, fire_event_mock):
        test_executor = executor(name='psh', platform='windows')
        test_ability = ability(ability_id='123', executors=[test_executor])
        fact1 = Fact(trait='remote.host.fqdn', value='dc')
        fact2 = Fact(trait='domain.user.name', value='Bob')
        relationship = Relationship(source=fact1, edge='has_user', target=fact2)
        link = Link(command='net user', paw='123456', ability=test_ability, id='111111', executor=test_executor)
        operation = operation(name='test-op', agents=[],
                              adversary=Adversary(name='sample', adversary_id='XYZ', atomic_ordering=[],
                                                  description='test'),
                              source=Source(id='test-source', facts=[fact1, fact2]))
        event_loop.run_until_complete(operation._init_source())
        event_loop.run_until_complete(link.save_fact(operation, fact2, 1, relationship))

        assert fact2.origin_type == OriginType.SEEDED
        assert '123456' in fact2.collected_by
