import logging
import re
from io import BytesIO
from urllib.error import HTTPError

import mock
import pytest

from uaclient.clouds.aws import (
    AWS_TOKEN_PUT_HEADER,
    AWS_TOKEN_REQ_HEADER,
    AWS_TOKEN_TTL_SECONDS,
    IMDS_IPV4_ADDRESS,
    IMDS_IPV6_ADDRESS,
    IMDS_URL,
    IMDS_V2_TOKEN_URL,
    UAAutoAttachAWSInstance,
)
from uaclient.exceptions import UserFacingError

M_PATH = "uaclient.clouds.aws."


class TestUAAutoAttachAWSInstance:
    def test_cloud_type(self):
        instance = UAAutoAttachAWSInstance()
        assert "aws" == instance.cloud_type

    @mock.patch(M_PATH + "util.readurl")
    def test__get_imds_v2_token_headers_none_on_404(self, readurl):
        """A 404 on private AWS regions indicates lack IMDSv2 support."""
        readurl.side_effect = HTTPError(
            "http://me", 404, "No IMDSv2 support", None, BytesIO()
        )
        instance = UAAutoAttachAWSInstance()
        assert None is instance._get_imds_v2_token_headers(
            ip_address=IMDS_IPV4_ADDRESS
        )
        assert "IMDSv1" == instance._api_token
        # No retries on 404. It is a permanent indication of no IMDSv2 support.
        instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
        assert 1 == readurl.call_count

    @mock.patch(M_PATH + "util.readurl")
    def test__get_imds_v2_token_headers_caches_response(self, readurl):
        """Return API token headers for IMDSv2 access. Response is cached."""
        instance = UAAutoAttachAWSInstance()
        url = "http://169.254.169.254/latest/api/token"
        readurl.return_value = "somebase64token==", {"header": "stuff"}
        assert {
            AWS_TOKEN_PUT_HEADER: "somebase64token=="
        } == instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
        instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
        assert "somebase64token==" == instance._api_token
        assert [
            mock.call(
                url,
                method="PUT",
                headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
                timeout=1,
            )
        ] == readurl.call_args_list

    @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
    @pytest.mark.parametrize("fail_count,exception", ((3, False), (4, True)))
    @mock.patch(M_PATH + "util.time.sleep")
    @mock.patch(M_PATH + "util.readurl")
    def test_retry_backoff_on__get_imds_v2_token_headers_caches_response(
        self, readurl, sleep, fail_count, exception, caplog_text
    ):
        """Retry backoff before failing _get_imds_v2_token_headers."""

        def fake_someurlerrors(url, method=None, headers=None, timeout=1):
            if readurl.call_count <= fail_count:
                raise HTTPError(
                    "http://me",
                    700 + readurl.call_count,
                    "funky error msg",
                    None,
                    BytesIO(),
                )
            return "base64token==", {"header": "stuff"}

        readurl.side_effect = fake_someurlerrors
        instance = UAAutoAttachAWSInstance()
        if exception:
            with pytest.raises(HTTPError) as excinfo:
                instance._get_imds_v2_token_headers(
                    ip_address=IMDS_IPV4_ADDRESS
                )
            assert 704 == excinfo.value.code
        else:
            assert {
                AWS_TOKEN_PUT_HEADER: "base64token=="
            } == instance._get_imds_v2_token_headers(
                ip_address=IMDS_IPV4_ADDRESS
            )

        expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)]
        assert expected_sleep_calls == sleep.call_args_list
        expected_logs = [
            "HTTP Error 701: funky error msg Retrying 3 more times.",
            "HTTP Error 702: funky error msg Retrying 2 more times.",
            "HTTP Error 703: funky error msg Retrying 1 more times.",
        ]
        logs = caplog_text()
        for log in expected_logs:
            assert log in logs

    @mock.patch(M_PATH + "util.readurl")
    def test_identity_doc_from_aws_url_pkcs7(self, readurl):
        """Return pkcs7 content from IMDS as AWS' identity doc"""
        readurl.return_value = "pkcs7WOOT!==", {"header": "stuff"}
        instance = UAAutoAttachAWSInstance()
        assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc
        url = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7"
        token_url = "http://169.254.169.254/latest/api/token"
        assert [
            mock.call(
                token_url,
                method="PUT",
                headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
                timeout=1,
            ),
            mock.call(
                url, headers={AWS_TOKEN_PUT_HEADER: "pkcs7WOOT!=="}, timeout=1
            ),
        ] == readurl.call_args_list

    @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
    @pytest.mark.parametrize("fail_count,exception", ((3, False),))
    @mock.patch(M_PATH + "util.time.sleep")
    @mock.patch(M_PATH + "util.readurl")
    def test_retry_backoff_on_failed_identity_doc(
        self, readurl, sleep, fail_count, exception, caplog_text
    ):
        """Retry backoff is attempted before failing to get AWS.identity_doc"""

        def fake_someurlerrors(url, method=None, headers=None, timeout=1):
            # due to _get_imds_v2_token_headers
            if "latest/api/token" in url:
                return "base64token==", {"header": "stuff"}
            if readurl.call_count <= fail_count + 1:
                raise HTTPError(
                    "http://me",
                    700 + readurl.call_count,
                    "funky error msg",
                    None,
                    BytesIO(),
                )
            return "pkcs7WOOT!==", {"header": "stuff"}

        readurl.side_effect = fake_someurlerrors
        instance = UAAutoAttachAWSInstance()
        if exception:
            with pytest.raises(HTTPError) as excinfo:
                instance.identity_doc
            assert 705 == excinfo.value.code
        else:
            assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc

        expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)]
        assert expected_sleep_calls == sleep.call_args_list
        expected_logs = [
            "HTTP Error 702: funky error msg Retrying 3 more times.",
            "HTTP Error 703: funky error msg Retrying 2 more times.",
            "HTTP Error 704: funky error msg Retrying 1 more times.",
        ]
        logs = caplog_text()
        for log in expected_logs:
            assert log in logs

    @pytest.mark.parametrize("uuid", ("ec2", "ec2yep"))
    @mock.patch(M_PATH + "util.load_file")
    def test_is_viable_based_on_sys_hypervisor_uuid(self, load_file, uuid):
        """Viable ec2 platform is determined by /sys/hypervisor/uuid prefix"""
        load_file.return_value = uuid
        instance = UAAutoAttachAWSInstance()
        assert True is instance.is_viable

    @pytest.mark.parametrize(
        "hypervisor_uuid,prod_uuid,prod_serial,viable",
        (
            ("notec2", "ec2UUID", "ec2Serial", True),
            (None, "ec2UUID", "ec2Serial", True),
            ("notec2", "EC2UUID", "Ec2Serial", True),
            (None, "EC2UUID", "Ec2Serial", True),
            ("notec2", "!EC2UUID", "Ec2Serial", False),
            ("notec2", "EC2UUID", "1Ec2Serial", False),
            ("notec2", "ec2UUID", "ec3Serial", False),
            ("notec2", "ec3UUID", "ec2Serial", False),
        ),
    )
    @mock.patch(M_PATH + "util.load_file")
    def test_is_viable_based_on_sys_product_serial_and_uuid(
        self, load_file, hypervisor_uuid, prod_uuid, prod_serial, viable
    ):
        """Platform is viable when product serial and uuid start with ec2"""

        def fake_load_file(f_name):
            if f_name == "/sys/hypervisor/uuid":
                if hypervisor_uuid is not None:
                    return hypervisor_uuid
                raise FileNotFoundError()
            if f_name == "/sys/class/dmi/id/product_uuid":
                return prod_uuid
            if f_name == "/sys/class/dmi/id/product_serial":
                return prod_serial
            raise AssertionError("Invalid load_file of {}".format(f_name))

        load_file.side_effect = fake_load_file
        instance = UAAutoAttachAWSInstance()
        assert viable is instance.is_viable

    @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
    @mock.patch(M_PATH + "util.readurl")
    def test_identity_doc_default_to_ipv6_if_ipv4_fail(
        self, readurl, caplog_text
    ):
        instance = UAAutoAttachAWSInstance()
        ipv4_address = IMDS_IPV4_ADDRESS
        ipv6_address = IMDS_IPV6_ADDRESS

        def fake_someurlerrors(url, method=None, headers=None, timeout=1):
            if ipv4_address in url:
                raise Exception("IPv4 exception")

            if url == IMDS_V2_TOKEN_URL.format(ipv6_address):
                return "base64token==", {"header": "stuff"}

            if url == IMDS_URL.format(ipv6_address):
                return "pkcs7WOOT!==", {"header": "stuff"}

        readurl.side_effect = fake_someurlerrors
        assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc
        expected = [
            mock.call(
                IMDS_V2_TOKEN_URL.format(ipv4_address),
                method="PUT",
                headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
                timeout=1,
            ),
            mock.call(
                IMDS_V2_TOKEN_URL.format(ipv6_address),
                method="PUT",
                headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
                timeout=1,
            ),
            mock.call(
                IMDS_URL.format(ipv6_address),
                headers={AWS_TOKEN_PUT_HEADER: "base64token=="},
                timeout=1,
            ),
        ]

        assert expected == readurl.call_args_list

        expected_log = "Could not reach AWS IMDS at http://169.254.169.254:"
        assert expected_log in caplog_text()

    @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
    @mock.patch(M_PATH + "util.readurl")
    def test_identity_doc_logs_error_if_both_ipv4_and_ipv6_fails(
        self, readurl, caplog_text
    ):

        instance = UAAutoAttachAWSInstance()
        ipv4_address = IMDS_IPV4_ADDRESS
        ipv6_address = IMDS_IPV6_ADDRESS

        readurl.side_effect = Exception("Exception")

        expected_error = (
            "No valid AWS IMDS endpoint discovered at "
            "addresses: {}, {}".format(IMDS_IPV4_ADDRESS, IMDS_IPV6_ADDRESS)
        )
        with pytest.raises(UserFacingError, match=re.escape(expected_error)):
            instance.identity_doc

        expected = [
            mock.call(
                IMDS_V2_TOKEN_URL.format(ipv4_address),
                method="PUT",
                headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
                timeout=1,
            ),
            mock.call(
                IMDS_V2_TOKEN_URL.format(ipv6_address),
                method="PUT",
                headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
                timeout=1,
            ),
        ]
        assert expected == readurl.call_args_list

        expected_logs = [
            "Could not reach AWS IMDS at http://169.254.169.254:",
            "Could not reach AWS IMDS at http://[fd00:ec2::254]:",
        ]

        for expected_log in expected_logs:
            assert expected_log in caplog_text()
