from __future__ import annotations

import hashlib
import importlib.util
import json
import os
import shutil
import subprocess
from pathlib import Path

import pytest
import yaml

SCRIPT = Path(__file__).with_name("check-release-process-guards.py")
REPO_ROOT = SCRIPT.parents[2]
SPEC = importlib.util.spec_from_file_location("check_release_process_guards", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
GUARDS = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(GUARDS)
REVIEWED_PARENT = "4e391ee726642d99abc2c61966dcd80a836e6c1c"
RAW_SCANNER_PARENT = "11ac6d9e9d27677d06d513364f2e658f5ed99870"


def _copy_contract_tree(tmp_path: Path, monkeypatch) -> Path:
    shutil.copy2(REPO_ROOT / "codemagic.yaml", tmp_path / "codemagic.yaml")
    fixture = tmp_path / ".github/scripts/fixtures/codemagic_workflow_contract/v1.json"
    fixture.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/scripts/fixtures/codemagic_workflow_contract/v1.json", fixture)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    return tmp_path / "codemagic.yaml"


def _mutate(path: Path, old: str, new: str, *, count: int = 1) -> None:
    text = path.read_text(encoding="utf-8")
    assert text.count(old) >= count, old
    path.write_text(text.replace(old, new, count), encoding="utf-8")


def _errors_after(tmp_path: Path, monkeypatch, old: str, new: str, *, count: int = 1) -> list[str]:
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    _mutate(codemagic, old, new, count=count)
    return GUARDS.check_codemagic_release_publishers()


def _assert_contract_rejects(errors: list[str]) -> None:
    assert any("contract" in error or "fixture" in error for error in errors), errors


def _load_parent_guard(tmp_path: Path, revision: str = REVIEWED_PARENT):
    """Load the reviewed parent to prove its narrower lock accepted known bypasses."""
    parent_script = tmp_path / "parent-guard.py"
    parent_script.write_bytes(
        subprocess.check_output(
            ["git", "show", f"{revision}:.github/scripts/check-release-process-guards.py"],
            cwd=REPO_ROOT,
        )
    )
    spec = importlib.util.spec_from_file_location("reviewed_parent_guard", parent_script)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def _copy_reviewed_parent_contract(tmp_path: Path) -> Path:
    codemagic = tmp_path / "codemagic.yaml"
    codemagic.write_bytes(
        subprocess.check_output(
            ["git", "show", f"{REVIEWED_PARENT}:codemagic.yaml"],
            cwd=REPO_ROOT,
        )
    )
    fixture = tmp_path / ".github/scripts/fixtures/codemagic_workflow_contract/v1.json"
    fixture.parent.mkdir(parents=True, exist_ok=True)
    fixture.write_bytes(
        subprocess.check_output(
            ["git", "show", f"{REVIEWED_PARENT}:.github/scripts/fixtures/codemagic_workflow_contract/v1.json"],
            cwd=REPO_ROOT,
        )
    )
    return codemagic


def _append_unreviewed_workflow(
    path: Path, script: str, *, credential_group: str = "alternate_release_credentials"
) -> None:
    indented_script = script.replace("\n", "\n          ")
    path.write_text(
        path.read_text(encoding="utf-8") + f"""\n  unreviewed-publisher:
    environment:
      groups:
        - {credential_group}
    scripts:
      - name: Unreviewed publisher
        script: |
          {indented_script}
""",
        encoding="utf-8",
    )


def _restore_parent_preview_credential_shape(path: Path) -> None:
    """Keep historical-parent regression probes independent of the temporary exception."""
    old = "        - desktop_preview_secrets\n        - appstore_credentials\n        - desktop_secrets\n"
    if old in path.read_text(encoding="utf-8"):
        _mutate(path, old, "        - desktop_preview_secrets\n")
    contract_path = _fixture_path(path.parent)
    contract = json.loads(contract_path.read_text(encoding="utf-8"))
    preview = yaml.safe_load(path.read_text(encoding="utf-8"))["workflows"]["omi-desktop-swift-preview"]
    contract["omi-desktop-swift-preview"]["semantic_sha256"] = hashlib.sha256(
        json.dumps(preview, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    ).hexdigest()
    contract_path.write_text(json.dumps(contract, indent=2) + "\n", encoding="utf-8")


def _fixture_path(root: Path) -> Path:
    return root / ".github/scripts/fixtures/codemagic_workflow_contract/v1.json"


def _approve_current_codemagic_document(root: Path) -> None:
    codemagic = root / "codemagic.yaml"
    raw_bytes = codemagic.read_bytes()
    document = yaml.safe_load(raw_bytes.decode("utf-8"))
    contract = json.loads(_fixture_path(root).read_text(encoding="utf-8"))
    contract["codemagic_raw_sha256"] = hashlib.sha256(raw_bytes).hexdigest()
    contract["codemagic_semantic_sha256"] = hashlib.sha256(
        json.dumps(document, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    ).hexdigest()
    for workflow_name in ("omi-desktop-swift-release", "omi-desktop-swift-preview"):
        workflow_json = json.dumps(
            document["workflows"][workflow_name],
            sort_keys=True,
            separators=(",", ":"),
            ensure_ascii=False,
        )
        contract[workflow_name]["semantic_sha256"] = hashlib.sha256(workflow_json.encode("utf-8")).hexdigest()
    _fixture_path(root).write_text(json.dumps(contract, indent=2) + "\n", encoding="utf-8")


def _parent_bypass_workflow(extra: str) -> str:
    return f'''workflows:
  omi-desktop-swift-release:
    scripts: &desktop_signed_artifact_steps
      - name: Smoke signed desktop artifact
        script: echo smoke
      - name: Create GitHub release
        script: |
          curl --request POST /v2/desktop/beta/candidates/reserve
          gh release create "$CM_TAG"
          {extra}
  omi-desktop-swift-preview:
    environment:
      vars:
        PREVIEW_MODE: "true"
    scripts: *desktop_signed_artifact_steps
'''


@pytest.mark.parametrize(
    "mutation",
    (
        'X=gh; $X release create "$TAG_NAME"',
        '${X:-gh} release create "$TAG_NAME"',
        'R=release; gh "$R" create "$TAG_NAME"',
        'gh r$(printf elease) create "$TAG_NAME"',
        'gh release c$(printf reate) "$TAG_NAME"',
        'echo harmless#$(X=gh; $X release create "$TAG_NAME")',
        'curl -X POST https://api.github.com/repos/BasedHardware/omi/releases',
    ),
)
def test_exact_contract_rejects_constructed_or_api_release_authority(tmp_path, monkeypatch, mutation):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        '          gh release create "$CM_TAG" \\\n',
        f'          {mutation}\n          gh release create "$CM_TAG" \\\n',
    )
    _assert_contract_rejects(errors)


@pytest.mark.parametrize(
    "replacement",
    (
        "false && curl --fail-with-body --silent --show-error \\",
        "if false; then curl --fail-with-body --silent --show-error; fi\n            curl --fail-with-body --silent --show-error \\",
        "curl() { echo decoy; }\n            curl --fail-with-body --silent --show-error \\",
    ),
)
def test_exact_contract_rejects_suppressed_or_shadowed_reservation(tmp_path, monkeypatch, replacement):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        "            curl --fail-with-body --silent --show-error \\",
        f"            {replacement}",
    )
    _assert_contract_rejects(errors)


def test_exact_contract_rejects_preview_alias_without_early_exit(tmp_path, monkeypatch):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        '          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then\n            echo "External previews do not create GitHub releases."\n            exit 0\n          fi\n',
        "",
    )
    _assert_contract_rejects(errors)
    assert any("preview publication script" in error for error in errors)


def test_preview_credential_exception_is_exact_and_rejects_group_drift(tmp_path, monkeypatch):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    _mutate(
        codemagic,
        "        - desktop_secrets\n      vars:\n        PREVIEW_MODE: \"true\"\n",
        "        - desktop_secrets\n        - unexpected_preview_credentials\n      vars:\n        PREVIEW_MODE: \"true\"\n",
    )
    _approve_current_codemagic_document(tmp_path)
    errors = GUARDS.check_codemagic_release_publishers()
    assert any("approved temporary credential groups" in error for error in errors), errors


def test_exact_contract_rejects_another_workflow_importing_release_credentials(tmp_path, monkeypatch):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        "  omi-desktop-swift-preview:\n",
        "  unrelated:\n    environment:\n      groups:\n        - desktop_secrets\n    scripts: []\n\n  omi-desktop-swift-preview:\n",
    )
    assert any("unrelated imports normal release credential" in error for error in errors)


@pytest.mark.parametrize("token_name", ("GH_TOKEN", "GITHUB_TOKEN"))
def test_exact_contract_rejects_github_tokens_outside_canonical(tmp_path, monkeypatch, token_name):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        '        PREVIEW_MODE: "true"\n',
        f'        PREVIEW_MODE: "true"\n        {token_name}: shadow\n',
    )
    assert any(f"exposes {token_name} outside canonical" in error for error in errors)


def test_exact_contract_rejects_semantic_mutation_elsewhere_in_canonical_workflow(tmp_path, monkeypatch):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        "    max_build_duration: 120\n    environment:\n      groups:\n        - app_env\n",
        "    max_build_duration: 121\n    environment:\n      groups:\n        - app_env\n",
    )
    _assert_contract_rejects(errors)


def test_exact_contract_requires_shared_preview_script_node_not_a_copy(tmp_path, monkeypatch):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        "    scripts: *desktop_signed_artifact_steps\n",
        "    scripts:\n      - name: copied\n        script: echo copied\n",
    )
    assert any("exact YAML alias node" in error or "anchor and alias" in error for error in errors)


@pytest.mark.parametrize(
    "mutation, expected",
    (
        ("    <<: *desktop_signed_artifact_steps\n", "merge keys"),
        ("    name: !!str Release OMI Desktop (Swift)\n", "explicit YAML tags"),
        ("  omi-desktop-swift-release:\n    scripts: []\n", "duplicate key"),
    ),
)
def test_exact_contract_rejects_yaml_merge_tag_and_duplicate_collisions(tmp_path, monkeypatch, mutation, expected):
    errors = _errors_after(
        tmp_path,
        monkeypatch,
        "  omi-desktop-swift-preview:\n",
        f"{mutation}  omi-desktop-swift-preview:\n",
    )
    assert any(expected in error for error in errors), errors


@pytest.mark.parametrize(
    "bypass",
    (
        'X=gh; $X release create "$TAG_NAME"',
        '${X:-gh} release create "$TAG_NAME"',
        'R=release; gh "$R" create "$TAG_NAME"',
        'gh r$(printf elease) create "$TAG_NAME"',
        'gh release c$(printf reate) "$TAG_NAME"',
        'echo harmless#$(X=gh; $X release create "$TAG_NAME")',
        "curl -X POST https://api.github.com/repos/BasedHardware/omi/releases",
    ),
)
def test_reviewed_parent_accepts_constructed_release_and_api_bypasses(tmp_path, bypass):
    parent = _load_parent_guard(tmp_path, RAW_SCANNER_PARENT)
    (tmp_path / "codemagic.yaml").write_text(_parent_bypass_workflow(bypass), encoding="utf-8")
    parent.ROOT = tmp_path
    assert parent.check_codemagic_release_publishers() == [], bypass


@pytest.mark.parametrize(
    "replacement",
    (
        "false && curl --request POST /v2/desktop/beta/candidates/reserve",
        "if false; then curl --request POST /v2/desktop/beta/candidates/reserve; fi",
        "curl() { echo decoy; }\n          curl --request POST /v2/desktop/beta/candidates/reserve",
    ),
)
def test_reviewed_parent_accepts_suppressed_or_shadowed_reservations(tmp_path, replacement):
    parent = _load_parent_guard(tmp_path, RAW_SCANNER_PARENT)
    workflow = _parent_bypass_workflow("echo harmless").replace(
        "curl --request POST /v2/desktop/beta/candidates/reserve", replacement, 1
    )
    (tmp_path / "codemagic.yaml").write_text(workflow, encoding="utf-8")
    parent.ROOT = tmp_path
    assert parent.check_codemagic_release_publishers() == [], replacement


def test_reviewed_parent_accepts_preview_alias_without_early_exit(tmp_path):
    parent = _load_parent_guard(tmp_path, RAW_SCANNER_PARENT)
    (tmp_path / "codemagic.yaml").write_text(_parent_bypass_workflow("echo harmless"), encoding="utf-8")
    parent.ROOT = tmp_path
    assert parent.check_codemagic_release_publishers() == []


def test_codemagic_workflow_contract_accepts_current_production_configuration():
    assert GUARDS.check_codemagic_release_publishers() == []


def _mobile_trigger_errors_after(tmp_path: Path, monkeypatch, old: str, new: str) -> list[str]:
    codemagic = tmp_path / "codemagic.yaml"
    shutil.copy2(REPO_ROOT / "codemagic.yaml", codemagic)
    dispatcher = tmp_path / ".github/workflows/mobile_internal_build.yml"
    dispatcher.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/workflows/mobile_internal_build.yml", dispatcher)
    dispatcher_script = tmp_path / ".github/scripts/dispatch_mobile_internal_builds.py"
    dispatcher_script.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/scripts/dispatch_mobile_internal_builds.py", dispatcher_script)
    _mutate(dispatcher, old, new)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    return GUARDS.check_mobile_codemagic_release_triggers()


def test_mobile_codemagic_dispatcher_is_production_safe(tmp_path, monkeypatch):
    codemagic = tmp_path / "codemagic.yaml"
    shutil.copy2(REPO_ROOT / "codemagic.yaml", codemagic)
    dispatcher = tmp_path / ".github/workflows/mobile_internal_build.yml"
    dispatcher.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/workflows/mobile_internal_build.yml", dispatcher)
    dispatcher_script = tmp_path / ".github/scripts/dispatch_mobile_internal_builds.py"
    dispatcher_script.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/scripts/dispatch_mobile_internal_builds.py", dispatcher_script)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)

    assert GUARDS.check_mobile_codemagic_release_triggers() == []


def test_mobile_codemagic_trigger_guard_rejects_legacy_github_dispatcher(tmp_path, monkeypatch):
    codemagic = tmp_path / "codemagic.yaml"
    shutil.copy2(REPO_ROOT / "codemagic.yaml", codemagic)
    dispatcher = tmp_path / ".github/workflows/mobile_internal_auto.yml"
    dispatcher.parent.mkdir(parents=True)
    dispatcher.write_text("name: legacy dispatcher\n", encoding="utf-8")
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)

    errors = GUARDS.check_mobile_codemagic_release_triggers()

    assert any("must not be dispatched through GitHub Actions" in error for error in errors), errors


def _desktop_candidate_trigger_guard_root(tmp_path: Path, monkeypatch) -> tuple[Path, Path]:
    codemagic = tmp_path / "codemagic.yaml"
    candidate = tmp_path / ".github/workflows/desktop_auto_release.yml"
    preview = tmp_path / ".github/workflows/desktop_publish_preview.yml"
    candidate.parent.mkdir(parents=True)
    shutil.copy2(REPO_ROOT / "codemagic.yaml", codemagic)
    shutil.copy2(REPO_ROOT / ".github/workflows/desktop_auto_release.yml", candidate)
    shutil.copy2(REPO_ROOT / ".github/workflows/desktop_publish_preview.yml", preview)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    return codemagic, candidate


def test_desktop_candidate_trigger_guard_keeps_direct_tag_lane_and_preview_exception(tmp_path, monkeypatch):
    _desktop_candidate_trigger_guard_root(tmp_path, monkeypatch)

    assert GUARDS.check_desktop_candidate_trigger_authority() == []


def test_desktop_candidate_trigger_guard_rejects_native_trigger_race(tmp_path, monkeypatch):
    codemagic, _ = _desktop_candidate_trigger_guard_root(tmp_path, monkeypatch)
    _mutate(
        codemagic,
        "    working_directory: desktop/macos\n",
        "    triggering:\n      events:\n        - tag\n    working_directory: desktop/macos\n",
    )

    errors = GUARDS.check_desktop_candidate_trigger_authority()

    assert any("must not race direct dispatch with a native trigger" in error for error in errors), errors


def test_desktop_candidate_trigger_guard_rejects_missing_direct_dispatch(tmp_path, monkeypatch):
    _, candidate = _desktop_candidate_trigger_guard_root(tmp_path, monkeypatch)
    _mutate(candidate, "--timeout-seconds 0", "--timeout-seconds 600")

    errors = GUARDS.check_desktop_candidate_trigger_authority()

    assert any("missing direct Codemagic dispatch: --timeout-seconds 0" in error for error in errors), errors


def test_desktop_candidate_trigger_guard_rejects_direct_build_api_for_normal_lane(tmp_path, monkeypatch):
    _, candidate = _desktop_candidate_trigger_guard_root(tmp_path, monkeypatch)
    candidate.write_text(
        candidate.read_text(encoding="utf-8")
        + "\n# forbidden normal-candidate dispatcher\n# https://api.codemagic.io/builds\n",
        encoding="utf-8",
    )

    errors = GUARDS.check_desktop_candidate_trigger_authority()

    assert any("must not start Codemagic with a direct builds API POST" in error for error in errors), errors


@pytest.mark.parametrize(
    ("old", "new"),
    (
        ("    branches: [main]\n", "    branches: [release]\n"),
        ("    paths: ['app/**']\n", "    paths: ['desktop/**']\n"),
        ("    - cron: '0 */3 * * *'\n", "    - cron: '0 */4 * * *'\n"),
        ("cancel-in-progress: false", "cancel-in-progress: true"),
    ),
)
def test_mobile_codemagic_trigger_guard_rejects_regressions(tmp_path, monkeypatch, old, new):
    errors = _mobile_trigger_errors_after(tmp_path, monkeypatch, old, new)

    assert any(
        "main app/** pushes" in error
        or "three-hour schedule" in error
        or "workflow_dispatch" in error
        or "must not cancel" in error
        for error in errors
    ), errors


def test_mobile_codemagic_dispatcher_guard_rejects_missing_dispatch_command(tmp_path, monkeypatch):
    codemagic = tmp_path / "codemagic.yaml"
    shutil.copy2(REPO_ROOT / "codemagic.yaml", codemagic)
    dispatcher = tmp_path / ".github/workflows/mobile_internal_build.yml"
    dispatcher.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/workflows/mobile_internal_build.yml", dispatcher)
    dispatcher_script = tmp_path / ".github/scripts/dispatch_mobile_internal_builds.py"
    dispatcher_script.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/scripts/dispatch_mobile_internal_builds.py", dispatcher_script)
    _mutate(
        dispatcher,
        "python3 .github/scripts/dispatch_mobile_internal_builds.py",
        "python3 .github/scripts/other_dispatcher.py",
    )
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)

    errors = GUARDS.check_mobile_codemagic_release_triggers()

    assert errors == ["mobile internal build dispatcher must invoke dispatch_mobile_internal_builds.py"], errors


def test_mobile_codemagic_dispatcher_guard_rejects_missing_workflow_target(tmp_path, monkeypatch):
    codemagic = tmp_path / "codemagic.yaml"
    shutil.copy2(REPO_ROOT / "codemagic.yaml", codemagic)
    dispatcher = tmp_path / ".github/workflows/mobile_internal_build.yml"
    dispatcher.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/workflows/mobile_internal_build.yml", dispatcher)
    dispatcher_script = tmp_path / ".github/scripts/dispatch_mobile_internal_builds.py"
    dispatcher_script.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / ".github/scripts/dispatch_mobile_internal_builds.py", dispatcher_script)
    _mutate(
        dispatcher_script,
        'MOBILE_WORKFLOWS = ("ios-internal-auto", "android-internal-auto")',
        'MOBILE_WORKFLOWS = ("ios-internal-auto",)',
    )
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)

    errors = GUARDS.check_mobile_codemagic_release_triggers()

    assert errors == ["mobile internal build dispatcher script must declare both Codemagic mobile workflows"], errors


@pytest.mark.parametrize(
    "forbidden_authority",
    (
        "GCP_SERVICE_ACCOUNT_KEY",
        "Cloud Run Admin",
        "roles/run.admin",
        "Storage Object Admin",
        "roles/storage.objectAdmin",
        "GCR push",
    ),
)
def test_normal_release_rejects_forbidden_broad_gcp_authority_even_after_lock_update(
    tmp_path, monkeypatch, forbidden_authority
):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    _mutate(
        codemagic,
        '        GCP_PROJECT: "based-hardware"\n',
        f'        GCP_PROJECT: "based-hardware"\n        FORBIDDEN_GCP_AUTHORITY: "{forbidden_authority}"\n',
    )
    _approve_current_codemagic_document(tmp_path)

    errors = GUARDS.check_codemagic_release_publishers()

    assert any("forbidden broad GCP authority" in error and forbidden_authority in error for error in errors), errors


def test_normal_release_gcp_authority_guard_ignores_harmless_source_comments(tmp_path, monkeypatch):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    _mutate(
        codemagic,
        "  # OMI DESKTOP SWIFT RELEASE\n",
        "  # OMI DESKTOP SWIFT RELEASE (historical prose may mention roles/run.admin)\n",
    )
    _approve_current_codemagic_document(tmp_path)

    assert GUARDS.check_codemagic_release_publishers() == []


@pytest.mark.parametrize(
    "script",
    (
        'X=gh; $X release create "$CM_TAG"',
        './release-publisher "$CM_TAG"',
        'python3 -c \'import subprocess; subprocess.run(["gh", "release", "create", "v0"])\'',
        'URL=https://api.github.com/repos/BasedHardware/omi/releases; curl -X POST "$URL"',
    ),
    ids=("shell-construction", "direct-wrapper", "python-construction", "variable-api-url"),
)
def test_reviewed_parent_accepts_unreviewed_publisher_bypasses_but_global_lock_rejects(tmp_path, monkeypatch, script):
    codemagic = _copy_reviewed_parent_contract(tmp_path)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    _restore_parent_preview_credential_shape(codemagic)
    _approve_current_codemagic_document(tmp_path)
    _append_unreviewed_workflow(codemagic, script)

    parent = _load_parent_guard(tmp_path)
    parent.ROOT = tmp_path
    assert parent.check_codemagic_release_publishers() == [], script

    errors = GUARDS.check_codemagic_release_publishers()
    assert any("entire document" in error for error in errors), errors


def test_reviewed_parent_accepts_unknown_credential_group_with_publisher_but_global_lock_rejects(tmp_path, monkeypatch):
    codemagic = _copy_reviewed_parent_contract(tmp_path)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    _restore_parent_preview_credential_shape(codemagic)
    _approve_current_codemagic_document(tmp_path)
    _append_unreviewed_workflow(
        codemagic,
        'X=gh; $X release create "$CM_TAG"',
        credential_group="unrecognized_release_authority",
    )

    parent = _load_parent_guard(tmp_path)
    parent.ROOT = tmp_path
    assert parent.check_codemagic_release_publishers() == []

    errors = GUARDS.check_codemagic_release_publishers()
    assert any("entire document" in error for error in errors), errors


@pytest.mark.parametrize(
    ("old", "new", "parent_accepts"),
    (
        (
            "    max_build_duration: 120\n    integrations:\n      app_store_connect: codemagic_v4\n",
            "    max_build_duration: 121\n    integrations:\n      app_store_connect: codemagic_v4\n",
            True,
        ),
        (
            "  # AUTO-DEPLOY WORKFLOWS (Triggered on push to main)\n",
            "  # This reviewed comment changes only raw source bytes.\n",
            True,
        ),
        (
            "&desktop_signed_artifact_steps",
            "&renamed_desktop_signed_artifact_steps",
            False,
        ),
        (
            "workflows:\n",
            "review_metadata: reviewed\nworkflows:\n",
            True,
        ),
    ),
    ids=("unrelated-workflow", "comment-bytes", "anchor-spelling", "top-level-field"),
)
def test_global_document_lock_rejects_every_codemagic_mutation(tmp_path, monkeypatch, old, new, parent_accepts):
    codemagic = _copy_reviewed_parent_contract(tmp_path)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    _restore_parent_preview_credential_shape(codemagic)
    _approve_current_codemagic_document(tmp_path)
    _mutate(codemagic, old, new)
    if old == "&desktop_signed_artifact_steps":
        _mutate(codemagic, "*desktop_signed_artifact_steps", "*renamed_desktop_signed_artifact_steps")

    parent = _load_parent_guard(tmp_path)
    _restore_parent_preview_credential_shape(codemagic)
    parent.ROOT = tmp_path
    parent_errors = parent.check_codemagic_release_publishers()
    assert (parent_errors == []) is parent_accepts, parent_errors

    errors = GUARDS.check_codemagic_release_publishers()
    assert any("entire document" in error for error in errors), errors


def test_global_document_raw_lock_rejects_semantically_equivalent_yaml_rewrite(tmp_path, monkeypatch):
    codemagic = _copy_reviewed_parent_contract(tmp_path)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    _restore_parent_preview_credential_shape(codemagic)
    original_document = yaml.safe_load(codemagic.read_text(encoding="utf-8"))
    _approve_current_codemagic_document(tmp_path)
    _mutate(
        codemagic,
        "    name: Auto Deploy iOS to Internal TestFlight\n",
        '    name: "Auto Deploy iOS to Internal TestFlight"\n',
    )
    assert yaml.safe_load(codemagic.read_text(encoding="utf-8")) == original_document

    parent = _load_parent_guard(tmp_path)
    parent.ROOT = tmp_path
    assert parent.check_codemagic_release_publishers() == []

    errors = GUARDS.check_codemagic_release_publishers()
    assert any("raw byte digest" in error for error in errors), errors
    assert not any("semantic digest" in error for error in errors), errors


def test_global_document_semantic_lock_is_checked_even_when_raw_bytes_are_unchanged(tmp_path, monkeypatch):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    loaded = yaml.safe_load(codemagic.read_text(encoding="utf-8"))
    assert isinstance(loaded, dict)
    loaded["semantic_lock_test_seam"] = "changed only through the safe-load seam"
    monkeypatch.setattr(GUARDS, "_load_codemagic_with_duplicates", lambda _raw_bytes: (loaded, [], []))

    errors = GUARDS.check_codemagic_release_publishers()
    assert not any("raw byte digest" in error for error in errors), errors
    assert any("semantic digest" in error for error in errors), errors


@pytest.mark.parametrize(
    ("field", "value"),
    (
        ("codemagic_raw_sha256", "0" * 64),
        ("codemagic_semantic_sha256", "0" * 64),
        ("codemagic_raw_sha256", None),
        ("codemagic_semantic_sha256", "not-a-sha256"),
    ),
    ids=("raw-tampered", "semantic-tampered", "raw-missing", "semantic-malformed"),
)
def test_global_document_lock_rejects_tampered_missing_or_malformed_fixture_digests(
    tmp_path, monkeypatch, field, value
):
    _copy_contract_tree(tmp_path, monkeypatch)
    fixture = _fixture_path(tmp_path)
    contract = json.loads(fixture.read_text(encoding="utf-8"))
    if value is None:
        del contract[field]
    else:
        contract[field] = value
    fixture.write_text(json.dumps(contract, indent=2) + "\n", encoding="utf-8")

    errors = GUARDS.check_codemagic_release_publishers()
    assert any("fixture" in error and field in error for error in errors), errors


def test_fixture_independently_hashes_only_current_codemagic_source():
    codemagic = REPO_ROOT / "codemagic.yaml"
    fixture = _fixture_path(REPO_ROOT)
    contract = json.loads(fixture.read_text(encoding="utf-8"))
    raw_bytes = codemagic.read_bytes()
    semantic_json = json.dumps(
        yaml.safe_load(raw_bytes.decode("utf-8")), sort_keys=True, separators=(",", ":"), ensure_ascii=False
    )

    assert contract["codemagic_raw_sha256"] == hashlib.sha256(raw_bytes).hexdigest()
    assert contract["codemagic_semantic_sha256"] == hashlib.sha256(semantic_json.encode("utf-8")).hexdigest()
    assert contract["codemagic_raw_sha256"] != hashlib.sha256(fixture.read_bytes()).hexdigest()


def test_global_document_lock_rejects_codemagic_symlink_even_when_target_is_approved(tmp_path, monkeypatch):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    approved = tmp_path / "approved-codemagic.yaml"
    approved.write_bytes(codemagic.read_bytes())
    codemagic.unlink()
    codemagic.symlink_to(approved)

    errors = GUARDS.check_codemagic_release_publishers()

    assert any("ordinary regular file" in error and "codemagic.yaml" in error for error in errors), errors


def test_global_document_lock_rejects_fixture_symlink_even_when_target_is_approved(tmp_path, monkeypatch):
    _copy_contract_tree(tmp_path, monkeypatch)
    fixture = _fixture_path(tmp_path)
    approved = tmp_path / "approved-contract.json"
    approved.write_bytes(fixture.read_bytes())
    fixture.unlink()
    fixture.symlink_to(approved)

    errors = GUARDS.check_codemagic_release_publishers()

    assert any("ordinary regular file" in error and "v1.json" in error for error in errors), errors


@pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="platform does not support FIFOs")
def test_global_document_lock_rejects_nonregular_codemagic_file(tmp_path, monkeypatch):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    codemagic.unlink()
    os.mkfifo(codemagic)

    errors = GUARDS.check_codemagic_release_publishers()

    assert any("ordinary regular file" in error and "codemagic.yaml" in error for error in errors), errors


@pytest.mark.parametrize(
    ("target", "expected"),
    (
        ("codemagic", "codemagic.yaml must be valid UTF-8"),
        ("fixture", "Codemagic workflow contract fixture must be valid UTF-8"),
    ),
)
def test_global_document_lock_rejects_invalid_utf8_security_bound_inputs(tmp_path, monkeypatch, target, expected):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    path = codemagic if target == "codemagic" else _fixture_path(tmp_path)
    path.write_bytes(b"\xff")

    errors = GUARDS.check_codemagic_release_publishers()

    assert any(expected in error for error in errors), errors


@pytest.mark.parametrize("target", ("codemagic", "fixture"))
@pytest.mark.parametrize("seam", ("after-open", "after-final-fstat"))
def test_global_document_lock_rejects_security_bound_path_replacement_at_every_read_seam(
    tmp_path, monkeypatch, target, seam
):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    path = codemagic if target == "codemagic" else _fixture_path(tmp_path)
    original_open = GUARDS.os.open
    original_fstat = GUARDS.os.fstat
    protected_fds: set[int] = set()
    fstat_calls: dict[int, int] = {}
    replacements = 0

    def replace_path() -> None:
        nonlocal replacements
        replacement = tmp_path / f"replacement-{target}"
        replacement.write_text("workflows: {}\n", encoding="utf-8")
        os.replace(replacement, path)
        replacements += 1

    def replace_path_after_open(opened_path, flags, *args):
        fd = original_open(opened_path, flags, *args)
        if Path(opened_path) == path:
            protected_fds.add(fd)
            if seam == "after-open":
                replace_path()
        return fd

    def replace_path_after_final_fstat(fd):
        result = original_fstat(fd)
        if fd in protected_fds:
            fstat_calls[fd] = fstat_calls.get(fd, 0) + 1
            if seam == "after-final-fstat" and fstat_calls[fd] == 2:
                replace_path()
        return result

    monkeypatch.setattr(GUARDS.os, "open", replace_path_after_open)
    monkeypatch.setattr(GUARDS.os, "fstat", replace_path_after_final_fstat)

    errors = GUARDS.check_codemagic_release_publishers()

    assert replacements == 1
    assert any("changed while being read" in error and str(path) in error for error in errors), errors


def test_global_document_lock_rejects_path_replacement_without_o_nofollow(tmp_path, monkeypatch):
    codemagic = _copy_contract_tree(tmp_path, monkeypatch)
    original_open = GUARDS.os.open

    def replace_path_after_open(path, flags, *args):
        fd = original_open(path, flags, *args)
        if Path(path) == codemagic:
            replacement = tmp_path / "replacement-codemagic.yaml"
            replacement.write_text("workflows: {}\n", encoding="utf-8")
            os.replace(replacement, codemagic)
        return fd

    monkeypatch.setattr(GUARDS.os, "O_NOFOLLOW", 0)
    monkeypatch.setattr(GUARDS.os, "open", replace_path_after_open)

    errors = GUARDS.check_codemagic_release_publishers()

    assert any("changed while being read" in error and "codemagic.yaml" in error for error in errors), errors


def _write_contract(tmp_path: Path, contract: object) -> None:
    _fixture_path(tmp_path).write_text(json.dumps(contract, separators=(",", ":")), encoding="utf-8")


def _fixture_contract(tmp_path: Path) -> dict[str, object]:
    contract = json.loads(_fixture_path(tmp_path).read_text(encoding="utf-8"))
    assert isinstance(contract, dict)
    return contract


def test_global_document_lock_rejects_duplicate_fixture_keys_at_every_object_level(tmp_path, monkeypatch):
    _copy_contract_tree(tmp_path, monkeypatch)
    contract = _fixture_contract(tmp_path)
    raw_digest = contract["codemagic_raw_sha256"]
    duplicate_top_level = json.dumps(contract, separators=(",", ":")).replace(
        f'"codemagic_raw_sha256":"{raw_digest}"',
        f'"codemagic_raw_sha256":"{raw_digest}","codemagic_raw_sha256":"{raw_digest}"',
        1,
    )
    _fixture_path(tmp_path).write_text(duplicate_top_level, encoding="utf-8")
    errors = GUARDS.check_codemagic_release_publishers()
    assert any("duplicate key" in error for error in errors), errors

    _copy_contract_tree(tmp_path, monkeypatch)
    contract = _fixture_contract(tmp_path)
    preview = contract["omi-desktop-swift-preview"]
    assert isinstance(preview, dict)
    preview_digest = preview["semantic_sha256"]
    duplicate_nested = json.dumps(contract, separators=(",", ":")).replace(
        f'"semantic_sha256":"{preview_digest}"',
        f'"semantic_sha256":"{preview_digest}","semantic_sha256":"{preview_digest}"',
        1,
    )
    _fixture_path(tmp_path).write_text(duplicate_nested, encoding="utf-8")
    errors = GUARDS.check_codemagic_release_publishers()
    assert any("duplicate key" in error for error in errors), errors


@pytest.mark.parametrize(
    ("mutate", "expected"),
    (
        (lambda contract: contract.update({"unexpected": "value"}), "top-level keys"),
        (
            lambda contract: contract["omi-desktop-swift-preview"].update({"unexpected": "value"}),
            "omi-desktop-swift-preview keys",
        ),
        (lambda contract: contract.pop("codemagic_raw_sha256"), "top-level keys"),
        (
            lambda contract: contract["omi-desktop-swift-release"].pop("publication_script"),
            "omi-desktop-swift-release keys",
        ),
        (
            lambda contract: contract["omi-desktop-swift-preview"].update({"semantic_sha256": []}),
            "semantic_sha256 must be a lowercase SHA-256 digest",
        ),
        (
            lambda contract: contract["omi-desktop-swift-preview"].update({"semantic_sha256": True}),
            "semantic_sha256 must be a lowercase SHA-256 digest",
        ),
        (lambda contract: contract.update({"schema_version": True}), "schema_version must be the exact integer 1"),
        (lambda contract: contract.update({"schema_version": 2}), "schema_version must be the exact integer 1"),
        (lambda contract: contract.pop("schema_version"), "top-level keys"),
        (
            lambda contract: contract.update({"codemagic_raw_sha256": "A" * 64}),
            "codemagic_raw_sha256 must be a lowercase SHA-256 digest",
        ),
        (
            lambda contract: contract.update({"codemagic_raw_sha256": "a" * 63}),
            "codemagic_raw_sha256 must be a lowercase SHA-256 digest",
        ),
        (
            lambda contract: contract.update({"codemagic_raw_sha256": "g" * 64}),
            "codemagic_raw_sha256 must be a lowercase SHA-256 digest",
        ),
        (
            lambda contract: contract["omi-desktop-swift-release"].update({"publication_script": True}),
            "publication_script must be an exact string",
        ),
        (
            lambda contract: contract["omi-desktop-swift-release"].update({"publication_script_sha256": "0" * 64}),
            "publication script digest does not match",
        ),
    ),
    ids=(
        "unknown-top-level",
        "unknown-nested",
        "missing-top-level",
        "missing-nested",
        "wrong-type",
        "bool",
        "schema-version-bool",
        "schema-version-unsupported",
        "schema-version-missing",
        "uppercase-digest",
        "short-digest",
        "nonhex-digest",
        "publication-script-bool",
        "publication-script-digest-mismatch",
    ),
)
def test_global_document_lock_rejects_untrusted_fixture_shape(tmp_path, monkeypatch, mutate, expected):
    _copy_contract_tree(tmp_path, monkeypatch)
    contract = _fixture_contract(tmp_path)
    mutate(contract)
    _write_contract(tmp_path, contract)

    errors = GUARDS.check_codemagic_release_publishers()

    assert any(expected in error for error in errors), errors


def test_global_document_lock_rejects_malformed_fixture_json(tmp_path, monkeypatch):
    _copy_contract_tree(tmp_path, monkeypatch)
    _fixture_path(tmp_path).write_text('{"codemagic_raw_sha256":', encoding="utf-8")

    errors = GUARDS.check_codemagic_release_publishers()

    assert any("invalid JSON" in error for error in errors), errors


def test_firmware_release_metadata_uses_resolved_bash_and_converted_output(tmp_path, monkeypatch):
    script = tmp_path / "omi/firmware/scripts/ci/make-release-body.sh"
    script.parent.mkdir(parents=True)
    script.write_text("# fixture\n", encoding="utf-8")
    commands = []

    def fake_run(command, **kwargs):
        commands.append(command)
        Path(kwargs["env"]["OUT"]).write_text(
            "<!-- KEY_VALUE_START\n"
            "release_firmware_version:9.8.7\n"
            "minimum_firmware_required:3.0.6\n"
            "minimum_app_version:1.0.74\n"
            "minimum_app_version_code:438\n"
            "is_legacy_secure_dfu:False\n"
            "ota_update_steps:battery,internet\n"
            "KEY_VALUE_END -->\n",
            encoding="utf-8",
        )
        return subprocess.CompletedProcess(command, 0, stdout="", stderr="")

    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    monkeypatch.setattr(GUARDS, "bash_executable", lambda: "git-bash.exe")
    monkeypatch.setattr(GUARDS, "bash_path", lambda value, _bash: str(value))
    monkeypatch.setattr(GUARDS.subprocess, "run", fake_run)

    assert GUARDS.check_firmware_release_metadata() == []
    assert commands == [["git-bash.exe", str(script)]]


def test_firmware_release_metadata_reports_status_when_shell_has_no_output(tmp_path, monkeypatch):
    script = tmp_path / "omi/firmware/scripts/ci/make-release-body.sh"
    script.parent.mkdir(parents=True)
    script.write_text("# fixture\n", encoding="utf-8")

    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)
    monkeypatch.setattr(GUARDS, "bash_executable", lambda: "git-bash.exe")
    monkeypatch.setattr(GUARDS, "bash_path", lambda value, _bash: str(value))
    monkeypatch.setattr(
        GUARDS.subprocess,
        "run",
        lambda command, **_kwargs: subprocess.CompletedProcess(command, 9, stdout=None, stderr=None),
    )

    assert GUARDS.check_firmware_release_metadata() == ["firmware release body smoke failed: exit 9"]


def test_desktop_promotion_guard_rejects_reintroduced_qualification_trigger(tmp_path, monkeypatch):
    workflows = tmp_path / ".github/workflows"
    workflows.mkdir(parents=True)
    for name in ("desktop_promote_beta.yml", "desktop_recover_beta.yml"):
        shutil.copy2(REPO_ROOT / ".github/workflows" / name, workflows / name)
    monkeypatch.setattr(GUARDS, "ROOT", tmp_path)

    assert GUARDS.check_desktop_promotion_independent_of_qualification() == []

    promotion = workflows / "desktop_promote_beta.yml"
    original = promotion.read_text(encoding="utf-8")
    promotion.write_text(original + "\nqualification_run_id: 1\n", encoding="utf-8")
    errors = GUARDS.check_desktop_promotion_independent_of_qualification()
    assert any("still depends on qualification" in error for error in errors), errors