from pathlib import Path

from watchfiles import Change

import asyncio
import pytest

from src.config.file_watcher import FileChange, FileWatcher

from typing import Sequence


@pytest.mark.asyncio
async def test_dispatch_changes_with_path_and_change_type_filters(tmp_path: Path):
    watcher = FileWatcher(paths=[tmp_path])
    target_file = (tmp_path / "bot_config.toml").resolve()

    received: list[list[FileChange]] = []

    async def callback(changes):
        received.append(list(changes))

    watcher.subscribe(callback, paths=[target_file], change_types=[Change.modified])

    await watcher._dispatch_changes(
        [
            FileChange(change_type=Change.added, path=target_file),
            FileChange(change_type=Change.modified, path=target_file),
            FileChange(change_type=Change.modified, path=(tmp_path / "other.toml").resolve()),
        ]
    )

    assert len(received) == 1
    assert len(received[0]) == 1
    assert received[0][0].change_type == Change.modified
    assert received[0][0].path == target_file


@pytest.mark.asyncio
async def test_sync_callback_supported(tmp_path: Path):
    watcher = FileWatcher(paths=[tmp_path])
    target_file = (tmp_path / "model_config.toml").resolve()

    received_paths: list[Path] = []

    def sync_callback(changes):
        received_paths.extend(change.path for change in changes)

    watcher.subscribe(sync_callback, paths=[target_file])

    await watcher._dispatch_changes([FileChange(change_type=Change.modified, path=target_file)])

    assert received_paths == [target_file]


@pytest.mark.asyncio
async def test_callback_timeout_and_cooldown(tmp_path: Path):
    watcher = FileWatcher(
        paths=[tmp_path],
        callback_timeout_s=0.05,
        callback_failure_threshold=2,
        callback_cooldown_s=0.2,
    )
    target_file = (tmp_path / "bot_config.toml").resolve()

    async def slow_callback(changes):
        await asyncio.sleep(0.2)

    watcher.subscribe(slow_callback, paths=[target_file])

    await watcher._dispatch_changes([FileChange(change_type=Change.modified, path=target_file)])
    await watcher._dispatch_changes([FileChange(change_type=Change.modified, path=target_file)])

    stats_after_failures = watcher.stats
    assert stats_after_failures.callbacks_timed_out == 2
    assert stats_after_failures.callbacks_failed == 2

    await watcher._dispatch_changes([FileChange(change_type=Change.modified, path=target_file)])
    stats_after_cooldown_skip = watcher.stats
    assert stats_after_cooldown_skip.callbacks_skipped_cooldown >= 1


@pytest.mark.asyncio
async def test_start_requires_subscription(tmp_path: Path):
    watcher = FileWatcher(paths=[tmp_path])

    with pytest.raises(RuntimeError):
        await watcher.start()


@pytest.mark.asyncio
async def test_unsubscribe_stops_dispatch(tmp_path: Path):
    watcher = FileWatcher(paths=[tmp_path])
    target_file = (tmp_path / "bot_config.toml").resolve()

    calls = 0

    async def callback(changes):
        nonlocal calls
        calls += 1

    subscription_id = watcher.subscribe(callback, paths=[target_file])
    assert watcher.unsubscribe(subscription_id) is True

    await watcher._dispatch_changes([FileChange(change_type=Change.modified, path=target_file)])

    assert calls == 0


@pytest.mark.asyncio
async def test_add_callback_while_watcher_running(tmp_path: Path):
    dirs = (tmp_path / "a_dir").resolve()
    dirs.mkdir(exist_ok=True)
    file = (dirs / "a.toml").resolve()
    file.touch()
    watcher = FileWatcher(paths=[dirs], debounce_ms=200)

    calls = 0

    async def callback(changes: Sequence[FileChange]):
        nonlocal calls
        print(f"Callback called with changes: {[f'{change.change_type} {change.path}' for change in changes]}")
        calls += 1

    uuid = watcher.subscribe(callback, paths=[file])
    await watcher.start()
    try:
        with file.open("w") as f:
            f.write("change")
        await asyncio.sleep(0.5)
        assert calls == 1
        watcher.unsubscribe(uuid)
        with file.open("w") as f:
            f.write("change2")
        await asyncio.sleep(0.5)
        assert calls == 1
    finally:
        await watcher.stop()
