# Copyright 2015-2017 Yelp 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 subprocess

import mock
import pytest
import yaml

from paasta_tools import firewall
from paasta_tools import firewall_update
from paasta_tools.utils import TimeoutError


def test_parse_args_daemon():
    args = firewall_update.parse_args(
        [
            "-d",
            "mysoadir",
            "-v",
            "--synapse-service-dir",
            "myservicedir",
            "daemon",
            "-u",
            "123",
        ]
    )
    assert args.mode == "daemon"
    assert args.synapse_service_dir == "myservicedir"
    assert args.soa_dir == "mysoadir"
    assert args.update_secs == 123
    assert args.verbose


def test_parse_args_default_daemon():
    args = firewall_update.parse_args(["daemon"])
    assert args.mode == "daemon"
    assert args.synapse_service_dir == firewall.DEFAULT_SYNAPSE_SERVICE_DIR
    assert args.soa_dir == firewall_update.DEFAULT_SOA_DIR
    assert args.update_secs == firewall_update.DEFAULT_UPDATE_SECS
    assert not args.verbose


def test_parse_args_cron():
    args = firewall_update.parse_args(["-d", "mysoadir", "-v", "cron"])
    assert args.mode == "cron"
    assert args.soa_dir == "mysoadir"
    assert args.verbose


def test_parse_args_default_cron():
    args = firewall_update.parse_args(["cron"])
    assert args.mode == "cron"
    assert args.soa_dir == firewall_update.DEFAULT_SOA_DIR
    assert not args.verbose


@mock.patch(
    "paasta_tools.utils.load_system_paasta_config",
    autospec=True,
)
@mock.patch.object(
    firewall_update,
    "load_system_paasta_config",
    autospec=True,
    return_value=mock.Mock(**{"get_cluster.return_value": "mycluster"}),
)
@mock.patch.object(
    firewall,
    "services_running_here",
    autospec=True,
    return_value=(
        ("myservice", "hassecurityinbound", "02:42:a9:fe:00:0a", "1.1.1.1"),
        ("myservice", "hassecurityoutbound", "02:42:a9:fe:00:0a", "1.1.1.1"),
    ),
)
def test_smartstack_dependencies_of_running_firewalled_services(_, __, ___, tmpdir):
    soa_dir = tmpdir.mkdir("yelpsoa")
    myservice_dir = soa_dir.mkdir("myservice")

    marathon_config = {
        "hassecurityinbound": {
            "dependencies_reference": "my_ref",
            "security": {"inbound_firewall": "reject"},
        },
        "hassecurityoutbound": {
            "dependencies_reference": "my_ref",
            "security": {"outbound_firewall": "block"},
        },
        "nosecurity": {"dependencies_reference": "my_ref"},
    }
    myservice_dir.join("marathon-mycluster.yaml").write(yaml.safe_dump(marathon_config))

    dependencies_config = {
        "my_ref": [
            {"well-known": "internet"},
            {"smartstack": "mydependency.depinstance"},
            {"smartstack": "another.one"},
        ]
    }
    myservice_dir.join("dependencies.yaml").write(yaml.safe_dump(dependencies_config))

    result = firewall_update.smartstack_dependencies_of_running_firewalled_services(
        soa_dir=str(soa_dir)
    )
    assert dict(result) == {
        "mydependency.depinstance": {
            ("myservice", "hassecurityinbound"),
            ("myservice", "hassecurityoutbound"),
        },
        "another.one": {
            ("myservice", "hassecurityinbound"),
            ("myservice", "hassecurityoutbound"),
        },
    }


@mock.patch.object(
    firewall_update,
    "smartstack_dependencies_of_running_firewalled_services",
    autospec=True,
)
@mock.patch.object(
    firewall_update, "process_inotify_event", side_effect=StopIteration, autospec=True
)
def test_run_daemon(process_inotify_mock, smartstack_deps_mock, mock_daemon_args):
    class kill_after_too_long:
        def __init__(self):
            self.count = 0

        def __call__(self, *args, **kwargs):
            self.count += 1
            assert self.count <= 5, "Took too long to detect file change"
            return {}

    smartstack_deps_mock.side_effect = kill_after_too_long()
    subprocess.Popen(
        [
            "bash",
            "-c",
            "sleep 0.2; echo > %s/mydep.depinstance.json"
            % mock_daemon_args.synapse_service_dir,
        ]
    )
    with pytest.raises(StopIteration):
        firewall_update.run_daemon(mock_daemon_args)
    assert smartstack_deps_mock.call_count > 0
    assert process_inotify_mock.call_args[0][0][3] == b"mydep.depinstance.json"
    assert process_inotify_mock.call_args[0][1] == {}


@mock.patch.object(firewall, "firewall_flock", autospec=True)
@mock.patch.object(firewall, "general_update", autospec=True)
def test_run_cron(mock_general_update, mock_firewall_flock, mock_cron_args):
    firewall_update.run_cron(mock_cron_args)
    assert mock_general_update.called is True
    assert mock_firewall_flock.return_value.__enter__.called is True


@mock.patch.object(
    firewall, "firewall_flock", autospec=True, side_effect=TimeoutError("Oh noes")
)
@mock.patch.object(firewall, "general_update", autospec=True)
def test_run_cron_flock_error(mock_general_update, mock_firewall_flock, mock_cron_args):
    with pytest.raises(TimeoutError):
        firewall_update.run_cron(mock_cron_args)


@mock.patch.object(firewall_update, "log", autospec=True)
@mock.patch.object(firewall_update.firewall, "ensure_service_chains", autospec=True)
@mock.patch.object(firewall_update.firewall, "active_service_groups", autospec=True)
@mock.patch.object(firewall, "firewall_flock", autospec=True)
def test_process_inotify_event(
    firewall_flock_mock,
    active_service_groups_mock,
    ensure_service_chains_mock,
    log_mock,
):
    active_service_groups_mock.return_value = {
        firewall.ServiceGroup("myservice", "myinstance"): {"00:00:00:00:00:00"},
        firewall.ServiceGroup("anotherservice", "instance"): {"11:11:11:11:11:11"},
        firewall.ServiceGroup("thirdservice", "instance"): {"22:22:22:22:22:22"},
    }

    services_by_dependencies = {
        "mydep.depinstance": {
            ("myservice", "myinstance"),
            ("anotherservice", "instance"),
        }
    }
    soa_dir = mock.Mock()
    synapse_service_dir = mock.Mock()
    firewall_update.process_inotify_event(
        (None, None, None, b"mydep.depinstance.json"),
        services_by_dependencies,
        soa_dir,
        synapse_service_dir,
    )
    assert log_mock.debug.call_count == 3
    log_mock.debug.assert_any_call("Updated ('myservice', 'myinstance')")
    log_mock.debug.assert_any_call("Updated ('anotherservice', 'instance')")
    assert ensure_service_chains_mock.mock_calls == [
        mock.call(
            {
                firewall.ServiceGroup("myservice", "myinstance"): {"00:00:00:00:00:00"},
                firewall.ServiceGroup("anotherservice", "instance"): {
                    "11:11:11:11:11:11"
                },
            },
            soa_dir,
            synapse_service_dir,
        )
    ]

    assert firewall_flock_mock.return_value.__enter__.called is True

    # Verify that tmp writes do not apply
    log_mock.reset_mock()
    ensure_service_chains_mock.reset_mock()
    firewall_update.process_inotify_event(
        (None, None, None, b"mydep.depinstance.tmp"),
        services_by_dependencies,
        soa_dir,
        synapse_service_dir,
    )
    assert log_mock.debug.call_count == 1
    assert ensure_service_chains_mock.call_count == 0


@mock.patch.object(firewall_update, "log", autospec=True)
@mock.patch.object(firewall_update.firewall, "ensure_service_chains", autospec=True)
@mock.patch.object(firewall_update.firewall, "active_service_groups", autospec=True)
@mock.patch.object(
    firewall, "firewall_flock", autospec=True, side_effect=TimeoutError("Oh noes")
)
def test_process_inotify_event_flock_error(
    firewall_flock_mock,
    active_service_groups_mock,
    ensure_service_chains_mock,
    log_mock,
):
    active_service_groups_mock.return_value = {
        firewall.ServiceGroup("myservice", "myinstance"): {"00:00:00:00:00:00"},
        firewall.ServiceGroup("anotherservice", "instance"): {"11:11:11:11:11:11"},
        firewall.ServiceGroup("thirdservice", "instance"): {"22:22:22:22:22:22"},
    }

    services_by_dependencies = {
        "mydep.depinstance": {
            ("myservice", "myinstance"),
            ("anotherservice", "instance"),
        }
    }
    soa_dir = mock.Mock()
    synapse_service_dir = mock.Mock()
    firewall_update.process_inotify_event(
        (None, None, None, b"mydep.depinstance.json"),
        services_by_dependencies,
        soa_dir,
        synapse_service_dir,
    )
    assert log_mock.debug.call_count == 1
    assert log_mock.error.call_count == 1


@pytest.fixture
def mock_daemon_args(tmpdir):
    return firewall_update.parse_args(
        [
            "-d",
            str(tmpdir.mkdir("yelpsoa")),
            "--synapse-service-dir",
            str(tmpdir.mkdir("synapse")),
            "daemon",
        ]
    )


@pytest.fixture
def mock_cron_args(tmpdir):
    return firewall_update.parse_args(
        [
            "-d",
            str(tmpdir.mkdir("yelpsoa")),
            "--synapse-service-dir",
            str(tmpdir.mkdir("synapse")),
            "cron",
        ]
    )
