"""Tests for recoil/core/project.py.

Covers: enum values, missing-config default, valid-config parse, unknown-mode
ValueError, all 7 capability predicates per mode, multi-instance independence
(no caching).
"""

import json
import sys
import warnings
from pathlib import Path

import pytest


# ── Path setup ──────────────────────────────────────────────────
_RECOIL_ROOT = Path(__file__).resolve().parent.parent
if str(_RECOIL_ROOT) not in sys.path:
    sys.path.insert(0, str(_RECOIL_ROOT))


# ── Fixtures ────────────────────────────────────────────────────


@pytest.fixture
def tmp_projects_root(tmp_path, monkeypatch):
    """Patch projects-root via env var for isolated project_config tests.

    Post-Phase-22: Project + project.py paths all flow through
    `projects_root()` (the typed-raise SSOT), which honors RECOIL_PROJECTS_ROOT
    at call time. setenv replaces the legacy `setattr(paths, "projects_root()", ...)`
    pattern.
    """
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(tmp_path))
    # REC-101 guard: paths.py requires a `.recoil-data-root` sentinel to confirm
    # the resolved root is real data (not a Dropbox/Smart-Sync stub) before use.
    (tmp_path / ".recoil-data-root").write_text("recoil-data-root\n")
    return tmp_path


def _write_config(projects_root: Path, name: str, config: dict | None) -> None:
    """Helper: create projects_root/{name}/project_config.json (or skip)."""
    proj_dir = projects_root / name
    proj_dir.mkdir(parents=True, exist_ok=True)
    if config is not None:
        (proj_dir / "project_config.json").write_text(json.dumps(config))


# ── ProjectMode enum ────────────────────────────────────────────


class TestProjectMode:
    def test_enum_values(self):
        from recoil.core.project import ProjectMode

        assert ProjectMode.MICRODRAMA.value == "microdrama"
        assert ProjectMode.CLIENT_DELIVERABLE.value == "client_deliverable"

    def test_str_enum_equality(self):
        from recoil.core.project import ProjectMode

        # str, Enum — equal to raw strings
        assert ProjectMode.MICRODRAMA == "microdrama"
        assert ProjectMode("microdrama") == ProjectMode.MICRODRAMA


# ── Project construction ────────────────────────────────────────


class TestProjectConstruction:
    def test_missing_config_defaults_to_microdrama(self, tmp_projects_root):
        from recoil.core.project import Project, ProjectMode

        _write_config(tmp_projects_root, "tartarus", None)

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            p = Project("tartarus")

        assert p.mode == ProjectMode.MICRODRAMA
        # Must emit a UserWarning about defaulting
        assert any("defaulting to microdrama" in str(warn.message) for warn in w)

    def test_valid_microdrama_config(self, tmp_projects_root):
        from recoil.core.project import Project, ProjectMode

        _write_config(tmp_projects_root, "tartarus", {"mode": "microdrama"})
        p = Project("tartarus")
        assert p.mode == ProjectMode.MICRODRAMA

    def test_valid_client_deliverable_config(self, tmp_projects_root):
        from recoil.core.project import Project, ProjectMode

        _write_config(tmp_projects_root, "driver-beware", {"mode": "client_deliverable"})
        p = Project("driver-beware")
        assert p.mode == ProjectMode.CLIENT_DELIVERABLE

    def test_unknown_mode_raises(self, tmp_projects_root):
        from recoil.core.project import Project

        _write_config(tmp_projects_root, "weird", {"mode": "experimental"})
        with pytest.raises(ValueError, match="Unknown project_type 'experimental'"):
            Project("weird")

    def test_malformed_json_raises(self, tmp_projects_root):
        from recoil.core.project import Project

        proj_dir = tmp_projects_root / "broken"
        proj_dir.mkdir()
        (proj_dir / "project_config.json").write_text("{not valid json")
        with pytest.raises(json.JSONDecodeError):
            Project("broken")


# ── Capability predicates ───────────────────────────────────────


class TestMicrodramaCapabilities:
    """Tartarus-style microdrama project — all predicates True except none."""

    @pytest.fixture
    def project(self, tmp_projects_root):
        from recoil.core.project import Project

        _write_config(tmp_projects_root, "tartarus", {"mode": "microdrama"})
        return Project("tartarus")

    def test_uses_pass_naming(self, project):
        assert project.uses_pass_naming is True

    def test_sweeps_orphans(self, project):
        assert project.sweeps_orphans is True

    def test_uses_pass_store(self, project):
        assert project.uses_pass_store is True

    def test_auto_extracts_segments(self, project):
        assert project.auto_extracts_segments is True

    def test_captures_verdicts(self, project):
        assert project.captures_verdicts is True

    def test_has_take_semantics(self, project):
        assert project.has_take_semantics is True

    def test_ui_grouping_strategy(self, project):
        assert project.ui_grouping_strategy == "pass_anchors"


class TestClientDeliverableCapabilities:
    """Driver-beware-style client project — pass infra disabled, verdicts on."""

    @pytest.fixture
    def project(self, tmp_projects_root):
        from recoil.core.project import Project

        _write_config(tmp_projects_root, "driver-beware", {"mode": "client_deliverable"})
        return Project("driver-beware")

    def test_uses_pass_naming(self, project):
        assert project.uses_pass_naming is False

    def test_sweeps_orphans(self, project):
        assert project.sweeps_orphans is False

    def test_uses_pass_store(self, project):
        assert project.uses_pass_store is False

    def test_auto_extracts_segments(self, project):
        assert project.auto_extracts_segments is False

    def test_captures_verdicts(self, project):
        # Client mode still captures verdicts
        assert project.captures_verdicts is True

    def test_has_take_semantics(self, project):
        assert project.has_take_semantics is False

    def test_ui_grouping_strategy(self, project):
        assert project.ui_grouping_strategy == "flat"


# ── Factory + isolation ─────────────────────────────────────────


class TestGetProjectFactory:
    def test_get_project_returns_cached_instance(self, tmp_projects_root):
        """Per BUILD_SPEC Phase 2: get_project is lru_cache(maxsize=64).

        Repeated calls for the same slug return the same instance until
        cache_clear() is called.
        """
        from recoil.core.project import get_project

        get_project.cache_clear()
        _write_config(tmp_projects_root, "tartarus", {"mode": "microdrama"})
        p1 = get_project("tartarus")
        p2 = get_project("tartarus")
        # Same instance — cache hit on second call.
        assert p1 is p2
        assert p1.mode == p2.mode

    def test_config_change_reflected_after_cache_clear(self, tmp_projects_root):
        """Per BUILD_SPEC Phase 2: lru_cache holds the Project instance until
        explicit cache_clear(). Long-running daemons that edit config on disk
        must call get_project.cache_clear() to see the change.
        """
        from recoil.core.project import get_project, ProjectMode

        get_project.cache_clear()
        _write_config(tmp_projects_root, "x", {"mode": "microdrama"})
        p1 = get_project("x")
        assert p1.mode == ProjectMode.MICRODRAMA

        # Edit config on disk; without cache_clear, the cached instance
        # is returned — the new mode is NOT seen.
        _write_config(tmp_projects_root, "x", {"mode": "client_deliverable"})
        cached = get_project("x")
        assert cached is p1
        assert cached.mode == ProjectMode.MICRODRAMA  # stale, by design

        # After cache_clear, the next call rebuilds from disk.
        get_project.cache_clear()
        p2 = get_project("x")
        assert p2.mode == ProjectMode.CLIENT_DELIVERABLE

    def test_two_projects_independent(self, tmp_projects_root):
        from recoil.core.project import get_project, ProjectMode

        _write_config(tmp_projects_root, "tartarus", {"mode": "microdrama"})
        _write_config(tmp_projects_root, "driver-beware", {"mode": "client_deliverable"})

        t = get_project("tartarus")
        d = get_project("driver-beware")
        assert t.mode == ProjectMode.MICRODRAMA
        assert d.mode == ProjectMode.CLIENT_DELIVERABLE
        assert t.uses_pass_naming != d.uses_pass_naming
