"""Centralized config-backed mutation fixture for #714.

Mutation tests must opt into this helper explicitly. It never grants
mutation authority via environment variables alone.

Design rules:
- temporary v2 configuration with non-empty ``allowed_repositories``
- profile-scoped operations (not every mutation op for every test)
- deterministic workspace remotes (no host Git dependency)
- one canonical repository per profile by default
- isolated state + cleanup in ``finally``
"""
from __future__ import annotations

import json
import os
import tempfile
from contextlib import contextmanager
from copy import deepcopy
from typing import Iterable, Sequence
from unittest.mock import patch

PRGS_SLUG = "Scaled-Tech-Consulting/Gitea-Tools"
PRGS_URL = f"https://gitea.prgs.cc/{PRGS_SLUG}.git"
MDCPS_SLUG = "913443/eAgenda"
MDCPS_URL = f"https://gitea.dadeschools.net/{MDCPS_SLUG}.git"
EXAMPLE_SLUG = "Example-Org/Example-Repo"
EXAMPLE_URL = f"https://gitea.example.com/{EXAMPLE_SLUG}.git"
TIMESHEET_SLUG = "Scaled-Tech-Consulting/Timesheet"


def _author_ops() -> list[str]:
    return [
        "gitea.read",
        "gitea.issue.create",
        "gitea.issue.close",
        "gitea.issue.comment",
        "gitea.pr.create",
        "gitea.pr.comment",
        "gitea.branch.create",
        "gitea.branch.push",
        "gitea.repo.commit",
    ]


def _author_forbidden() -> list[str]:
    return [
        "gitea.pr.approve",
        "gitea.pr.merge",
        "gitea.pr.request_changes",
        "gitea.pr.review",
    ]


def _reviewer_ops() -> list[str]:
    return [
        "gitea.read",
        "gitea.pr.review",
        "gitea.pr.approve",
        "gitea.pr.comment",
        "gitea.issue.comment",
    ]


def _merger_ops() -> list[str]:
    return [
        "gitea.read",
        "gitea.pr.merge",
        "gitea.pr.comment",
    ]


def _profile(
    *,
    name: str,
    context: str,
    role: str,
    base_url: str,
    auth_env: str,
    allowed_operations: Sequence[str],
    forbidden_operations: Sequence[str],
    allowed_repositories: Sequence[str],
    username: str | None = None,
) -> dict:
    body = {
        "enabled": True,
        "context": context,
        "role": role,
        "base_url": base_url,
        "auth": {"type": "env", "name": auth_env},
        "allowed_operations": list(allowed_operations),
        "forbidden_operations": list(forbidden_operations),
        "allowed_repositories": list(allowed_repositories),
        "execution_profile": name,
    }
    if username:
        body["username"] = username
    return body


def build_dual_remote_config(
    *,
    allowed_operations: Iterable[str] | None = None,
    allowed_repositories_prgs: Sequence[str] | None = None,
    allowed_repositories_mdcps: Sequence[str] | None = None,
    extra_profiles: dict | None = None,
    include_example_repo: bool = False,
) -> dict:
    """Build a minimal dual-host v2 config.

    Each profile authorizes only its host-canonical repository by default.
    ``include_example_repo`` adds Example-Org/Example-Repo when a test patches
    REMOTES to that synthetic unit-test identity.
    """
    ops = list(allowed_operations or _author_ops())
    forb = _author_forbidden()
    prgs_repos = list(allowed_repositories_prgs or [PRGS_SLUG])
    mdcps_repos = list(allowed_repositories_mdcps or [MDCPS_SLUG])
    if include_example_repo:
        for repos in (prgs_repos, mdcps_repos):
            if EXAMPLE_SLUG not in repos:
                repos.append(EXAMPLE_SLUG)

    profiles = {
        "test-author-prgs": _profile(
            name="test-author-prgs",
            context="prgs",
            role="author",
            base_url="https://gitea.prgs.cc",
            auth_env="GITEA_TOKEN_TEST",
            allowed_operations=ops,
            forbidden_operations=forb,
            allowed_repositories=prgs_repos,
        ),
        "test-author-dadeschools": _profile(
            name="test-author-dadeschools",
            context="mdcps",
            role="author",
            base_url="https://gitea.dadeschools.net",
            auth_env="GITEA_TOKEN_TEST",
            allowed_operations=ops,
            forbidden_operations=forb,
            allowed_repositories=mdcps_repos,
        ),
        "test-reviewer-prgs": _profile(
            name="test-reviewer-prgs",
            context="prgs",
            role="reviewer",
            base_url="https://gitea.prgs.cc",
            auth_env="GITEA_TOKEN_TEST",
            allowed_operations=_reviewer_ops(),
            forbidden_operations=[
                "gitea.pr.create",
                "gitea.branch.push",
                "gitea.pr.merge",
                "gitea.issue.create",
            ],
            allowed_repositories=prgs_repos,
        ),
        "test-merger-prgs": _profile(
            name="test-merger-prgs",
            context="prgs",
            role="merger",
            base_url="https://gitea.prgs.cc",
            auth_env="GITEA_TOKEN_TEST",
            allowed_operations=_merger_ops(),
            forbidden_operations=[
                "gitea.pr.create",
                "gitea.branch.push",
                "gitea.pr.approve",
                "gitea.issue.create",
            ],
            allowed_repositories=prgs_repos,
        ),
    }
    if extra_profiles:
        profiles.update(deepcopy(extra_profiles))

    # Legacy aliases used by older tests — same host scope as the base profile.
    for alias, base in (
        ("gitea-author", "test-author-prgs"),
        ("author-test", "test-author-dadeschools"),
        ("author", "test-author-dadeschools"),
        ("full-author", "test-author-dadeschools"),
        ("test-author", "test-author-dadeschools"),
        ("prgs-author", "test-author-prgs"),
        ("gitea-reviewer", "test-reviewer-prgs"),
        ("prgs-reviewer", "test-reviewer-prgs"),
        ("gitea-merger", "test-merger-prgs"),
        ("prgs-merger", "test-merger-prgs"),
    ):
        if alias not in profiles and base in profiles:
            clone = deepcopy(profiles[base])
            clone["execution_profile"] = alias
            profiles[alias] = clone

    return {
        "version": 2,
        "rules": {"allow_runtime_switching": True},
        "contexts": {
            "prgs": {
                "enabled": True,
                "gitea": {"enabled": True, "base_url": "https://gitea.prgs.cc"},
            },
            "mdcps": {
                "enabled": True,
                "gitea": {
                    "enabled": True,
                    "base_url": "https://gitea.dadeschools.net",
                },
            },
        },
        "profiles": profiles,
    }


def build_v2_config(**kwargs):
    """Back-compat alias."""
    return build_dual_remote_config(
        allowed_operations=kwargs.get("allowed_operations"),
        extra_profiles=kwargs.get("extra_profiles"),
        include_example_repo=bool(kwargs.get("include_example_repo")),
    )


def inject_allowed_repositories(
    config: dict,
    repos: Sequence[str],
    *,
    profiles: Sequence[str] | None = None,
) -> dict:
    """Return a deep copy of *config* with allowlists set on selected profiles."""
    out = deepcopy(config)
    targets = set(profiles) if profiles is not None else set(out.get("profiles") or {})
    for name, prof in (out.get("profiles") or {}).items():
        if name in targets:
            prof["allowed_repositories"] = list(repos)
    return out


def ensure_all_profiles_have_scope(
    config: dict,
    default_repos: Sequence[str],
) -> dict:
    """Add non-empty allowed_repositories to every profile that lacks one."""
    out = deepcopy(config)
    for _name, prof in (out.get("profiles") or {}).items():
        raw = prof.get("allowed_repositories")
        if not isinstance(raw, (list, tuple)) or not raw:
            prof["allowed_repositories"] = list(default_repos)
    return out


@contextmanager
def mutation_profile_env(
    *,
    profile_name: str = "test-author-dadeschools",
    allowed_operations: Iterable[str] | None = None,
    allowed_repositories: Sequence[str] | None = None,
    workspace_urls: dict | None = None,
    clear_env: bool = False,
    extra_env: dict | None = None,
    extra_profiles: dict | None = None,
    include_example_repo: bool = False,
    config: dict | None = None,
):
    """Context manager: config-backed mutation authority + deterministic remotes."""
    import gitea_config
    import gitea_mcp_server as srv
    import session_context_binding as session_ctx

    if config is None:
        prgs_repos = None
        mdcps_repos = None
        if allowed_repositories is not None:
            # Caller-specified single allowlist applied to both host profiles.
            prgs_repos = list(allowed_repositories)
            mdcps_repos = list(allowed_repositories)
        cfg = build_dual_remote_config(
            allowed_operations=allowed_operations,
            allowed_repositories_prgs=prgs_repos,
            allowed_repositories_mdcps=mdcps_repos,
            extra_profiles=extra_profiles,
            include_example_repo=include_example_repo,
        )
    else:
        cfg = deepcopy(config)
        if allowed_repositories is not None:
            cfg = ensure_all_profiles_have_scope(cfg, list(allowed_repositories))

    urls = (
        {"prgs": PRGS_URL, "dadeschools": MDCPS_URL}
        if workspace_urls is None
        else dict(workspace_urls)
    )

    tmp = tempfile.TemporaryDirectory(prefix="gitea-mut-cfg-")
    try:
        cfg_path = os.path.join(tmp.name, "profiles.json")
        with open(cfg_path, "w", encoding="utf-8") as fh:
            json.dump(cfg, fh)
        env = {
            "GITEA_MCP_CONFIG": cfg_path,
            "GITEA_MCP_PROFILE": profile_name,
            "GITEA_TOKEN_TEST": "test-token",
            "PYTEST_CURRENT_TEST": os.environ.get(
                "PYTEST_CURRENT_TEST", "mutation_profile_env"
            ),
        }
        if extra_env:
            env.update(extra_env)

        def _remote_url(name: str):
            if name in urls:
                return urls[name]
            # Prefer live REMOTES (tests often patch Example-Org).
            try:
                entry = srv.REMOTES.get(name) or {}
                host = entry.get("host")
                org = entry.get("org")
                repo = entry.get("repo")
                if host and org and repo:
                    if (
                        name == "prgs"
                        and org == "Scaled-Tech-Consulting"
                        and repo == "Timesheet"
                    ):
                        return PRGS_URL
                    if name == "dadeschools" and repo == "Timesheet":
                        return MDCPS_URL
                    return f"https://{host}/{org}/{repo}.git"
            except Exception:
                pass
            return None

        gitea_config._active_profile_override = None
        try:
            session_ctx._reset_session_context_for_testing()
        except Exception:
            pass

        payload = {
            "env": env,
            "config_path": cfg_path,
            "profile_name": profile_name,
            "urls": urls,
            "config": cfg,
        }
        with patch.dict(os.environ, env, clear=clear_env):
            os.environ.setdefault(
                "PYTEST_CURRENT_TEST",
                env.get("PYTEST_CURRENT_TEST", "mutation_profile_env"),
            )
            with patch.object(srv, "_local_git_remote_url", side_effect=_remote_url):
                mcp_srv = None
                try:
                    import mcp_server as mcp_srv  # type: ignore
                except Exception:
                    mcp_srv = None
                if mcp_srv is not None:
                    with patch.object(
                        mcp_srv, "_local_git_remote_url", side_effect=_remote_url
                    ):
                        yield payload
                else:
                    yield payload
    finally:
        try:
            session_ctx._reset_session_context_for_testing()
        except Exception:
            pass
        gitea_config._active_profile_override = None
        tmp.cleanup()


# Process-lifetime shared config for mass-migrating env-only mutation tests.
_SHARED_CFG_PATH = None
_SHARED_CFG_DIR = None
_SHARED_CFG_WITH_EXAMPLE_PATH = None


def shared_mutation_config_path(*, include_example_repo: bool = False) -> str:
    """Write (once) a dual-remote mutation config and return its path."""
    global _SHARED_CFG_PATH, _SHARED_CFG_DIR, _SHARED_CFG_WITH_EXAMPLE_PATH
    import atexit
    import shutil

    if include_example_repo:
        if _SHARED_CFG_WITH_EXAMPLE_PATH and os.path.isfile(
            _SHARED_CFG_WITH_EXAMPLE_PATH
        ):
            return _SHARED_CFG_WITH_EXAMPLE_PATH
        if _SHARED_CFG_DIR is None:
            _SHARED_CFG_DIR = tempfile.mkdtemp(prefix="gitea-shared-mut-cfg-")
            atexit.register(lambda: shutil.rmtree(_SHARED_CFG_DIR, ignore_errors=True))
        path = os.path.join(_SHARED_CFG_DIR, "profiles-with-example.json")
        with open(path, "w", encoding="utf-8") as fh:
            json.dump(build_dual_remote_config(include_example_repo=True), fh)
        _SHARED_CFG_WITH_EXAMPLE_PATH = path
        return path

    if _SHARED_CFG_PATH and os.path.isfile(_SHARED_CFG_PATH):
        return _SHARED_CFG_PATH
    if _SHARED_CFG_DIR is None:
        _SHARED_CFG_DIR = tempfile.mkdtemp(prefix="gitea-shared-mut-cfg-")
        atexit.register(lambda: shutil.rmtree(_SHARED_CFG_DIR, ignore_errors=True))
    _SHARED_CFG_PATH = os.path.join(_SHARED_CFG_DIR, "profiles.json")
    with open(_SHARED_CFG_PATH, "w", encoding="utf-8") as fh:
        json.dump(build_dual_remote_config(), fh)
    return _SHARED_CFG_PATH


def shared_mutation_env(
    profile_name: str = "test-author-dadeschools",
    *,
    include_example_repo: bool = False,
    **extra,
) -> dict:
    """Env dict for mutation tests: config-backed + optional extras.

    Always carries ``PYTEST_CURRENT_TEST`` so ``patch.dict(..., clear=True)``
    does not strip the pytest marker and trip the #695 native-transport wall.
    """
    env = {
        "GITEA_MCP_CONFIG": shared_mutation_config_path(
            include_example_repo=include_example_repo
        ),
        "GITEA_MCP_PROFILE": profile_name,
        "GITEA_TOKEN_TEST": "test-token",
        "PYTEST_CURRENT_TEST": os.environ.get(
            "PYTEST_CURRENT_TEST", "mutation_profile_env"
        ),
    }
    env.update(extra)
    # Callers must not be able to drop the pytest marker by accident.
    env.setdefault(
        "PYTEST_CURRENT_TEST",
        os.environ.get("PYTEST_CURRENT_TEST", "mutation_profile_env"),
    )
    return env


def install_deterministic_remote_urls() -> None:
    """Patch server modules so workspace remotes are deterministic.

    Prefer live ``REMOTES`` entries (so tests that patch Example-Org keep
    workspace alignment). Map the historical prgs→Timesheet default to
    Gitea-Tools so session binding matches the control repository under test.
    """
    import gitea_mcp_server as srv

    def _url(name: str):
        try:
            entry = srv.REMOTES.get(name) or {}
            host = entry.get("host")
            org = entry.get("org")
            repo = entry.get("repo")
            if host and org and repo:
                if (
                    name == "prgs"
                    and org == "Scaled-Tech-Consulting"
                    and repo == "Timesheet"
                ):
                    return PRGS_URL
                if name == "dadeschools" and repo == "Timesheet":
                    # Prefer mdcps eAgenda canonical for dual-config tests.
                    return MDCPS_URL
                return f"https://{host}/{org}/{repo}.git"
        except Exception:
            pass
        if name == "prgs":
            return PRGS_URL
        if name == "dadeschools":
            return MDCPS_URL
        return None

    srv._local_git_remote_url = _url  # type: ignore[method-assign]
    try:
        import mcp_server as mcp_srv

        mcp_srv._local_git_remote_url = _url  # type: ignore[method-assign]
    except Exception:
        pass
