import logging
import os
import shutil
import tempfile
import time

import salt.config
import salt.version
from tests.support.case import MultimasterModuleCase
from tests.support.helpers import slowTest
from tests.support.mixins import AdaptedConfigurationTestCaseMixin
from tests.support.unit import skipIf

try:
    import pyinotify  # pylint: disable=unused-import

    HAS_PYINOTIFY = True
except ImportError:
    HAS_PYINOTIFY = False


log = logging.getLogger(__name__)


@skipIf(not HAS_PYINOTIFY, "pyinotify is not available")
@skipIf(
    salt.utils.platform.is_freebsd(),
    "Skip on FreeBSD, IN_CREATE event is not supported",
)
class TestBeaconsInotify(MultimasterModuleCase, AdaptedConfigurationTestCaseMixin):
    """
    Validate the inotify beacon in multimaster environment
    """

    def setUp(self):
        self.tmpdir = salt.utils.stringutils.to_unicode(tempfile.mkdtemp())
        self.addCleanup(shutil.rmtree, self.tmpdir, ignore_errors=True)

    @slowTest
    def test_beacons_duplicate_53344(self):
        # Also add a status beacon to use it for interval checks
        res = self.run_function(
            "beacons.add",
            ("inotify", [{"files": {self.tmpdir: {"mask": ["create"]}}}]),
            master_tgt="mm-master",
        )
        log.debug("Inotify beacon add returned: %s", res)
        self.assertTrue(res.get("result"))
        self.addCleanup(
            self.run_function, "beacons.delete", ("inotify",), master_tgt="mm-master"
        )
        res = self.run_function(
            "beacons.add", ("status", [{"time": ["all"]}]), master_tgt="mm-master",
        )
        log.debug("Status beacon add returned: %s", res)
        self.assertTrue(res.get("result"))
        self.addCleanup(
            self.run_function, "beacons.delete", ("status",), master_tgt="mm-master"
        )

        # Ensure beacons are added.
        res = self.run_function(
            "beacons.list", (), return_yaml=False, master_tgt="mm-master",
        )
        log.debug("Beacons list: %s", res)
        self.assertEqual(
            {
                "inotify": [{"files": {self.tmpdir: {"mask": ["create"]}}}],
                "status": [{"time": ["all"]}],
            },
            res,
        )

        file_path = os.path.join(self.tmpdir, "tmpfile")
        master_listener = salt.utils.event.get_master_event(
            self.mm_master_opts, sock_dir=self.mm_master_opts["sock_dir"]
        )
        self.addCleanup(master_listener.destroy)
        sub_master_listener = salt.utils.event.get_master_event(
            self.mm_sub_master_opts, sock_dir=self.mm_sub_master_opts["sock_dir"]
        )
        self.addCleanup(sub_master_listener.destroy)

        # We have to wait beacon first execution that would configure the inotify watch.
        # Since beacons will be executed both together waiting for the first status beacon event
        # which will mean the inotify beacon was executed too.
        start = time.time()
        stop_at = start + self.mm_minion_opts["loop_interval"] * 2 + 60
        event = sub_event = None
        while True:
            if time.time() > stop_at:
                break
            if not event:
                event = master_listener.get_event(
                    full=True,
                    wait=1,
                    tag="salt/beacon/mm-minion/status",
                    match_type="startswith",
                )
            if sub_event is None:
                sub_event = sub_master_listener.get_event(
                    full=True,
                    wait=1,
                    tag="salt/beacon/mm-minion/status",
                    match_type="startswith",
                )
            if event and sub_event:
                break
        log.debug("Status events received: %s, %s", event, sub_event)

        if not event or not sub_event:
            self.fail("Failed to receive at least one of the status events")

        with salt.utils.files.fopen(file_path, "w") as f:
            pass

        start = time.time()
        # Now in successful case this test will get results at most in 2 loop intervals.
        # Waiting for 2 loops intervals + some seconds to the hardware stupidity.
        stop_at = start + self.mm_minion_opts["loop_interval"] * 3 + 60
        event = sub_event = None
        expected_tag = salt.utils.stringutils.to_str(
            "salt/beacon/mm-minion/inotify/{}".format(self.tmpdir)
        )
        while True:
            if time.time() > stop_at:
                break
            if not event:
                event = master_listener.get_event(
                    full=True, wait=1, tag=expected_tag, match_type="startswith"
                )
            if sub_event is None:
                sub_event = sub_master_listener.get_event(
                    full=True, wait=1, tag=expected_tag, match_type="startswith"
                )
            if event and sub_event:
                break
        log.debug("Inotify events received: %s, %s", event, sub_event)

        if not event or not sub_event:
            self.fail("Failed to receive at least one of the inotify events")

        # We can't determine the timestamp so remove it from results
        if event:
            del event["data"]["_stamp"]
        if sub_event:
            del sub_event["data"]["_stamp"]

        expected = {
            "data": {"path": file_path, "change": "IN_CREATE", "id": "mm-minion"},
            "tag": expected_tag,
        }

        # It's better to compare both at once to see both responses in the error log.
        self.assertEqual((expected, expected), (event, sub_event))
