"""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 PipelineInfo, 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,
        pipelines: dict[int, list[PipelineInfo]] | None = None,
        unavailable_pipelines: set[int] | 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()
        }
        # Error-status watch (§3) fixtures (the no-checkout config source is a FORGE
        # read now — see FakeForge.repo_file — not a Woodpecker read).
        self._pipelines = {rid: list(ps) for rid, ps in (pipelines or {}).items()}
        self._unavailable_pipelines = unavailable_pipelines or set()
        self._org_values: dict[str, bytes] = {}
        self._repo_values: dict[int, dict[str, bytes]] = {}

    # -- 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
        ]

    def recent_pipelines(self, repo_id: int, limit: int = 10) -> list[PipelineInfo]:
        if repo_id in self._unavailable_pipelines:
            raise SurfaceUnavailableError(f"pipelines:{repo_id}", "woodpecker HTTP 503")
        return self._pipelines.get(repo_id, [])[:limit]

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

    # Deployed VALUES, which the real API never returns. Recorded here ONLY so a
    # test can prove a `refresh-value` actually changed what the surface serves —
    # the whole point of the deploy ledger is that this is invisible in production.
    # TEST values only (AG-18).

    def deployed_value(self, name: str, repo_id: int | None = None) -> bytes | None:
        if repo_id is None:
            return self._org_values.get(name)
        return self._repo_values.get(repo_id, {}).get(name)

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

    def update_org_secret(
        self,
        name: str,
        *,
        events: frozenset[str],
        images: tuple[str, ...],
        value: bytes | None = None,
    ) -> None:
        # value=None is a re-scope: it must NOT disturb the deployed value.
        if value is not None:
            self._org_values[name] = value
        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:
        self._repo_values.setdefault(repo_id, {})[name] = 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, ...],
        value: bytes | None = None,
    ) -> None:
        if value is not None:
            self._repo_values.setdefault(repo_id, {})[name] = value
        self._repo_secrets.setdefault(repo_id, {})[name] = SecretState(
            frozenset(events), tuple(images)
        )


class FakeForge:
    """In-proc forge admin: per-repo deploy keys + register write + committed-file
    reads (:meth:`repo_file`, the no-checkout consumed-check source). ``rate_limited``
    names repos whose reads raise :class:`SurfaceUnavailableError` (the 429 fixture);
    ``files`` maps a repo to its RAW committed ``.woodpecker.yml`` text."""

    def __init__(
        self,
        keys_by_repo: dict[str, list[DeployKey]] | None = None,
        *,
        rate_limited: set[str] | None = None,
        files: dict[str, 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()
        self._files = dict(files or {})

    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 repo_file(self, repo: str, path: str = ".woodpecker.yml") -> str | None:
        if repo in self._rate_limited:
            raise SurfaceUnavailableError(repo, "forge HTTP 429")
        return self._files.get(repo)  # None => 404 (no committed file, not-consumed)

    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")
