import asyncio
import sys
import pytest
from unittest.mock import patch
from asgi_autoreload.broadcaster import Broadcaster
from asgi_autoreload.watcher import FileWatcher


async def _fake_awatch(*paths, stop_event, **kwargs):
    await asyncio.sleep(0.01)
    yield {("modified", "/fake/file.py")}
    await stop_event.wait()


async def test_watcher_broadcasts_on_change():
    b = Broadcaster()
    q = await b.subscribe()
    w = FileWatcher(["."], b)

    with patch("asgi_autoreload.watcher.awatch", _fake_awatch):
        w.start()
        await asyncio.sleep(0.05)
        await w.stop()

    assert not q.empty()
    assert q.get_nowait() == "reload"


async def test_watcher_stop_before_change():
    b = Broadcaster()
    q = await b.subscribe()

    async def slow_awatch(*paths, stop_event, **kwargs):
        await stop_event.wait()
        return
        yield  # make it an async generator

    w = FileWatcher(["."], b)
    with patch("asgi_autoreload.watcher.awatch", slow_awatch):
        w.start()
        await w.stop()

    assert q.empty()


async def test_build_command_runs_on_startup():
    b = Broadcaster()
    ran = []

    async def slow_awatch(*paths, stop_event, **kwargs):
        await stop_event.wait()
        return
        yield

    w = FileWatcher(["."], b, build_command=[sys.executable, "-c", "import sys; sys.exit(0)"])
    with patch("asgi_autoreload.watcher.awatch", slow_awatch):
        w.start()
        await asyncio.sleep(0.05)
        await w.stop()
    # no exception = command ran successfully


async def test_build_command_runs_before_each_broadcast():
    b = Broadcaster()
    q = await b.subscribe()
    order: list[str] = []

    original_exec = asyncio.create_subprocess_exec

    async def fake_exec(*args, **kwargs):
        order.append("build")
        proc = await original_exec(*args, **kwargs)
        return proc

    w = FileWatcher(["."], b, build_command=[sys.executable, "-c", ""])
    with patch("asgi_autoreload.watcher.awatch", _fake_awatch), \
         patch("asyncio.create_subprocess_exec", fake_exec):
        w.start()
        await asyncio.sleep(0.05)
        await w.stop()

    # build ran on startup + on file change
    assert order.count("build") >= 2
    assert not q.empty()


async def test_excluded_path_does_not_trigger_reload(tmp_path):
    b = Broadcaster()
    q = await b.subscribe()
    excluded = tmp_path / "static"
    excluded.mkdir()

    async def fake_awatch(*paths, watch_filter, stop_event, **kwargs):
        # emit a change inside the excluded directory
        path = str(excluded / "bundle.css")
        if watch_filter(None, path):
            yield {("modified", path)}
        await stop_event.wait()

    w = FileWatcher([str(tmp_path)], b, exclude_paths=[str(excluded)])
    with patch("asgi_autoreload.watcher.awatch", fake_awatch):
        w.start()
        await asyncio.sleep(0.05)
        await w.stop()

    assert q.empty()


async def test_non_excluded_path_still_triggers_reload(tmp_path):
    b = Broadcaster()
    q = await b.subscribe()
    excluded = tmp_path / "static"
    excluded.mkdir()
    src = tmp_path / "src"
    src.mkdir()

    async def fake_awatch(*paths, watch_filter, stop_event, **kwargs):
        path = str(src / "style.css")
        if watch_filter(None, path):
            yield {("modified", path)}
        await stop_event.wait()

    w = FileWatcher([str(tmp_path)], b, exclude_paths=[str(excluded)])
    with patch("asgi_autoreload.watcher.awatch", fake_awatch):
        w.start()
        await asyncio.sleep(0.05)
        await w.stop()

    assert not q.empty()
