"""Test project isolation — verify no cross-project path leakage.

Step 5 of console_path_scoping consultation (March 2, 2026):
Create 2 temp projects, verify writes go to the correct project directory.
"""

import json
from unittest.mock import patch

import pytest

from recoil.core.paths import ProjectPaths
import recoil.core.paths as _core_paths


@pytest.fixture
def two_projects(tmp_path):
    """Create two temp projects with bibles and v3 state dirs."""
    projects_root = tmp_path / "projects"
    projects_root.mkdir(parents=True)
    # projects_root() requires a .recoil-data-root sentinel.
    (projects_root / ".recoil-data-root").write_text("recoil-data-root\n")

    for name in ("alpha", "beta"):
        p = projects_root / name
        # v3 per-project state lives under _pipeline/state/visual/.
        state = p / "_pipeline" / "state" / "visual"
        state.mkdir(parents=True)
        (state / "plans").mkdir()

        bible = {
            "characters": {f"{name}_CHAR": {"visual_description": f"{name} character"}},
            "locations": {f"{name}_LOC": {"description": f"{name} location"}},
            "props": {f"{name}_PROP": {"description": f"{name} prop"}},
        }
        (state / "global_bible.json").write_text(json.dumps(bible))

        # ProjectPaths.for_project() requires the project to exist on disk;
        # project_config.json marks it (and unfreezes the v2/v3 layout).
        (p / "project_config.json").write_text(json.dumps({}))

        # v3 asset taxonomy: assets/{char,loc,prop}/
        (p / "assets" / "char").mkdir(parents=True)
        (p / "assets" / "loc").mkdir(parents=True)
        (p / "assets" / "prop").mkdir(parents=True)
        (p / "prep").mkdir(parents=True)
        (p / "renders").mkdir(parents=True)

    return projects_root


class TestProjectOutputDir:
    """v3 output dirs (prep_dir/renders_dir) resolve to the correct project."""

    def test_different_projects_get_different_dirs(self, two_projects, monkeypatch):
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        alpha_out = ProjectPaths.for_project("alpha").prep_dir
        beta_out = ProjectPaths.for_project("beta").prep_dir

        assert alpha_out != beta_out
        assert "alpha" in str(alpha_out)
        assert "beta" in str(beta_out)

    def test_output_dir_is_inside_project(self, two_projects, monkeypatch):
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        paths = ProjectPaths.for_project("alpha")
        assert str(paths.prep_dir).startswith(str(two_projects / "alpha"))
        assert str(paths.renders_dir).startswith(str(two_projects / "alpha"))


class TestProjectRefsDir:
    """v3 asset dirs (assets/{char,...}) resolve to project-specific refs."""

    def test_refs_isolated_per_project(self, two_projects, monkeypatch):
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        alpha_refs = ProjectPaths.for_project("alpha").asset_class_dir("char")
        beta_refs = ProjectPaths.for_project("beta").asset_class_dir("char")

        assert alpha_refs != beta_refs
        assert "alpha" in str(alpha_refs)
        assert "beta" in str(beta_refs)

    def test_refs_dir_under_project_output(self, two_projects, monkeypatch):
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        # v3: assets/ is a sibling of prep/renders under the project root.
        refs = ProjectPaths.for_project("alpha").assets_dir
        assert str(refs).startswith(str(two_projects / "alpha"))


@pytest.mark.xfail(
    strict=True,
    reason="REC-155: review_server._paths_for_project calls the deleted "
    "project_output_dir() and raises DeprecatedPathAPIError. The deprecated "
    "Production Console (8430) path SSOT is broken. Un-xfail when REC-155 lands.",
)
class TestPathsForProject:
    """_paths_for_project() returns complete, isolated path set."""

    def test_all_paths_scoped_to_project(self, two_projects, monkeypatch):
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        # Import inside patch so it picks up the patched value
        import importlib
        import editors.review_server as rs
        importlib.reload(rs)

        pp = rs._paths_for_project("alpha")

        # Every path should contain "alpha"
        for key in ("output_dir", "frames_dir", "previs_dir", "refs_dir",
                    "character_refs_dir", "location_refs_dir", "plans_dir",
                    "bible_path", "casting_state_path", "state_dir"):
            assert "alpha" in str(pp[key]), f"{key} not scoped to alpha: {pp[key]}"
            assert "beta" not in str(pp[key]), f"{key} leaks beta: {pp[key]}"

    def test_different_projects_never_overlap(self, two_projects, monkeypatch):
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        import importlib
        import editors.review_server as rs
        importlib.reload(rs)

        alpha_pp = rs._paths_for_project("alpha")
        beta_pp = rs._paths_for_project("beta")

        for key in ("output_dir", "frames_dir", "refs_dir", "plans_dir"):
            assert str(alpha_pp[key]) != str(beta_pp[key]), \
                f"{key} overlaps between projects"


class TestNoEngineOutputLeakage:
    """Verify no code writes refs to starsend/output/refs/ (the engine dir)."""

    def test_previz_context_uses_project_refs(self, two_projects, monkeypatch):
        """previz_context prop/expression refs resolve to project dir."""
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        with patch.object(_core_paths, "DEFAULT_PROJECT", "alpha"):
            refs = ProjectPaths.for_project("alpha").assets_dir
            assert "starsend/output/refs" not in str(refs)
            assert str(two_projects / "alpha" / "assets") == str(refs)

    def test_asset_manager_uses_project_refs(self, two_projects, monkeypatch):
        """AssetManager.get_prop_ref resolves to project dir."""
        monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(two_projects))
        prop_dir = ProjectPaths.for_project("beta").asset_subject_dir("prop", "sword")
        assert "starsend/output/refs" not in str(prop_dir)
        assert "beta" in str(prop_dir)


@pytest.mark.parametrize("ns", ["visual", "custom_ns"])
def test_state_namespace_resolution(ns, monkeypatch, tmp_path):
    """Verify STATE_NAMESPACE controls state-directory path resolution."""
    monkeypatch.setattr("recoil.core.paths.STATE_NAMESPACE", ns)
    project_dir = tmp_path / "test_project"
    state_dir = project_dir / "state" / ns
    state_dir.mkdir(parents=True)

    # Verify paths constructed with the monkeypatched STATE_NAMESPACE
    # resolve to the expected directory.
    from recoil.core.paths import STATE_NAMESPACE as imported_ns
    expected = project_dir / "state" / imported_ns
    assert state_dir == expected, (
        f"STATE_NAMESPACE monkeypatch failed: "
        f"state_dir={state_dir}, expected={expected}"
    )
    assert state_dir.exists()
