import os
import shutil
import tempfile
import time

import salt.utils.files
import salt.utils.platform
from salt.beacons import watchdog
from salt.ext.six.moves import range
from tests.support.helpers import slowTest
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.unit import TestCase, skipIf


def check_events(config):
    total_delay = 1
    delay_per_loop = 20e-3

    for _ in range(int(total_delay / delay_per_loop)):
        events = watchdog.beacon(config)

        if events:
            return events

        time.sleep(delay_per_loop)

    return []


def create(path, content=None):
    with salt.utils.files.fopen(path, "w") as f:
        if content:
            f.write(content)
        os.fsync(f)


@skipIf(not watchdog.HAS_WATCHDOG, "watchdog is not available")
@skipIf(
    salt.utils.platform.is_darwin(),
    "Tests were being skipped pre macos under nox. Keep it like that for now.",
)
class IWatchdogBeaconTestCase(TestCase, LoaderModuleMockMixin):
    """
    Test case for salt.beacons.watchdog
    """

    def setup_loader_modules(self):
        return {watchdog: {}}

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def tearDown(self):
        watchdog.close({})
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def assertValid(self, config):
        ret = watchdog.validate(config)
        self.assertEqual(ret, (True, "Valid beacon configuration"))

    def test_empty_config(self):
        config = [{}]
        ret = watchdog.beacon(config)
        self.assertEqual(ret, [])

    @skipIf(
        salt.utils.platform.is_freebsd(),
        "Skip on FreeBSD - does not yet have full inotify/watchdog support",
    )
    def test_file_create(self):
        path = os.path.join(self.tmpdir, "tmpfile")

        config = [{"directories": {self.tmpdir: {"mask": ["create"]}}}]
        self.assertValid(config)
        self.assertEqual(watchdog.beacon(config), [])

        create(path)

        ret = check_events(config)
        self.assertEqual(len(ret), 1)
        self.assertEqual(ret[0]["path"], path)
        self.assertEqual(ret[0]["change"], "created")

    def test_file_modified(self):
        path = os.path.join(self.tmpdir, "tmpfile")
        # Create triggers a modify event along with the create event in Py3
        # So, let's do this before configuring the beacon
        create(path)

        config = [{"directories": {self.tmpdir: {"mask": ["modify"]}}}]
        self.assertValid(config)
        self.assertEqual(watchdog.beacon(config), [])

        create(path, "some content")

        ret = check_events(config)

        modified = False
        for event in ret:
            # "modified" requires special handling
            # A modification sometimes triggers 2 modified events depending on
            # the OS and the python version
            # When the "modified" event triggers on modify, it will have the
            # path to the temp file (path), other modified events will contain
            # the path minus "tmpfile" and will not match. That's how we'll
            # distinguish the two
            if event["change"] == "modified":
                if event["path"] == path:
                    modified = True

        # Check results of the for loop to validate modified
        self.assertTrue(modified)

    def test_file_deleted(self):
        path = os.path.join(self.tmpdir, "tmpfile")
        create(path)

        config = [{"directories": {self.tmpdir: {"mask": ["delete"]}}}]
        self.assertValid(config)
        self.assertEqual(watchdog.beacon(config), [])

        os.remove(path)

        ret = check_events(config)
        self.assertEqual(len(ret), 1)
        self.assertEqual(ret[0]["path"], path)
        self.assertEqual(ret[0]["change"], "deleted")

    @skipIf(
        salt.utils.platform.is_freebsd(),
        "Skip on FreeBSD - does not yet have full inotify/watchdog support",
    )
    def test_file_moved(self):
        path = os.path.join(self.tmpdir, "tmpfile")
        create(path)

        config = [{"directories": {self.tmpdir: {"mask": ["move"]}}}]
        self.assertValid(config)
        self.assertEqual(watchdog.beacon(config), [])

        os.rename(path, path + "_moved")

        ret = check_events(config)
        self.assertEqual(len(ret), 1)
        self.assertEqual(ret[0]["path"], path)
        self.assertEqual(ret[0]["change"], "moved")

    @skipIf(
        salt.utils.platform.is_freebsd(),
        "Skip on FreeBSD - does not yet have full inotify/watchdog support",
    )
    def test_file_create_in_directory(self):
        config = [{"directories": {self.tmpdir: {"mask": ["create"]}}}]
        self.assertValid(config)
        self.assertEqual(watchdog.beacon(config), [])

        path = os.path.join(self.tmpdir, "tmpfile")
        create(path)

        ret = check_events(config)
        self.assertEqual(len(ret), 1)
        self.assertEqual(ret[0]["path"], path)
        self.assertEqual(ret[0]["change"], "created")

    @skipIf(
        salt.utils.platform.is_freebsd(),
        "Skip on FreeBSD - does not yet have full inotify/watchdog support",
    )
    @slowTest
    def test_trigger_all_possible_events(self):
        path = os.path.join(self.tmpdir, "tmpfile")
        moved = path + "_moved"

        config = [{"directories": {self.tmpdir: {}}}]
        self.assertValid(config)
        self.assertEqual(watchdog.beacon(config), [])

        # create
        create(path)
        # modify
        create(path, "modified content")
        # move
        os.rename(path, moved)
        # delete
        os.remove(moved)

        # Give the events time to load into the queue
        time.sleep(1)

        ret = check_events(config)

        events = {"created": "", "deleted": "", "moved": ""}
        modified = False
        for event in ret:
            if event["change"] == "created":
                self.assertEqual(event["path"], path)
                events.pop("created", "")
            if event["change"] == "moved":
                self.assertEqual(event["path"], path)
                events.pop("moved", "")
            if event["change"] == "deleted":
                self.assertEqual(event["path"], moved)
                events.pop("deleted", "")
            # "modified" requires special handling
            # All events [created, moved, deleted] also trigger a "modified"
            # event on Linux
            # Only the "created" event triggers a modified event on Py3 Windows
            # When the "modified" event triggers on modify, it will have the
            # path to the temp file (path), other modified events will contain
            # the path minus "tmpfile" and will not match. That's how we'll
            # distinguish the two
            if event["change"] == "modified":
                if event["path"] == path:
                    modified = True

        # Check results of the for loop to validate modified
        self.assertTrue(modified)

        # Make sure all events were checked
        self.assertDictEqual(events, {})
