# SPDX-License-Identifier: Apache-2.0
"""Reject meaningful temporary pack faults with renewed item hashes.

Positive controls allow ordinary cache directories. Authored files stay intact.
"""

from __future__ import annotations

import importlib.util
import json
import os
import shutil
import subprocess
import tempfile
from collections.abc import Callable
from pathlib import Path
from types import ModuleType

HERE = Path(__file__).resolve().parent


def load(leaf: Path, name: str) -> ModuleType:
    """Load a verifier from an explicit temporary or source leaf."""
    spec = importlib.util.spec_from_file_location(name, leaf / "verify_pack.py")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def change_data(leaf: Path, action: Callable[[dict], None]) -> None:
    """Deliberately mutate one temporary source-data fixture."""
    path = leaf / "play-path.json"
    data = json.loads(path.read_text())
    action(data)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")


def replace(leaf: Path, name: str, old: str, new: str) -> None:
    """Apply one named substantive fault to a temporary resource."""
    path = leaf / name
    text = path.read_text()
    if old not in text:
        raise ValueError(f"Fault did not alter intended source: {name}")
    path.write_text(text.replace(old, new, 1))


def wrong_print(leaf: Path) -> None:
    """Actually re-render an intentionally changed temporary aid."""
    svg = leaf / "print" / "play-board.svg"
    replace(leaf, "print/play-board.svg", 'fill="white"', 'fill="#eecccc"')
    subprocess.run(
        [
            "rsvg-convert",
            "--format=pdf",
            "--output",
            str(leaf / "print" / "play-board.pdf"),
            str(svg),
        ],
        env={**os.environ, "SOURCE_DATE_EPOCH": "1790726400", "TZ": "UTC"},
        check=True,
        capture_output=True,
    )


def main() -> None:
    """Run the explicit authored or read-only verification mode."""
    owner = load(HERE, "small_world_owner_probes")
    before = owner.state()
    cases = [
        (
            "missing start",
            lambda p: change_data(p, lambda d: d["starts"].pop()),
            "ten complete",
        ),
        (
            "missing opening",
            lambda p: change_data(p, lambda d: d["starts"][0].pop("opening")),
            "missing opening",
        ),
        (
            "long opening",
            lambda p: change_data(
                p, lambda d: d["starts"][0].update(opening="More words " * 20)
            ),
            "opening too long",
        ),
        (
            "duplicate route",
            lambda p: change_data(
                p,
                lambda d: d["starts"][0]["routes"].__setitem__(
                    1, d["starts"][0]["routes"][0]
                ),
            ),
            "distinct routes",
        ),
        (
            "route mode lost",
            lambda p: change_data(
                p,
                lambda d: d["starts"][0]["routes"].__setitem__(
                    2, "Any activity: " + "Useful words " * 40
                ),
            ),
            "concrete route mode",
        ),
        (
            "thin route",
            lambda p: change_data(
                p,
                lambda d: d["starts"][0]["routes"].__setitem__(
                    0, "Draw or arrange: Do it."
                ),
            ),
            "thin route",
        ),
        (
            "duplicate example",
            lambda p: change_data(
                p,
                lambda d: d["starts"][1]["examples"].__setitem__(
                    1, d["starts"][1]["examples"][0]
                ),
            ),
            "distinct examples",
        ),
        (
            "thin example",
            lambda p: change_data(
                p, lambda d: d["starts"][1]["examples"].__setitem__(1, "Library: Play.")
            ),
            "thin example",
        ),
        (
            "clock gap",
            lambda p: change_data(
                p,
                lambda d: d["starts"][0]["steps"].__setitem__(
                    1, d["starts"][0]["steps"][1].replace("2–5", "3–5")
                ),
            ),
            "clock gap/overlap",
        ),
        (
            "clock overlap",
            lambda p: change_data(
                p,
                lambda d: d["starts"][0]["steps"].__setitem__(
                    1, d["starts"][0]["steps"][1].replace("2–5", "1–5")
                ),
            ),
            "clock gap/overlap",
        ),
        (
            "clock backwards",
            lambda p: change_data(
                p,
                lambda d: d["starts"][0]["steps"].__setitem__(
                    1, d["starts"][0]["steps"][1].replace("2–5", "2–1")
                ),
            ),
            "clock gap/overlap",
        ),
        (
            "short clock",
            lambda p: change_data(
                p,
                lambda d: d["starts"][0]["steps"].__setitem__(
                    3, d["starts"][0]["steps"][3].replace("8–10", "8–9")
                ),
            ),
            "clock not ten",
        ),
        (
            "fiction as feedback",
            lambda p: change_data(
                p,
                lambda d: d["starts"][7]["steps"].__setitem__(
                    2,
                    "5–8 minutes: The pretend visitor's praise proves the guide works for people.",
                ),
            ),
            "fictional and real feedback",
        ),
        (
            "plan as benefit",
            lambda p: change_data(
                p,
                lambda d: d["starts"][4].update(
                    notice="The untried plan proves that the quiet corner helped the visitor."
                ),
            ),
            "observed benefit",
        ),
        (
            "adult support erased",
            lambda p: change_data(
                p,
                lambda d: d["starts"][8].update(
                    notice="All adult actions count as the child's own work."
                ),
            ),
            "adult support attributed",
        ),
        (
            "completion reward",
            lambda p: change_data(
                p,
                lambda d: d["starts"][9]["routes"].__setitem__(
                    2,
                    "Small start or no materials: "
                    + "Complete all ten to earn your reward. " * 8,
                ),
            ),
            "completion/reward",
        ),
        (
            "wrong framework version",
            lambda p: replace(p, "SOURCE-NOTES.md", "V2.0 (2022)", "V1.0 (2009)"),
            "date/version",
        ),
        (
            "wrong primary locator",
            lambda p: replace(
                p,
                "SOURCE-NOTES.md",
                "approved-learning-frameworks)",
                "unverified-frameworks)",
            ),
            "source URL/locator",
        ),
        (
            "outside collection",
            lambda p: replace(
                p,
                "digital/index.html",
                '"use strict";',
                '"use strict"; localStorage.setItem("child", "saved");',
            ),
            "collection/outside/media",
        ),
        (
            "active no-script choices",
            lambda p: replace(
                p,
                "digital/index.html",
                'disabled data-group="place"',
                'data-group="place"',
            ),
            "disabled",
        ),
        (
            "unbuilt helper",
            lambda p: replace(
                p, "digital/index.html", "</main>", "</main>__CARD_DATA__"
            ),
            "Unbuilt helper",
        ),
        (
            "print beyond page",
            lambda p: replace(p, "print/play-board.svg", 'width="706"', 'width="850"'),
            "box bounds",
        ),
        ("wrong rendered print", wrong_print, "reproduction drift"),
        (
            "lost linear equivalent",
            lambda p: replace(
                p,
                "print/TEXT-ALTERNATIVES.md",
                "Three supplied broad shape suggestions",
                "Three finished child characters",
            ),
            "reproduction drift",
        ),
        (
            "broken card link",
            lambda p: replace(
                p,
                "README.md",
                "cards/01-a-place-for-a-visitor.md",
                "cards/01-absent.md",
            ),
            "Broken local link",
        ),
        (
            "broken print fragment",
            lambda p: replace(
                p, "cards/01-a-place-for-a-visitor.md", "#play-board", "#absent-board"
            ),
            "Broken local fragment",
        ),
        (
            "changed upstream notice",
            lambda p: replace(
                p, "print/DEJAVU-FONT-LICENSE.txt", "Bitstream", "Changedstream"
            ),
            "font licence bytes",
        ),
    ]
    rejected = []
    controls = 0
    with tempfile.TemporaryDirectory(prefix="subjectnest-small-world-probes-") as tmp:
        for index, (name, action, expected) in enumerate(cases):
            leaf = Path(tmp) / str(index)
            shutil.copytree(
                HERE, leaf, ignore=shutil.ignore_patterns(*owner.CACHE_PARTS)
            )
            action(leaf)
            verifier = load(leaf, f"small_world_probe_{index}")
            verifier.refresh()
            try:
                verifier.verify()
            except (ValueError, OSError, TypeError, KeyError) as exc:
                if expected.lower() not in str(exc).lower():
                    raise ValueError(f"{name} rejected at wrong gate: {exc}") from exc
                rejected.append(name)
            else:
                raise ValueError(f"Fault passed: {name}")
        for index, folder in enumerate(
            ("__pycache__", "print/.ruff_cache", "digital/.pytest_cache")
        ):
            leaf = Path(tmp) / f"cache-{index}"
            shutil.copytree(
                HERE, leaf, ignore=shutil.ignore_patterns(*owner.CACHE_PARTS)
            )
            cache = leaf / folder
            cache.mkdir()
            (cache / "ordinary-cache.bin").write_bytes(b"cache control")
            load(leaf, f"small_world_cache_{index}").verify()
            controls += 1
    if owner.state() != before:
        raise ValueError("Negative probes altered authored bytes/size/mtime")
    print(
        json.dumps(
            {
                "status": "PASS",
                "meaningful_faults_rejected": len(rejected),
                "cache_positive_controls": controls,
                "faults": rejected,
            },
            indent=2,
        )
    )


if __name__ == "__main__":
    main()
