"""In-process fake surfaces + fixtures builders — no network, no real credentials.

These implement exactly the read+write API subset the doctor/probes/sync consume,
so the incident-replay, redaction, and reconciler contract tests run whole flows
offline (SPEC §6). The fakes are MUTABLE — ``sync --apply`` writes to them, and a
follow-up ``plan`` reads the mutation back (the idempotence proof).

AG-18 in the fakes: ``create_*_secret`` accepts the value but NEVER stores it (a
real Woodpecker secret value is write-only — a read never returns it), so a leaked
value cannot come from a fake's state.
"""

from __future__ import annotations

from pathlib import Path

from mgf.secrets.errors import SurfaceUnavailableError
from mgf.secrets.forge import DeployKey
from mgf.secrets.hosts import HostStat
from mgf.secrets.registry import Registry, load
from mgf.secrets.woodpecker import RepoInfo, SecretState


class FakeWoodpecker:
    """In-proc Woodpecker admin: org/repo secret states (events + D-7 images),
    activated repos, and the create/update writes sync exercises."""

    def __init__(
        self,
        *,
        org_secrets: dict[str, set[str]] | None = None,
        repos: list[tuple[int, str]] | None = None,
        repo_secrets: dict[int, dict[str, set[str]]] | None = None,
        org_images: dict[str, list[str]] | None = None,
        repo_images: dict[int, dict[str, list[str]]] | None = None,
    ) -> None:
        oi = org_images or {}
        self._org: dict[str, SecretState] = {
            k: SecretState(frozenset(v), tuple(oi.get(k, ())))
            for k, v in (org_secrets or {}).items()
        }
        self._repos = repos or []
        ri = repo_images or {}
        self._repo_secrets: dict[int, dict[str, SecretState]] = {
            rid: {
                k: SecretState(frozenset(v), tuple(ri.get(rid, {}).get(k, ())))
                for k, v in secs.items()
            }
            for rid, secs in (repo_secrets or {}).items()
        }

    # -- reads --------------------------------------------------------------- #

    def org_id(self) -> int:
        return 1

    def org_secrets(self) -> dict[str, frozenset[str]]:
        return {k: v.events for k, v in self._org.items()}

    def repo_secrets(self, repo_id: int) -> dict[str, frozenset[str]]:
        return {k: v.events for k, v in self._repo_secrets.get(repo_id, {}).items()}

    def org_secret_states(self) -> dict[str, SecretState]:
        return dict(self._org)

    def repo_secret_states(self, repo_id: int) -> dict[str, SecretState]:
        return dict(self._repo_secrets.get(repo_id, {}))

    def activated_repos(self) -> list[RepoInfo]:
        return [
            RepoInfo(id=rid, name=name, full_name=f"magogi-admin/{name}", active=True)
            for rid, name in self._repos
        ]

    # -- writes (mutate live state; value is accepted but never stored) ------- #

    def create_org_secret(
        self, name: str, value: bytes, events: frozenset[str], images: tuple[str, ...]
    ) -> None:
        _ = value
        self._org[name] = SecretState(frozenset(events), tuple(images))

    def update_org_secret(
        self, name: str, *, events: frozenset[str], images: tuple[str, ...]
    ) -> None:
        self._org[name] = SecretState(frozenset(events), tuple(images))

    def create_repo_secret(
        self, repo_id: int, name: str, value: bytes, events: frozenset[str], images: tuple[str, ...]
    ) -> None:
        _ = value
        self._repo_secrets.setdefault(repo_id, {})[name] = SecretState(
            frozenset(events), tuple(images)
        )

    def update_repo_secret(
        self, repo_id: int, name: str, *, events: frozenset[str], images: tuple[str, ...]
    ) -> None:
        self._repo_secrets.setdefault(repo_id, {})[name] = SecretState(
            frozenset(events), tuple(images)
        )


class FakeForge:
    """In-proc forge admin: per-repo deploy keys + register write. ``rate_limited``
    names repos whose reads raise :class:`SurfaceUnavailableError` (the 429 fixture)."""

    def __init__(
        self,
        keys_by_repo: dict[str, list[DeployKey]] | None = None,
        *,
        rate_limited: set[str] | None = None,
    ) -> None:
        self._keys = {k: list(v) for k, v in (keys_by_repo or {}).items()}
        self._rate_limited = rate_limited or set()

    def repo_deploy_keys(self, repo: str) -> list[DeployKey]:
        if repo in self._rate_limited:
            raise SurfaceUnavailableError(repo, "forge HTTP 429")
        return list(self._keys.get(repo, []))

    def register_deploy_key(self, repo: str, *, title: str, key: str, read_only: bool) -> None:
        # Record the PUBLIC key so a re-plan (and a reuse capture) round-trips it.
        self._keys.setdefault(repo, []).append(
            DeployKey(title=title, fingerprint="SHA256:registered", read_only=read_only, key=key)
        )


class FakeHost:
    """In-proc host admin: stat map + chmod/chown writes that mutate the stat."""

    def __init__(self, stats: dict[tuple[str, str], HostStat] | None = None) -> None:
        self._stats = dict(stats or {})

    def stat(self, host: str, path: str) -> HostStat:
        return self._stats.get((host, path), HostStat("missing"))

    def chmod(self, host: str, path: str, mode: str) -> bool:
        cur = self._stats.get((host, path))
        if cur is None or cur.status != "present":
            return False
        self._stats[(host, path)] = HostStat("present", mode=mode, owner=cur.owner)
        return True

    def chown(self, host: str, path: str, owner: str) -> bool:
        cur = self._stats.get((host, path))
        if cur is None or cur.status != "present":
            return False
        self._stats[(host, path)] = HostStat("present", mode=cur.mode, owner=owner)
        return True


def write_registry(tmp_path: Path, *toml_texts: str) -> Registry:
    """Write one SECRETS.toml per text and load them as a single registry."""
    paths = []
    for i, text in enumerate(toml_texts):
        p = tmp_path / f"SECRETS_{i}.toml"
        p.write_text(text, encoding="utf-8")
        paths.append(p)
    return load(paths)


def write_pipeline(fed_root: Path, repo: str, yaml_text: str) -> None:
    d = fed_root / repo
    d.mkdir(parents=True, exist_ok=True)
    (d / ".woodpecker.yml").write_text(yaml_text, encoding="utf-8")
