# Copyright 2017-2018 Amazon.com, Inc. and its affiliates. All Rights Reserved.
#
# Licensed under the MIT License. See the LICENSE accompanying this file
# for the specific language governing permissions and limitations under
# the License.
import logging
import random
import socket
import sys
import tempfile
import unittest
from unittest.mock import MagicMock

import pytest

import efs_utils_common
import efs_utils_common.constants as constants
import efs_utils_common.context as context
import efs_utils_common.network_utils as network_utils
import efs_utils_common.proxy as proxy

from .. import utils

try:
    import ConfigParser
except ImportError:
    from configparser import ConfigParser

DEFAULT_TLS_PORT_RANGE_LOW = 20049
DEFAULT_TLS_PORT_RANGE_HIGH = 21049
DEFAULT_TLS_PORT = random.randrange(
    DEFAULT_TLS_PORT_RANGE_LOW, DEFAULT_TLS_PORT_RANGE_HIGH
)


@pytest.fixture(autouse=True)
def setup_method(mocker):
    mount_context = context.MountContext()
    mount_context.reset()
    mount_context.mount_type = constants.MOUNT_TYPE_EFS
    mount_context.config_file_path = constants.CONFIG_FILE
    yield mount_context


def _get_config():
    try:
        config = ConfigParser.SafeConfigParser()
    except AttributeError:
        config = ConfigParser()
    config.add_section(efs_utils_common.constants.CONFIG_SECTION)
    config.set(
        efs_utils_common.constants.CONFIG_SECTION,
        "port_range_lower_bound",
        str(DEFAULT_TLS_PORT_RANGE_LOW),
    )
    config.set(
        efs_utils_common.constants.CONFIG_SECTION,
        "port_range_upper_bound",
        str(DEFAULT_TLS_PORT_RANGE_HIGH),
    )
    return config


def test_choose_tls_port_first_try(mocker, tmpdir):
    sock_mock = MagicMock()
    sock_mock.getsockname.return_value = ("local_host", DEFAULT_TLS_PORT)
    mocker.patch("socket.socket", return_value=sock_mock)
    options = {}

    tls_port_sock = proxy.choose_tls_port_and_get_bind_sock(
        _get_config(), options, str(tmpdir)
    )
    tls_port = proxy.get_tls_port_from_sock(tls_port_sock)
    assert DEFAULT_TLS_PORT_RANGE_LOW <= tls_port <= DEFAULT_TLS_PORT_RANGE_HIGH


def test_choose_tls_port_second_try(mocker, tmpdir):
    bad_sock = MagicMock()
    bad_sock.bind.side_effect = [socket.error, None]
    bad_sock.getsockname.return_value = ("local_host", DEFAULT_TLS_PORT)
    options = {}

    mocker.patch("socket.socket", return_value=bad_sock)

    tls_port_sock = proxy.choose_tls_port_and_get_bind_sock(
        _get_config(), options, str(tmpdir)
    )
    tls_port = proxy.get_tls_port_from_sock(tls_port_sock)

    assert DEFAULT_TLS_PORT_RANGE_LOW <= tls_port <= DEFAULT_TLS_PORT_RANGE_HIGH
    assert 2 == bad_sock.bind.call_count
    assert 1 == bad_sock.getsockname.call_count


@unittest.skipIf(sys.version_info < (3, 6), reason="requires python3.6")
def test_choose_tls_port_collision(mocker, tmpdir, caplog):
    """Ensure we don't choose a port that is pending mount"""
    sock = MagicMock()
    mocker.patch("socket.socket", return_value=sock)
    mocker.patch(
        "random.shuffle",
        return_value=range(DEFAULT_TLS_PORT_RANGE_LOW, DEFAULT_TLS_PORT_RANGE_HIGH),
    )

    port_suffix = ".%s" % str(DEFAULT_TLS_PORT_RANGE_LOW)
    temp_state_file = tempfile.NamedTemporaryFile(
        suffix=port_suffix, prefix="~", dir=tmpdir
    )

    options = {}
    with caplog.at_level(logging.DEBUG):
        proxy.choose_tls_port_and_get_bind_sock(_get_config(), options, tmpdir)

    temp_state_file.close()
    sock.bind.assert_called_once_with(("localhost", DEFAULT_TLS_PORT_RANGE_LOW + 1))
    assert "Skip binding TLS port" in caplog.text


def test_choose_tls_port_never_succeeds(mocker, tmpdir, capsys):
    bad_sock = MagicMock()
    bad_sock.bind.side_effect = socket.error()
    options = {}

    mocker.patch("socket.socket", return_value=bad_sock)

    with pytest.raises(SystemExit) as ex:
        proxy.choose_tls_port_and_get_bind_sock(_get_config(), options, str(tmpdir))

    assert 0 != ex.value.code

    out, err = capsys.readouterr()
    assert "Failed to locate an available port" in err

    assert (
        DEFAULT_TLS_PORT_RANGE_HIGH - DEFAULT_TLS_PORT_RANGE_LOW
        == bad_sock.bind.call_count
    )


def test_choose_tls_port_option_specified(mocker, tmpdir):
    sock_mock = MagicMock()
    sock_mock.getsockname.return_value = ("local_host", DEFAULT_TLS_PORT)
    mocker.patch("socket.socket", return_value=sock_mock)
    options = {"tlsport": DEFAULT_TLS_PORT}

    tls_port_sock = proxy.choose_tls_port_and_get_bind_sock(
        _get_config(), options, str(tmpdir)
    )
    tls_port = proxy.get_tls_port_from_sock(tls_port_sock)

    assert DEFAULT_TLS_PORT == tls_port


def test_choose_tls_port_option_specified_unavailable(mocker, tmpdir, capsys):
    bad_sock = MagicMock()
    bad_sock.bind.side_effect = socket.error()
    options = {"tlsport": 1000}

    mocker.patch("socket.socket", return_value=bad_sock)

    with pytest.raises(SystemExit) as ex:
        proxy.choose_tls_port_and_get_bind_sock(_get_config(), options, str(tmpdir))

    assert 0 != ex.value.code

    out, err = capsys.readouterr()
    assert "Specified port [1000] is unavailable" in err

    assert 1 == bad_sock.bind.call_count


def test_choose_tls_port_under_netns(mocker, tmpdir):
    mocker.patch("builtins.open")
    setns_mock = mocker.patch(
        "efs_utils_common.process_utils.setns", return_value=(None, None)
    )
    mocker.patch("socket.socket", return_value=MagicMock())
    options = {"netns": "/proc/1000/ns/net"}

    proxy.choose_tls_port_and_get_bind_sock(_get_config(), options, str(tmpdir))
    utils.assert_called(setns_mock)


def test_verify_tls_port(mocker):
    sock = MagicMock()
    sock.connect.side_effect = [ConnectionRefusedError, None]
    mocker.patch("socket.socket", return_value=sock)
    result = network_utils.verify_tlsport_can_be_connected(1000)
    assert result is False
    result = network_utils.verify_tlsport_can_be_connected(1000)
    assert result is True
    assert 2 == sock.connect.call_count
