import sys
from utilities_common.db import Db
from unittest import mock
from click.testing import CliRunner

import pytest
sys.path.append("../cli/config/plugins/")
import dhcp_relay

config_dhcp_relay_add_output = """\
Added DHCP relay address [{}] to Vlan1000
Restarting DHCP relay service...
"""
config_dhcp_relay_del_output = """\
Removed DHCP relay address [{}] from Vlan1000
Restarting DHCP relay service...
"""
expected_dhcp_relay_add_config_db_output = {
    "ipv4": {
        "dhcp_servers": [
            "192.0.0.1", "192.0.0.3"]
    },
    "ipv6": {
        "dhcpv6_servers": [
            "fc02:2000::1", "fc02:2000::3"]
    }
}
expected_dhcp_relay_del_config_db_output = {
    "ipv4": {
        "dhcp_servers": [
            "192.0.0.1"
        ]
    },
    "ipv6": {
        "dhcpv6_servers": [
            "fc02:2000::1"
        ]
    }
}
expected_dhcp_relay_add_multi_config_db_output = {
    "ipv4": {
        "dhcp_servers": [
            "192.0.0.1", "192.0.0.3", "192.0.0.4", "192.0.0.5"
        ]
    },
    "ipv6": {
        "dhcpv6_servers": [
            "fc02:2000::1", "fc02:2000::3", "fc02:2000::4", "fc02:2000::5"
        ]
    }
}

IP_VER_TEST_PARAM_MAP = {
    "ipv4": {
        "command": "helper",
        "ips": [
            "192.0.0.3",
            "192.0.0.4",
            "192.0.0.5"
        ],
        "exist_ip": "192.0.0.1",
        "nonexist_ip": "192.0.0.2",
        "invalid_ip": "192.0.0",
        "table": "VLAN"
    },
    "ipv6": {
        "command": "destination",
        "ips": [
            "fc02:2000::3",
            "fc02:2000::4",
            "fc02:2000::5"
        ],
        "exist_ip": "fc02:2000::1",
        "nonexist_ip": "fc02:2000::2",
        "invalid_ip": "fc02:2000:",
        "table": "DHCP_RELAY"
    }
}


@pytest.fixture(scope="module", params=["ipv4", "ipv6"])
def ip_version(request):
    """
    Parametrize Ip version

    Args:
        request: pytest request object

    Returns:
        Ip version needed for test case
    """
    return request.param


@pytest.fixture(scope="module", params=["add", "del"])
def op(request):
    """
    Parametrize operate tpye

    Args:
        request: pytest request object

    Returns:
        Operate tpye
    """
    return request.param


class TestConfigDhcpRelay(object):
    def test_plugin_registration(self):
        cli = mock.MagicMock()
        dhcp_relay.register(cli)

    def test_config_dhcp_relay_add_del_with_nonexist_vlanid_ipv4(self, op):
        runner = CliRunner()

        ip_version = "ipv4"
        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands[op], ["1001", IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0]])
            print(result.exit_code)
            print(result.stdout)
            assert result.exit_code != 0
            assert "Error: Vlan1001 doesn't exist" in result.output
            assert mock_run_command.call_count == 0

    def test_config_dhcp_relay_del_with_nonexist_vlanid_ipv6(self):
        runner = CliRunner()

        op = "del"
        ip_version = "ipv6"
        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands[op], ["1001", IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0]])
            print(result.exit_code)
            print(result.stdout)
            assert result.exit_code != 0
            assert "Error: Vlan1001 doesn't exist" in result.output
            assert mock_run_command.call_count == 0

    def test_config_add_del_dhcp_relay_with_invalid_ip(self, ip_version, op):
        runner = CliRunner()
        invalid_ip = IP_VER_TEST_PARAM_MAP[ip_version]["invalid_ip"]

        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands[op], ["1000", invalid_ip])
            print(result.exit_code)
            print(result.output)
            assert result.exit_code != 0
            assert "Error: {} is invalid IP address".format(invalid_ip) in result.output
            assert mock_run_command.call_count == 0

    def test_config_add_dhcp_with_exist_ip(self, mock_cfgdb, ip_version):
        runner = CliRunner()
        db = Db()
        db.cfgdb = mock_cfgdb
        exist_ip = IP_VER_TEST_PARAM_MAP[ip_version]["exist_ip"]

        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["add"], ["1000", exist_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert "{} is already a DHCP relay for Vlan1000".format(exist_ip) in result.output
            assert mock_run_command.call_count == 0

    def test_config_del_nonexist_dhcp_relay(self, mock_cfgdb, ip_version):
        runner = CliRunner()
        db = Db()
        db.cfgdb = mock_cfgdb
        nonexist_ip = IP_VER_TEST_PARAM_MAP[ip_version]["nonexist_ip"]

        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["del"], ["1000", nonexist_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code != 0
            assert "Error: {} is not a DHCP relay for Vlan1000".format(nonexist_ip) in result.output
            assert mock_run_command.call_count == 0

    def test_config_add_del_dhcp_relay(self, mock_cfgdb, ip_version):
        runner = CliRunner()
        db = Db()
        db.cfgdb = mock_cfgdb
        test_ip = IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0]
        config_db_table = IP_VER_TEST_PARAM_MAP[ip_version]["table"]

        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            # add new dhcp relay
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["add"], ["1000", test_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert result.output == config_dhcp_relay_add_output.format(test_ip)
            assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \
                == expected_dhcp_relay_add_config_db_output[ip_version]
            assert mock_run_command.call_count == 3
            db.cfgdb.set_entry.assert_called_once_with(config_db_table, "Vlan1000",
                                                       expected_dhcp_relay_add_config_db_output[ip_version])

        db.cfgdb.set_entry.reset_mock()
        # del dhcp relay
        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["del"], ["1000", test_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert result.output == config_dhcp_relay_del_output.format(test_ip)
            assert mock_run_command.call_count == 3
            assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \
                == expected_dhcp_relay_del_config_db_output[ip_version]
            db.cfgdb.set_entry.assert_called_once_with(config_db_table, "Vlan1000",
                                                       expected_dhcp_relay_del_config_db_output[ip_version])

    def test_config_add_del_dhcp_relay_with_enable_dhcp_server(self, mock_cfgdb):
        runner = CliRunner()
        db = Db()
        db.cfgdb = mock_cfgdb
        ip_version = "ipv4"
        test_ip = IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0]

        with mock.patch("utilities_common.cli.run_command"), \
             mock.patch.object(dhcp_relay, "is_dhcp_server_enabled", return_value=True):
            # add new dhcp relay
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["add"], ["1000", test_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert "Cannot change ipv4 dhcp_relay configuration when dhcp_server feature is enabled" in result.output

        db.cfgdb.set_entry.reset_mock()
        # del dhcp relay
        with mock.patch("utilities_common.cli.run_command"), \
             mock.patch.object(dhcp_relay, "is_dhcp_server_enabled", return_value=True):
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["del"], ["1000", test_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert "Cannot change ipv4 dhcp_relay configuration when dhcp_server feature is enabled" in result.output

    def test_config_add_del_multiple_dhcp_relay(self, mock_cfgdb, ip_version):
        runner = CliRunner()
        db = Db()
        db.cfgdb = mock_cfgdb
        test_ips = IP_VER_TEST_PARAM_MAP[ip_version]["ips"]
        config_db_table = IP_VER_TEST_PARAM_MAP[ip_version]["table"]

        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            # add new dhcp relay
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["add"], ["1000"] + test_ips, obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert result.output == config_dhcp_relay_add_output.format(",".join(test_ips))
            assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \
                == expected_dhcp_relay_add_multi_config_db_output[ip_version]
            assert mock_run_command.call_count == 3
            db.cfgdb.set_entry.assert_called_once_with(config_db_table, "Vlan1000",
                                                       expected_dhcp_relay_add_multi_config_db_output[ip_version])

        db.cfgdb.set_entry.reset_mock()
        # del dhcp relay
        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands["del"], ["1000"] + test_ips, obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert result.output == config_dhcp_relay_del_output.format(",".join(test_ips))
            assert mock_run_command.call_count == 3
            assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \
                == expected_dhcp_relay_del_config_db_output[ip_version]
            db.cfgdb.set_entry.assert_called_once_with(config_db_table, "Vlan1000",
                                                       expected_dhcp_relay_del_config_db_output[ip_version])

    def test_config_add_del_duplicate_dhcp_relay(self, mock_cfgdb, ip_version, op):
        runner = CliRunner()
        db = Db()
        db.cfgdb = mock_cfgdb
        test_ip = IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0] if op == "add" \
            else IP_VER_TEST_PARAM_MAP[ip_version]["exist_ip"]

        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands[op], ["1000", test_ip, test_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code != 0
            assert "Error: Find duplicate DHCP relay ip {} in {} list".format(test_ip, op) in result.output
            assert mock_run_command.call_count == 0

    def test_config_add_dhcp_relay_ipv6_with_non_entry(self, mock_cfgdb):
        op = "add"
        ip_version = "ipv6"
        test_ip = IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0]
        runner = CliRunner()
        db = Db()
        db.cfgdb = mock_cfgdb
        table = IP_VER_TEST_PARAM_MAP[ip_version]["table"]
        db.cfgdb.set_entry(table, "Vlan1000", None)
        assert db.cfgdb.get_entry(table, "Vlan1000") == {}
        assert len(db.cfgdb.get_keys(table)) == 0

        with mock.patch("utilities_common.cli.run_command") as mock_run_command:
            result = runner.invoke(dhcp_relay.dhcp_relay.commands[ip_version]
                                   .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]]
                                   .commands[op], ["1000", test_ip], obj=db)
            print(result.exit_code)
            print(result.output)
            assert result.exit_code == 0
            assert db.cfgdb.get_entry(table, "Vlan1000") == {"dhcpv6_servers": [test_ip]}
            assert mock_run_command.call_count == 3