from collections import namedtuple
from unittest.mock import patch

import pytest

from cloudinit.net.activators import (
    DEFAULT_PRIORITY,
    search_activator,
    select_activator,
)
from cloudinit.net.activators import (
    IfUpDownActivator,
    NetplanActivator,
    NetworkManagerActivator,
    NetworkdActivator
)
from cloudinit.net.network_state import parse_net_config_data
from cloudinit.safeyaml import load


V1_CONFIG = """\
version: 1
config:
- type: physical
  name: eth0
- type: physical
  name: eth1
"""

V2_CONFIG = """\
version: 2
ethernets:
  eth0:
    dhcp4: true
  eth1:
    dhcp4: true
"""

NETPLAN_CALL_LIST = [
    ((['netplan', 'apply'], ), {}),
]


@pytest.yield_fixture
def available_mocks():
    mocks = namedtuple('Mocks', 'm_which, m_file')
    with patch('cloudinit.subp.which', return_value=True) as m_which:
        with patch('os.path.isfile', return_value=True) as m_file:
            yield mocks(m_which, m_file)


@pytest.yield_fixture
def unavailable_mocks():
    mocks = namedtuple('Mocks', 'm_which, m_file')
    with patch('cloudinit.subp.which', return_value=False) as m_which:
        with patch('os.path.isfile', return_value=False) as m_file:
            yield mocks(m_which, m_file)


class TestSearchAndSelect:
    def test_defaults(self, available_mocks):
        resp = search_activator()
        assert resp == DEFAULT_PRIORITY

        activator = select_activator()
        assert activator == DEFAULT_PRIORITY[0]

    def test_priority(self, available_mocks):
        new_order = [NetplanActivator, NetworkManagerActivator]
        resp = search_activator(priority=new_order)
        assert resp == new_order

        activator = select_activator(priority=new_order)
        assert activator == new_order[0]

    def test_target(self, available_mocks):
        search_activator(target='/tmp')
        assert '/tmp' == available_mocks.m_which.call_args[1]['target']

        select_activator(target='/tmp')
        assert '/tmp' == available_mocks.m_which.call_args[1]['target']

    @patch('cloudinit.net.activators.IfUpDownActivator.available',
           return_value=False)
    def test_first_not_available(self, m_available, available_mocks):
        resp = search_activator()
        assert resp == DEFAULT_PRIORITY[1:]

        resp = select_activator()
        assert resp == DEFAULT_PRIORITY[1]

    def test_priority_not_exist(self, available_mocks):
        with pytest.raises(ValueError):
            search_activator(priority=['spam', 'eggs'])
        with pytest.raises(ValueError):
            select_activator(priority=['spam', 'eggs'])

    def test_none_available(self, unavailable_mocks):
        resp = search_activator()
        assert resp == []

        with pytest.raises(RuntimeError):
            select_activator()


IF_UP_DOWN_AVAILABLE_CALLS = [
    (('ifquery',), {'search': ['/sbin', '/usr/sbin'], 'target': None}),
    (('ifup',), {'search': ['/sbin', '/usr/sbin'], 'target': None}),
    (('ifdown',), {'search': ['/sbin', '/usr/sbin'], 'target': None}),
]

NETPLAN_AVAILABLE_CALLS = [
    (('netplan',), {'search': ['/usr/sbin', '/sbin'], 'target': None}),
]

NETWORK_MANAGER_AVAILABLE_CALLS = [
    (('nmcli',), {'target': None}),
]

NETWORKD_AVAILABLE_CALLS = [
    (('ip',), {'search': ['/usr/sbin', '/bin'], 'target': None}),
    (('systemctl',), {'search': ['/usr/sbin', '/bin'], 'target': None}),
]


@pytest.mark.parametrize('activator, available_calls', [
    (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS),
    (NetplanActivator, NETPLAN_AVAILABLE_CALLS),
    (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS),
    (NetworkdActivator, NETWORKD_AVAILABLE_CALLS),
])
class TestActivatorsAvailable:
    def test_available(
        self, activator, available_calls, available_mocks
    ):
        activator.available()
        assert available_mocks.m_which.call_args_list == available_calls


IF_UP_DOWN_BRING_UP_CALL_LIST = [
    ((['ifup', 'eth0'], ), {}),
    ((['ifup', 'eth1'], ), {}),
]

NETWORK_MANAGER_BRING_UP_CALL_LIST = [
    ((['nmcli', 'connection', 'up', 'ifname', 'eth0'], ), {}),
    ((['nmcli', 'connection', 'up', 'ifname', 'eth1'], ), {}),
]

NETWORKD_BRING_UP_CALL_LIST = [
    ((['ip', 'link', 'set', 'up', 'eth0'], ), {}),
    ((['ip', 'link', 'set', 'up', 'eth1'], ), {}),
    ((['systemctl', 'restart', 'systemd-networkd', 'systemd-resolved'], ), {}),
]


@pytest.mark.parametrize('activator, expected_call_list', [
    (IfUpDownActivator, IF_UP_DOWN_BRING_UP_CALL_LIST),
    (NetplanActivator, NETPLAN_CALL_LIST),
    (NetworkManagerActivator, NETWORK_MANAGER_BRING_UP_CALL_LIST),
    (NetworkdActivator, NETWORKD_BRING_UP_CALL_LIST),
])
class TestActivatorsBringUp:
    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_up_interface(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        activator.bring_up_interface('eth0')
        assert len(m_subp.call_args_list) == 1
        assert m_subp.call_args_list[0] == expected_call_list[0]

    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_up_interfaces(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        index = 0
        activator.bring_up_interfaces(['eth0', 'eth1'])
        for call in m_subp.call_args_list:
            assert call == expected_call_list[index]
            index += 1

    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_up_all_interfaces_v1(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        network_state = parse_net_config_data(load(V1_CONFIG))
        activator.bring_up_all_interfaces(network_state)
        for call in m_subp.call_args_list:
            assert call in expected_call_list

    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_up_all_interfaces_v2(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        network_state = parse_net_config_data(load(V2_CONFIG))
        activator.bring_up_all_interfaces(network_state)
        for call in m_subp.call_args_list:
            assert call in expected_call_list


IF_UP_DOWN_BRING_DOWN_CALL_LIST = [
    ((['ifdown', 'eth0'], ), {}),
    ((['ifdown', 'eth1'], ), {}),
]

NETWORK_MANAGER_BRING_DOWN_CALL_LIST = [
    ((['nmcli', 'connection', 'down', 'eth0'], ), {}),
    ((['nmcli', 'connection', 'down', 'eth1'], ), {}),
]

NETWORKD_BRING_DOWN_CALL_LIST = [
    ((['ip', 'link', 'set', 'down', 'eth0'], ), {}),
    ((['ip', 'link', 'set', 'down', 'eth1'], ), {}),
]


@pytest.mark.parametrize('activator, expected_call_list', [
    (IfUpDownActivator, IF_UP_DOWN_BRING_DOWN_CALL_LIST),
    (NetplanActivator, NETPLAN_CALL_LIST),
    (NetworkManagerActivator, NETWORK_MANAGER_BRING_DOWN_CALL_LIST),
    (NetworkdActivator, NETWORKD_BRING_DOWN_CALL_LIST),
])
class TestActivatorsBringDown:
    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_down_interface(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        activator.bring_down_interface('eth0')
        assert len(m_subp.call_args_list) == 1
        assert m_subp.call_args_list[0] == expected_call_list[0]

    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_down_interfaces(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        activator.bring_down_interfaces(['eth0', 'eth1'])
        assert expected_call_list == m_subp.call_args_list

    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_down_all_interfaces_v1(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        network_state = parse_net_config_data(load(V1_CONFIG))
        activator.bring_down_all_interfaces(network_state)
        for call in m_subp.call_args_list:
            assert call in expected_call_list

    @patch('cloudinit.subp.subp', return_value=('', ''))
    def test_bring_down_all_interfaces_v2(
        self, m_subp, activator, expected_call_list, available_mocks
    ):
        network_state = parse_net_config_data(load(V2_CONFIG))
        activator.bring_down_all_interfaces(network_state)
        for call in m_subp.call_args_list:
            assert call in expected_call_list