"""Tests for fs_watcher.events — FsEvent dataclass and from_watchdog parser."""

from __future__ import annotations

import json
from types import SimpleNamespace

import pytest

from recoil.pipeline._lib.fs_watcher.events import FsEvent, FsEventType


def test_fs_event_type_enum_values():
    """FsEventType has exactly the six canonical values."""
    assert FsEventType.CREATED.value == "created"
    assert FsEventType.MODIFIED.value == "modified"
    assert FsEventType.DELETED.value == "deleted"
    assert FsEventType.MOVED.value == "moved"
    assert FsEventType.INGESTED.value == "ingested"
    assert FsEventType.HEARTBEAT.value == "heartbeat"


def test_fs_event_is_frozen():
    """FsEvent is a frozen dataclass — fields cannot be mutated after construction."""
    ev = FsEvent(
        event_id="evt_1",
        event_type=FsEventType.MODIFIED,
        path="projects/foo/bar.txt",
        project="foo",
        asset_type=None,
        asset_id=None,
        src_path=None,
        is_directory=False,
        size_bytes=100,
        sha256=None,
        mtime=1700000000.0,
        ts=1700000001.0,
    )
    with pytest.raises(Exception):  # dataclass.FrozenInstanceError
        ev.path = "projects/foo/other.txt"


def test_fs_event_to_json_round_trip():
    """to_json produces valid JSON with the enum as its string value."""
    ev = FsEvent(
        event_id="evt_1",
        event_type=FsEventType.CREATED,
        path="projects/tartarus/output/refs/_canonical/characters/sadie/hero.jpg",
        project="tartarus",
        asset_type="character",
        asset_id="sadie",
        src_path=None,
        is_directory=False,
        size_bytes=734358,
        sha256=None,
        mtime=1744024473.12,
        ts=1744024473.34,
    )
    s = ev.to_json()
    parsed = json.loads(s)
    assert parsed["event_id"] == "evt_1"
    assert parsed["event_type"] == "created"  # enum serialized as string
    assert parsed["project"] == "tartarus"
    assert parsed["asset_type"] == "character"
    assert parsed["asset_id"] == "sadie"
    assert parsed["is_directory"] is False


def test_from_watchdog_extracts_project_and_asset(tmp_path, monkeypatch):
    """from_watchdog parses project, asset_type, asset_id from a canonical refs path."""
    # Set up a fake projects_root() pointing at tmp_path
    projects_root = tmp_path / "projects"
    projects_root.mkdir()

    import recoil.pipeline._lib.fs_watcher.events as ev_mod
    # Monkeypatch the projects_root() symbol resolved inside from_watchdog
    import recoil.core.paths
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(projects_root))
    (projects_root / ".recoil-data-root").write_text("recoil-data-root\n")

    # Construct a path that represents a character hero file
    hero_path = projects_root / "tartarus" / "output" / "refs" / "_canonical" / "characters" / "sadie" / "hero.jpg"
    hero_path.parent.mkdir(parents=True)
    hero_path.write_bytes(b"fake image")

    # Fake watchdog event
    wd_event = SimpleNamespace(
        event_type="modified",
        src_path=str(hero_path),
        is_directory=False,
    )

    ev = FsEvent.from_watchdog(wd_event, broker_ts=1700000000.0, event_id="evt_1")
    assert ev.event_type == FsEventType.MODIFIED
    assert ev.project == "tartarus"
    assert ev.asset_type == "character"
    assert ev.asset_id == "sadie"
    assert ev.path.endswith("projects/tartarus/output/refs/_canonical/characters/sadie/hero.jpg")
    assert ev.size_bytes == len(b"fake image")
    assert ev.mtime is not None
    assert ev.sha256 is None  # lazy — never populated at emit time
    assert ev.is_directory is False


def test_from_watchdog_handles_deleted_file(tmp_path, monkeypatch):
    """from_watchdog on a deleted path returns None for size_bytes and mtime."""
    projects_root = tmp_path / "projects"
    projects_root.mkdir()

    import recoil.core.paths
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(projects_root))
    (projects_root / ".recoil-data-root").write_text("recoil-data-root\n")

    missing_path = projects_root / "tartarus" / "output" / "refs" / "_canonical" / "locations" / "desert" / "hero.jpg"
    # Intentionally do NOT create the file

    wd_event = SimpleNamespace(
        event_type="deleted",
        src_path=str(missing_path),
        is_directory=False,
    )

    ev = FsEvent.from_watchdog(wd_event, broker_ts=1700000000.0, event_id="evt_del")
    assert ev.event_type == FsEventType.DELETED
    assert ev.project == "tartarus"
    assert ev.asset_type == "location"
    assert ev.asset_id == "desert"
    assert ev.size_bytes is None
    assert ev.mtime is None


def test_from_watchdog_moved_event_captures_src_path(tmp_path, monkeypatch):
    """MOVED events populate src_path with the old location and path with the new."""
    projects_root = tmp_path / "projects"
    projects_root.mkdir()

    import recoil.core.paths
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(projects_root))
    (projects_root / ".recoil-data-root").write_text("recoil-data-root\n")

    old_path = projects_root / "tartarus" / "output" / "refs" / "_canonical" / "props" / "knife" / "hero.jpg"
    new_path = projects_root / "tartarus" / "output" / "refs" / "_canonical" / "props" / "dagger" / "hero.jpg"
    new_path.parent.mkdir(parents=True)
    new_path.write_bytes(b"fake")

    wd_event = SimpleNamespace(
        event_type="moved",
        src_path=str(old_path),
        dest_path=str(new_path),
        is_directory=False,
    )

    ev = FsEvent.from_watchdog(wd_event, broker_ts=1700000000.0, event_id="evt_mv")
    assert ev.event_type == FsEventType.MOVED
    assert "dagger" in ev.path  # destination
    assert ev.src_path is not None
    assert "knife" in ev.src_path  # source
    assert ev.asset_type == "prop"
    assert ev.asset_id == "dagger"  # destination slug wins


def test_from_watchdog_on_non_canonical_path_has_null_asset_fields():
    """Paths outside output/refs/_canonical/ don't populate asset_type/asset_id."""
    wd_event = SimpleNamespace(
        event_type="modified",
        src_path="/tmp/somewhere/random.txt",
        is_directory=False,
    )
    ev = FsEvent.from_watchdog(wd_event, broker_ts=1700000000.0, event_id="evt_x")
    assert ev.asset_type is None
    assert ev.asset_id is None
