import json
import os
import shutil
from unittest.mock import patch

import pytest
from django.urls import reverse

from correlation.models import Rule
from incident.models import IncidentRecommendations, IncidentEffect

TESTS_DIR = os.path.dirname(__file__)

SID_REV_TESTS = [2, 20]


def generate_multiple_rules_checks(check_list, check_type, instance_name, action_key):
    """ Function for generating test cases for linked models, which can be assigned more than one instance to one value
        :param check_list - Final test list fixture, which will be send to test
        :param check_type - 'asset' or 'incident', model instances names, which held linked models in theirs
            'actins_json` field
        :param instance_name - string, which is basically model name without spaces and in lowercase
        :param action_key - string, which is used as key `actions_json` field to held corresponding models
    """
    test_dicts = [
        [{"name": f"test_1_{action_key}", "description": ""},
         {"name": f"test_2_{action_key}", "description": ""},
         {"name": "some_test", "description": ""}, ],
        [{"name": f"test_1_{action_key}", "description": ""},
         {"name": "some_test", "description": ""},
         {"name": f"test_2_{action_key}", "description": ""}],
        [{"name": "some_test", "description": ""},
         {"name": f"test_1_{action_key}", "description": ""},
         {"name": f"test_2_{action_key}", "description": ""}],
        [{"name": "some_test", "description": ""}]
    ]

    check_list.append(
        [check_type, False, instance_name,
         [{"name": f"test_1_{action_key}", "description": ""},
          {"name": f"test_2_{action_key}", "description": ""},
          {"name": f"test_3_{action_key}", "description": ""}],
         True]
    )
    for elem in test_dicts:
        check_list.append(
            [check_type, False, instance_name, elem, True]
        )
    check_list.append(
        [check_type, False, instance_name, 'some_test', False])


def get_names_of_objects(array_of_objects):
    if isinstance(array_of_objects, list):
        array_of_names = [obj['name'] for obj in array_of_objects]
        return array_of_names
    else:
        return 'wrong_format'


""" Fixture for linked models import/export tests
Structure of fixture:
    [
        'type of key in actions_json field, that held rule action`,
        True if actions_json field is ChoiceField, False if MultipleChoiceField,
        key of linked model, that are used in actions_json field,
        value, which should be placed for test case,
        True if rule should import without mistakes, False otherwise
    ]
"""
NAME_DESCRIPTION_TESTS = [
    ['asset', True, 'os', 'some_test', True],
    ['asset', True, 'os', 'test_os', True],
    ['asset', True, 'group', 'some_test', True],
    ['asset', True, 'group', 'test_group', True],
    ['asset', True, 'manufacturer', 'some_test', True],
    ['asset', True, 'manufacturer', 'test_manufacturer', True],
    ['incident', True, 'category', 'some_test', True],
    ['incident', True, 'category', 'test_incident_category', True],
]

generate_multiple_rules_checks(NAME_DESCRIPTION_TESTS, 'incident', 'effects', 'incidenteffect')
generate_multiple_rules_checks(NAME_DESCRIPTION_TESTS, 'incident', 'close_recommendations', 'incidentrecommendations')


@pytest.fixture
def actual_importing_rule(tmpdir):
    """
    Change version of app in file
    Args: tmpdir():
    Returns: path to updated file
    """
    path = os.path.join(TESTS_DIR, 'data/api_test_importing_rule_new_version.json')
    tmp_dir = tmpdir.mkdir("sub")
    shutil.copy(path, tmp_dir)
    tmp_file = os.path.join(tmp_dir, 'api_test_importing_rule_new_version.json')
    return tmp_file


def mock_correlator_task(*args, **kwargs): pass


@pytest.mark.django_db
class TestImportRules:
    @pytest.fixture(autouse=True)
    def setup_tests(self, client, django_user_model, add_user_with_permissions):
        """ Fixture for creating basic setup for following import tests """
        self.admin = django_user_model.objects.first()

    @pytest.mark.unit
    @pytest.mark.parametrize('sid_rev_test', SID_REV_TESTS)
    @patch('correlation.tasks.update_correlator_tasks', mock_correlator_task)
    @patch('correlation.api.update_correlator_tasks', mock_correlator_task)
    def test_modify_rev_after_importing_rules(self, api_client, sid_rev_test, actual_importing_rule):
        """ Test for checking the correct SID and REV fields parsing of rules, which are being imported """

        api_client.force_authenticate(self.admin)
        test_file = actual_importing_rule

        # Step 1. Add rules to empty DB from test file
        with open(test_file, 'r') as json_file:
            api_client.post(reverse('rules-import-rules'), {'uploaded_file': json_file})

        # Step 2. Create new file, which will store modified test data
        modified_test_file = 'import_modify_test.json'
        with open(test_file, 'r') as json_file, open(modified_test_file, 'w') as json_file_modified:
            json_data = json.load(json_file)
            json_data['rules'][0]['rev'] = sid_rev_test
            json.dump(json_data, json_file_modified)
        old_rule = Rule.objects.get()  # exists only one rule
        assert old_rule.archived == False

        # Step 3. Try to import file, with modified data
        with open(modified_test_file, 'r') as json_file:
            rule = Rule.objects.all()
            api_client.post(reverse('rules-import-rules'), {'uploaded_file': json_file})
        os.remove(modified_test_file)

        assert Rule.objects.count() == 2

        archived_rule = Rule.objects.first()
        assert archived_rule.archived == True

        new_rule = Rule.objects.last()
        assert new_rule.sid == old_rule.sid == archived_rule.sid
        assert new_rule.rev == sid_rev_test

    @pytest.mark.unit
    @pytest.mark.parametrize('name_description_test', NAME_DESCRIPTION_TESTS)
    @patch('correlation.tasks.update_correlator_tasks', mock_correlator_task)
    @patch('correlation.api.update_correlator_tasks', mock_correlator_task)
    def test_name_description_importing_rules(self, api_client, name_description_test, actual_importing_rule):
        """ Test for checking the correct parse on all linked models, which are stored in `actions_json` field in
            importing rules.
        """
        api_client.force_authenticate(self.admin)
        test_file = actual_importing_rule

        # Step 1. Get the current length of queryset of Rules, that are currently stored in database
        rules_before = len(Rule.objects.all())

        # Step 2. Prepare file for storing modified data, which later will be used in test cases
        modified_test_file = 'import_modify_test.json'

        # Step 3. Prepare test data from fixture
        with open(test_file, 'r') as json_file, open(modified_test_file, 'w') as json_file_modified:
            json_data = json.load(json_file)
            for action in json_data['rules'][0]['actions_json']:
                if action['type'] == name_description_test[0]:
                    if name_description_test[1]:
                        action[name_description_test[2]] = [{'name': name_description_test[3]}]
                    else:
                        json_data[name_description_test[2]] = name_description_test[3]
                        array_of_names = get_names_of_objects(name_description_test[3])
                        action[name_description_test[2]] = array_of_names
            json.dump(json_data, json_file_modified)

        # Step 4. Trying to pass json file with modified data for test case
        with open(modified_test_file, 'r') as json_file:
            api_client.post(reverse('rules-import-rules'), {'uploaded_file': json_file})

        # Step 5. Remove unnecessary file
        os.remove(modified_test_file)

        # Step 6. Get amount of rules, that are currently stored in database for assert statement
        rules_after = len(Rule.objects.all())
        if name_description_test[4]:
            assert rules_after != rules_before
        else:
            assert rules_after == rules_before

    @pytest.mark.unit
    @patch('correlation.tasks.update_correlator_tasks', mock_correlator_task)
    @patch('correlation.api.update_correlator_tasks', mock_correlator_task)
    def test_sid_and_rev_when_importing(self, api_client, actual_importing_rule):
        """ Test for checking if when rule is being imported that sid and rev values stays the same as in file,
        where from import is performed
        """
        api_client.force_authenticate(self.admin)

        with open(actual_importing_rule, 'r') as json_file:
            api_client.post(reverse('rules-import-rules'), {'uploaded_file': json_file})

        # Check if rev and sid fields are same as in imported rule
        path = os.path.join(TESTS_DIR, 'data/api_test_importing_rule_new_version.json')
        with open(path, 'r') as file:
            json_data = json.load(file)
        imported_rule = Rule.objects.get(name=json_data['rules'][0]['name'])
        assert imported_rule.sid == json_data['rules'][0]['sid']
        assert imported_rule.rev == json_data['rules'][0]['rev']

    @pytest.mark.unit
    @patch('correlation.tasks.update_correlator_tasks', mock_correlator_task)
    @patch('correlation.api.update_correlator_tasks', mock_correlator_task)
    def test_not_archived_rule_after_importing_invalid_rule(self, api_client, actual_importing_rule):
        api_client.force_authenticate(self.admin)
        test_file = actual_importing_rule

        # Step 1. Add rules to empty DB from test file
        with open(test_file, 'r') as json_file:
            api_client.post(reverse('rules-import-rules'), {'uploaded_file': json_file})

        # Step 2. Create new file, which will store modified test data
        modified_test_file = 'import_modify_test.json'
        with open(test_file, 'r') as json_file, open(modified_test_file, 'w') as json_file_modified:
            json_data = json.load(json_file)
            json_data['rules'][0]['rev'] = 2
            json_data['rules'][0]['name'] = ''  # invalid value, must be string
            json.dump(json_data, json_file_modified)
        old_rule = Rule.objects.get()  # exists only one rule
        assert old_rule.archived == False

        # Step 3. Try to import file, with modified data
        with open(modified_test_file, 'r') as json_file:
            rule = Rule.objects.all()
            api_client.post(reverse('rules-import-rules'), {'uploaded_file': json_file})
        os.remove(modified_test_file)

        assert Rule.objects.count() == 1

        new_rule = Rule.objects.get()
        assert new_rule.archived == False
        assert new_rule.sid == old_rule.sid
        assert new_rule.rev == old_rule.rev


@pytest.mark.django_db
class TestAPIImportService:
    @pytest.fixture(autouse=True)
    def setup_tests(self, client, django_user_model, add_user_with_permissions):
        """ Fixture for creating basic setup for following import tests """
        self.admin = django_user_model.objects.first()

    @pytest.mark.unit
    @patch('correlation.tasks.update_correlator_tasks', mock_correlator_task)
    @patch('correlation.api.update_correlator_tasks', mock_correlator_task)
    def test_api_import_rules(self, api_client, actual_importing_rule):
        api_client.force_authenticate(self.admin)
        with open(actual_importing_rule, 'r') as json_file:
            response = api_client.post(reverse('rules-import-rules'), {'uploaded_file': json_file})
        assert response.status_code == 200
        assert response['content-disposition'] == 'attachment; filename="Report.json"'
        assert Rule.objects.count() == 1
        assert IncidentRecommendations.objects.count() == 6
        assert IncidentEffect.objects.count() == 4

    @pytest.mark.unit
    @patch('correlation.tasks.update_correlator_tasks', mock_correlator_task)
    @patch('correlation.api.update_correlator_tasks', mock_correlator_task)
    def test_api_import_invalid_file(self, api_client):
        api_client.force_authenticate(self.admin)
        path = os.path.join(TESTS_DIR, 'data/image.png')
        with open(path, 'rb') as file:
            response = api_client.post(reverse('rules-import-rules'), data={'uploaded_file': file})
        assert response.status_code == 400
        assert 'uploaded_file' in response.json()
