# -*- coding: utf-8 -*-
# Upside Travel, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime
import os
import re
import textwrap
import unittest

import boto3
import botocore.session
from botocore.stub import Stubber
import mock

from clamav import RE_SEARCH_DIR
from clamav import scan_output_to_json
from clamav import md5_from_s3_tags
from clamav import time_from_s3
from clamav import update_defs_from_s3
from common import AV_DEFINITION_FILE_PREFIXES
from common import AV_DEFINITION_FILE_SUFFIXES
from common import AV_DEFINITION_S3_PREFIX
from common import AV_SIGNATURE_OK


class TestClamAV(unittest.TestCase):
    def setUp(self):
        # Common data
        self.s3_bucket_name = "test_bucket"
        self.s3_key_name = "test_key"

        # Clients and Resources
        self.s3 = boto3.resource("s3")
        self.s3_client = botocore.session.get_session().create_client("s3")
        self.sns_client = botocore.session.get_session().create_client(
            "sns", region_name="us-west-2"
        )

    def test_current_library_search_path(self):
        # Calling `ld --verbose` returns a lot of text but the line to check is this one:
        search_path = """SEARCH_DIR("=/usr/x86_64-redhat-linux/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/x86_64-redhat-linux/lib"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib");"""  # noqa
        rd_ld = re.compile(RE_SEARCH_DIR)
        all_search_paths = rd_ld.findall(search_path)
        expected_search_paths = [
            "/usr/x86_64-redhat-linux/lib64",
            "/usr/lib64",
            "/usr/local/lib64",
            "/lib64",
            "/usr/x86_64-redhat-linux/lib",
            "/usr/local/lib",
            "/lib",
            "/usr/lib",
        ]
        self.assertEqual(all_search_paths, expected_search_paths)

    def test_scan_output_to_json_clean(self):
        file_path = "/tmp/test.txt"
        signature = AV_SIGNATURE_OK
        output = textwrap.dedent(
            """\
        Scanning {0}
        {0}: {1}
        ----------- SCAN SUMMARY -----------
        Known viruses: 6305127
        Engine version: 0.101.4
        Scanned directories: 0
        Scanned files: 1
        Infected files: 0
        Data scanned: 0.00 MB
        Data read: 0.00 MB (ratio 0.00:1)
        Time: 80.299 sec (1 m 20 s)
        """.format(
                file_path, signature
            )
        )
        summary = scan_output_to_json(output)
        self.assertEqual(summary[file_path], signature)
        self.assertEqual(summary["Infected files"], "0")

    def test_scan_output_to_json_infected(self):
        file_path = "/tmp/eicar.com.txt"
        signature = "Eicar-Test-Signature FOUND"
        output = textwrap.dedent(
            """\
        Scanning {0}
        {0}: {1}
        {0}!(0): {1}
        ----------- SCAN SUMMARY -----------
        Known viruses: 6305127
        Engine version: 0.101.4
        Scanned directories: 0
        Scanned files: 1
        Infected files: 1
        Data scanned: 0.00 MB
        Data read: 0.00 MB (ratio 0.00:1)
        Time: 80.299 sec (1 m 20 s)
        """.format(
                file_path, signature
            )
        )
        summary = scan_output_to_json(output)
        self.assertEqual(summary[file_path], signature)
        self.assertEqual(summary["Infected files"], "1")

    def test_md5_from_s3_tags_no_md5(self):
        tag_set = {"TagSet": []}

        s3_stubber = Stubber(self.s3_client)
        get_object_tagging_response = tag_set
        get_object_tagging_expected_params = {
            "Bucket": self.s3_bucket_name,
            "Key": self.s3_key_name,
        }
        s3_stubber.add_response(
            "get_object_tagging",
            get_object_tagging_response,
            get_object_tagging_expected_params,
        )
        with s3_stubber:
            md5_hash = md5_from_s3_tags(
                self.s3_client, self.s3_bucket_name, self.s3_key_name
            )
            self.assertEquals("", md5_hash)

    def test_md5_from_s3_tags_has_md5(self):
        expected_md5_hash = "d41d8cd98f00b204e9800998ecf8427e"
        tag_set = {"TagSet": [{"Key": "md5", "Value": expected_md5_hash}]}

        s3_stubber = Stubber(self.s3_client)
        get_object_tagging_response = tag_set
        get_object_tagging_expected_params = {
            "Bucket": self.s3_bucket_name,
            "Key": self.s3_key_name,
        }
        s3_stubber.add_response(
            "get_object_tagging",
            get_object_tagging_response,
            get_object_tagging_expected_params,
        )
        with s3_stubber:
            md5_hash = md5_from_s3_tags(
                self.s3_client, self.s3_bucket_name, self.s3_key_name
            )
            self.assertEquals(expected_md5_hash, md5_hash)

    def test_time_from_s3(self):

        expected_s3_time = datetime.datetime(2019, 1, 1)

        s3_stubber = Stubber(self.s3_client)
        head_object_response = {"LastModified": expected_s3_time}
        head_object_expected_params = {
            "Bucket": self.s3_bucket_name,
            "Key": self.s3_key_name,
        }
        s3_stubber.add_response(
            "head_object", head_object_response, head_object_expected_params
        )
        with s3_stubber:
            s3_time = time_from_s3(
                self.s3_client, self.s3_bucket_name, self.s3_key_name
            )
            self.assertEquals(expected_s3_time, s3_time)

    @mock.patch("clamav.md5_from_file")
    @mock.patch("common.os.path.exists")
    def test_update_defs_from_s3(self, mock_exists, mock_md5_from_file):
        expected_md5_hash = "d41d8cd98f00b204e9800998ecf8427e"
        different_md5_hash = "d41d8cd98f00b204e9800998ecf8427f"

        mock_md5_from_file.return_value = different_md5_hash

        tag_set = {"TagSet": [{"Key": "md5", "Value": expected_md5_hash}]}
        expected_s3_time = datetime.datetime(2019, 1, 1)

        s3_stubber = Stubber(self.s3_client)

        key_names = []
        side_effect = []
        for file_prefix in AV_DEFINITION_FILE_PREFIXES:
            for file_suffix in AV_DEFINITION_FILE_SUFFIXES:
                side_effect.extend([True, True])
                filename = file_prefix + "." + file_suffix
                key_names.append(os.path.join(AV_DEFINITION_S3_PREFIX, filename))
        mock_exists.side_effect = side_effect

        for s3_key_name in key_names:
            get_object_tagging_response = tag_set
            get_object_tagging_expected_params = {
                "Bucket": self.s3_bucket_name,
                "Key": s3_key_name,
            }
            s3_stubber.add_response(
                "get_object_tagging",
                get_object_tagging_response,
                get_object_tagging_expected_params,
            )
            head_object_response = {"LastModified": expected_s3_time}
            head_object_expected_params = {
                "Bucket": self.s3_bucket_name,
                "Key": s3_key_name,
            }
            s3_stubber.add_response(
                "head_object", head_object_response, head_object_expected_params
            )

        expected_to_download = {
            "bytecode": {
                "local_path": "/tmp/clamav_defs/bytecode.cvd",
                "s3_path": "clamav_defs/bytecode.cvd",
            },
            "daily": {
                "local_path": "/tmp/clamav_defs/daily.cvd",
                "s3_path": "clamav_defs/daily.cvd",
            },
            "main": {
                "local_path": "/tmp/clamav_defs/main.cvd",
                "s3_path": "clamav_defs/main.cvd",
            },
        }
        with s3_stubber:
            to_download = update_defs_from_s3(
                self.s3_client, self.s3_bucket_name, AV_DEFINITION_S3_PREFIX
            )
            self.assertEquals(expected_to_download, to_download)

    @mock.patch("clamav.md5_from_file")
    @mock.patch("common.os.path.exists")
    def test_update_defs_from_s3_same_hash(self, mock_exists, mock_md5_from_file):
        expected_md5_hash = "d41d8cd98f00b204e9800998ecf8427e"
        different_md5_hash = expected_md5_hash

        mock_md5_from_file.return_value = different_md5_hash

        tag_set = {"TagSet": [{"Key": "md5", "Value": expected_md5_hash}]}
        expected_s3_time = datetime.datetime(2019, 1, 1)

        s3_stubber = Stubber(self.s3_client)

        key_names = []
        side_effect = []
        for file_prefix in AV_DEFINITION_FILE_PREFIXES:
            for file_suffix in AV_DEFINITION_FILE_SUFFIXES:
                side_effect.extend([True, True])
                filename = file_prefix + "." + file_suffix
                key_names.append(os.path.join(AV_DEFINITION_S3_PREFIX, filename))
        mock_exists.side_effect = side_effect

        for s3_key_name in key_names:
            get_object_tagging_response = tag_set
            get_object_tagging_expected_params = {
                "Bucket": self.s3_bucket_name,
                "Key": s3_key_name,
            }
            s3_stubber.add_response(
                "get_object_tagging",
                get_object_tagging_response,
                get_object_tagging_expected_params,
            )
            head_object_response = {"LastModified": expected_s3_time}
            head_object_expected_params = {
                "Bucket": self.s3_bucket_name,
                "Key": s3_key_name,
            }
            s3_stubber.add_response(
                "head_object", head_object_response, head_object_expected_params
            )

        expected_to_download = {}
        with s3_stubber:
            to_download = update_defs_from_s3(
                self.s3_client, self.s3_bucket_name, AV_DEFINITION_S3_PREFIX
            )
            self.assertEquals(expected_to_download, to_download)

    @mock.patch("clamav.md5_from_file")
    @mock.patch("common.os.path.exists")
    def test_update_defs_from_s3_old_files(self, mock_exists, mock_md5_from_file):
        expected_md5_hash = "d41d8cd98f00b204e9800998ecf8427e"
        different_md5_hash = "d41d8cd98f00b204e9800998ecf8427f"

        mock_md5_from_file.return_value = different_md5_hash

        tag_set = {"TagSet": [{"Key": "md5", "Value": expected_md5_hash}]}
        expected_s3_time = datetime.datetime(2019, 1, 1)

        s3_stubber = Stubber(self.s3_client)

        key_names = []
        side_effect = []
        for file_prefix in AV_DEFINITION_FILE_PREFIXES:
            for file_suffix in AV_DEFINITION_FILE_SUFFIXES:
                side_effect.extend([True, True])
                filename = file_prefix + "." + file_suffix
                key_names.append(os.path.join(AV_DEFINITION_S3_PREFIX, filename))
        mock_exists.side_effect = side_effect

        count = 0
        for s3_key_name in key_names:
            get_object_tagging_response = tag_set
            get_object_tagging_expected_params = {
                "Bucket": self.s3_bucket_name,
                "Key": s3_key_name,
            }
            s3_stubber.add_response(
                "get_object_tagging",
                get_object_tagging_response,
                get_object_tagging_expected_params,
            )
            head_object_response = {
                "LastModified": expected_s3_time - datetime.timedelta(hours=count)
            }
            head_object_expected_params = {
                "Bucket": self.s3_bucket_name,
                "Key": s3_key_name,
            }
            s3_stubber.add_response(
                "head_object", head_object_response, head_object_expected_params
            )
            count += 1

        expected_to_download = {
            "bytecode": {
                "local_path": "/tmp/clamav_defs/bytecode.cld",
                "s3_path": "clamav_defs/bytecode.cld",
            },
            "daily": {
                "local_path": "/tmp/clamav_defs/daily.cld",
                "s3_path": "clamav_defs/daily.cld",
            },
            "main": {
                "local_path": "/tmp/clamav_defs/main.cld",
                "s3_path": "clamav_defs/main.cld",
            },
        }
        with s3_stubber:
            to_download = update_defs_from_s3(
                self.s3_client, self.s3_bucket_name, AV_DEFINITION_S3_PREFIX
            )
            self.assertEquals(expected_to_download, to_download)