from __future__ import annotations

import time
from pathlib import Path
from unittest.mock import MagicMock

from blackbox.dashboard.watcher import LIVE_THRESHOLD_S, SessionWatcher


class TestSessionWatcherLiveIds:
    def test_empty_initially(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        assert set() == w.live_session_ids()

    def test_recent_modification_is_live(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        event = MagicMock()
        event.is_directory = False
        event.src_path = str(tmp_path / "abc123.jsonl")
        w.on_modified(event)
        assert {"abc123"} == w.live_session_ids()

    def test_old_modification_expires(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        with w._lock:
            w._last_modified["old-session"] = time.time() - LIVE_THRESHOLD_S - 10
        assert set() == w.live_session_ids()

    def test_non_jsonl_ignored(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        event = MagicMock()
        event.is_directory = False
        event.src_path = str(tmp_path / "notes.txt")
        w.on_modified(event)
        assert set() == w.live_session_ids()

    def test_directory_events_ignored(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        event = MagicMock()
        event.is_directory = True
        event.src_path = str(tmp_path / "somedir")
        w.on_modified(event)
        assert set() == w.live_session_ids()

    def test_on_created_delegates_to_on_modified(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        event = MagicMock()
        event.is_directory = False
        event.src_path = str(tmp_path / "new-session.jsonl")
        w.on_created(event)
        assert {"new-session"} == w.live_session_ids()

    def test_expired_entries_cleaned_up(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        with w._lock:
            w._last_modified["expired"] = time.time() - LIVE_THRESHOLD_S - 100
            w._last_modified["fresh"] = time.time()
        live = w.live_session_ids()
        assert {"fresh"} == live
        with w._lock:
            assert "expired" not in w._last_modified


class TestSessionWatcherStartStop:
    def test_start_without_directory_is_noop(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path / "nonexistent")
        w.start()
        assert w._observer is None

    def test_stop_without_start_is_safe(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        w.stop()

    def test_start_and_stop(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        w.start()
        assert w._observer is not None
        w.stop()
        assert w._observer is None

    def test_multiple_stops_safe(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        w.start()
        w.stop()
        w.stop()


class TestSessionWatcherCache:
    def test_get_sessions_calls_scan_on_first_call(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        sentinel: list[object] = []
        result = w.get_sessions(lambda _: sentinel)
        assert result is sentinel

    def test_get_sessions_returns_cached_on_second_call(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        call_count = 0

        def counting_scan(_: object) -> list[object]:
            nonlocal call_count
            call_count += 1
            return []

        w.get_sessions(counting_scan)
        w.get_sessions(counting_scan)
        assert 1 == call_count

    def test_on_modified_invalidates_cache(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        call_count = 0

        def counting_scan(_: object) -> list[object]:
            nonlocal call_count
            call_count += 1
            return []

        w.get_sessions(counting_scan)

        event = MagicMock()
        event.is_directory = False
        event.src_path = str(tmp_path / "sess.jsonl")
        w.on_modified(event)

        w.get_sessions(counting_scan)
        assert 2 == call_count

    def test_on_created_invalidates_cache(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        w.get_sessions(lambda _: [])

        event = MagicMock()
        event.is_directory = False
        event.src_path = str(tmp_path / "new.jsonl")
        w.on_created(event)

        assert w._cached_sessions is None

    def test_on_deleted_invalidates_cache(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        w.get_sessions(lambda _: [])

        event = MagicMock()
        event.is_directory = False
        event.src_path = str(tmp_path / "removed.jsonl")
        w.on_deleted(event)

        assert w._cached_sessions is None

    def test_on_deleted_ignores_non_jsonl(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        w.get_sessions(lambda _: [])

        event = MagicMock()
        event.is_directory = False
        event.src_path = str(tmp_path / "notes.txt")
        w.on_deleted(event)

        assert w._cached_sessions is not None

    def test_on_deleted_ignores_directories(self, tmp_path: Path) -> None:
        w = SessionWatcher(tmp_path)
        w.get_sessions(lambda _: [])

        event = MagicMock()
        event.is_directory = True
        event.src_path = str(tmp_path / "somedir")
        w.on_deleted(event)

        assert w._cached_sessions is not None
