"""Tests for rbsp.indexer.watcher -- FileEvent, _PollingWatcher, FileSystemWatcher."""

import os
import sys
import tempfile
import threading
import time
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.indexer.watcher import FileEvent, _PollingWatcher, FileSystemWatcher


# ---------------------------------------------------------------------------
# FileEvent enum
# ---------------------------------------------------------------------------

class TestFileEvent(unittest.TestCase):
    """Tests for the FileEvent enum values."""

    def test_created_exists(self):
        self.assertIsNotNone(FileEvent.CREATED)

    def test_modified_exists(self):
        self.assertIsNotNone(FileEvent.MODIFIED)

    def test_deleted_exists(self):
        self.assertIsNotNone(FileEvent.DELETED)

    def test_three_members(self):
        self.assertEqual(len(FileEvent), 3)

    def test_values_are_distinct(self):
        values = [e.value for e in FileEvent]
        self.assertEqual(len(values), len(set(values)))

    def test_names(self):
        names = {e.name for e in FileEvent}
        self.assertEqual(names, {"CREATED", "MODIFIED", "DELETED"})


# ---------------------------------------------------------------------------
# _PollingWatcher snapshot detection
# ---------------------------------------------------------------------------

class TestPollingWatcher(unittest.TestCase):
    """Test _PollingWatcher detects file creation, modification, and deletion."""

    def setUp(self):
        self.tmp_dir = tempfile.mkdtemp(prefix="rbsp_watcher_test_")
        self.events = []
        self.events_lock = threading.Lock()

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tmp_dir, ignore_errors=True)

    def _callback(self, event, path):
        with self.events_lock:
            self.events.append((event, path))

    def _get_events(self):
        with self.events_lock:
            return list(self.events)

    def test_detect_file_creation(self):
        watcher = _PollingWatcher(
            paths=[self.tmp_dir],
            callback=self._callback,
            poll_interval=0.1,
            recursive=True,
        )
        thread = threading.Thread(target=watcher.start, daemon=True)
        thread.start()

        # Give the initial snapshot time to be taken
        time.sleep(0.2)

        # Create a file
        filepath = os.path.join(self.tmp_dir, "new_file.txt")
        with open(filepath, "w") as f:
            f.write("hello")

        # Wait for polling cycle
        time.sleep(0.5)
        watcher.stop()
        thread.join(timeout=3)

        events = self._get_events()
        created_paths = [p for ev, p in events if ev == FileEvent.CREATED]
        self.assertTrue(
            any(filepath in p or os.path.basename(filepath) in p for p in created_paths),
            f"Expected CREATED event for {filepath}, got events: {events}",
        )

    def test_detect_file_modification(self):
        # Pre-create a file before the watcher starts
        filepath = os.path.join(self.tmp_dir, "existing.txt")
        with open(filepath, "w") as f:
            f.write("original")

        watcher = _PollingWatcher(
            paths=[self.tmp_dir],
            callback=self._callback,
            poll_interval=0.1,
            recursive=True,
        )
        thread = threading.Thread(target=watcher.start, daemon=True)
        thread.start()
        time.sleep(0.2)

        # Modify the file
        time.sleep(0.05)  # ensure mtime changes
        with open(filepath, "w") as f:
            f.write("modified")

        time.sleep(0.5)
        watcher.stop()
        thread.join(timeout=3)

        events = self._get_events()
        modified_paths = [p for ev, p in events if ev == FileEvent.MODIFIED]
        self.assertTrue(
            any(filepath in p for p in modified_paths),
            f"Expected MODIFIED event for {filepath}, got events: {events}",
        )

    def test_detect_file_deletion(self):
        # Pre-create a file
        filepath = os.path.join(self.tmp_dir, "to_delete.txt")
        with open(filepath, "w") as f:
            f.write("delete me")

        watcher = _PollingWatcher(
            paths=[self.tmp_dir],
            callback=self._callback,
            poll_interval=0.1,
            recursive=True,
        )
        thread = threading.Thread(target=watcher.start, daemon=True)
        thread.start()
        time.sleep(0.2)

        # Delete the file
        os.remove(filepath)

        time.sleep(0.5)
        watcher.stop()
        thread.join(timeout=3)

        events = self._get_events()
        deleted_paths = [p for ev, p in events if ev == FileEvent.DELETED]
        self.assertTrue(
            any(filepath in p for p in deleted_paths),
            f"Expected DELETED event for {filepath}, got events: {events}",
        )

    def test_filter_by_pattern(self):
        """Only files matching patterns trigger callbacks."""
        watcher = _PollingWatcher(
            paths=[self.tmp_dir],
            callback=self._callback,
            patterns=["*.py"],
            poll_interval=0.1,
            recursive=True,
        )
        thread = threading.Thread(target=watcher.start, daemon=True)
        thread.start()
        time.sleep(0.2)

        # Create a .py file and a .txt file
        py_path = os.path.join(self.tmp_dir, "test.py")
        txt_path = os.path.join(self.tmp_dir, "test.txt")
        with open(py_path, "w") as f:
            f.write("# python")
        with open(txt_path, "w") as f:
            f.write("text")

        time.sleep(0.5)
        watcher.stop()
        thread.join(timeout=3)

        events = self._get_events()
        event_paths = [p for _, p in events]
        # The .py file should be reported
        self.assertTrue(
            any(py_path in p for p in event_paths),
            f"Expected event for {py_path}",
        )
        # The .txt file should NOT be reported
        self.assertFalse(
            any(txt_path in p for p in event_paths),
            f"Did not expect event for {txt_path}, got events: {events}",
        )


# ---------------------------------------------------------------------------
# FileSystemWatcher lifecycle
# ---------------------------------------------------------------------------

class TestFileSystemWatcher(unittest.TestCase):
    """Test FileSystemWatcher start/stop lifecycle and is_running property."""

    def setUp(self):
        self.tmp_dir = tempfile.mkdtemp(prefix="rbsp_fsw_test_")

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tmp_dir, ignore_errors=True)

    def test_not_running_initially(self):
        watcher = FileSystemWatcher(
            paths=[self.tmp_dir],
            callback=lambda e, p: None,
        )
        self.assertFalse(watcher.is_running)

    def test_start_sets_running(self):
        watcher = FileSystemWatcher(
            paths=[self.tmp_dir],
            callback=lambda e, p: None,
            poll_interval=0.1,
        )
        watcher.start()
        time.sleep(0.2)
        self.assertTrue(watcher.is_running)
        watcher.stop()

    def test_stop_clears_running(self):
        watcher = FileSystemWatcher(
            paths=[self.tmp_dir],
            callback=lambda e, p: None,
            poll_interval=0.1,
        )
        watcher.start()
        time.sleep(0.2)
        watcher.stop(timeout=3)
        self.assertFalse(watcher.is_running)

    def test_start_idempotent(self):
        """Calling start() twice does not create a second thread."""
        watcher = FileSystemWatcher(
            paths=[self.tmp_dir],
            callback=lambda e, p: None,
            poll_interval=0.1,
        )
        watcher.start()
        time.sleep(0.1)
        thread1 = watcher._thread
        watcher.start()  # should be a no-op
        thread2 = watcher._thread
        self.assertIs(thread1, thread2)
        watcher.stop()

    def test_stop_without_start(self):
        """Stopping a watcher that was never started should not raise."""
        watcher = FileSystemWatcher(
            paths=[self.tmp_dir],
            callback=lambda e, p: None,
        )
        watcher.stop()  # should not raise
        self.assertFalse(watcher.is_running)


if __name__ == "__main__":
    unittest.main()
