"""Adversarial publication, rollback, and replay proofs for V15 authority."""

from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path
from unittest.mock import patch

import pytest
from pydantic import JsonValue, TypeAdapter

from gateway.platforms.task26_candidate_authority import (
    verify_candidate_authority,
)
from gateway.platforms.task26_runtime_authority import (
    FileCandidateAuthoritySource,
    append_external_authority,
    build_runtime_authority_pin,
    load_task26_production_authority,
    publish_runtime_authority_pin,
    recover_external_authority,
)
from checkin_cli.customer_coaching import load_customer_registry
from scripts.dualcoach_v111_disposable_fixture import write_runtime_authority
from scripts.nutricoach_v150_concrete_host import ConcreteLiveHost
from scripts.nutricoach_v150_sealed_controller import (
    APPROVAL_PHRASE,
    DisposableService,
    SealedControllerError,
    execute_disposable,
    rollback_committed,
)
from scripts.nutricoach_v150_sealed_authority import atomic_write
from scripts.nutricoach_v150_sealed_target import HostError
from tests.test_nutricoach_v150_sealed_controller import target_fixture

_PREDECESSOR = "a" * 64
_SUCCESSOR = "b" * 64
_PASS = hashlib.sha256(b"dualcoach-v111-disposable-pass").hexdigest()
_OBJECT = TypeAdapter(dict[str, JsonValue])


def _authority_host(
    tmp_path: Path,
    *,
    fault_stage: str = "install_exact_wheels",
    fault: BaseException | None = None,
) -> tuple[ConcreteLiveHost, Path, Path, Path]:
    external = tmp_path / "external-authority"
    pin, candidate = write_runtime_authority(external, _PREDECESSOR)
    _ = target_fixture(tmp_path)
    host = ConcreteLiveHost.disposable(
        tmp_path,
        DisposableService(),
        fault=fault,
        fault_stage=fault_stage,
    )
    _ = host.paths.dropin.write_text(
        "\n".join((
            f"LoadCredential=task26-authority-pin.json:{pin}",
            f"LoadCredential=task26-candidate-digest:{candidate}",
            "",
        )),
        encoding="utf-8",
    )
    return host, external, pin, candidate


def _load(
    host: ConcreteLiveHost,
    pin: Path,
    candidate: Path,
) -> tuple[FileCandidateAuthoritySource, str]:
    with patch.dict(
        os.environ,
        {
            "TASK26_AUTHORITY_PIN": str(pin),
            "TASK26_CANDIDATE_DIGEST_FILE": str(candidate),
        },
        clear=False,
    ):
        return load_task26_production_authority(
            profile_root=host.paths.profile,
            package_root=host.paths.execution_root,
        )


def test_existing_private_lock_load_is_read_only(tmp_path: Path) -> None:
    host, external, pin, candidate = _authority_host(tmp_path)
    lock = external / "runtime-authority.lock"
    lock.chmod(0o400)
    source, authorized = _load(host, pin, candidate)
    assert source.lock_path.stat().st_mode & 0o777 == 0o400
    assert authorized == _PREDECESSOR


def test_recovery_manifest_rejects_weekly_authority_path_substitution(
    tmp_path: Path,
) -> None:
    # Given
    host, _external, _pin, _candidate = _authority_host(tmp_path)
    host.capture_preflight()
    host.write_recovery_manifest()
    path = host.paths.execution_root / "recovery-manifest.json"
    document = _OBJECT.validate_json(path.read_bytes())
    document["weekly_authority"] = str(
        host.paths.profile / "data/weekly-operations-authority-attacker"
    )
    path.chmod(0o600)
    _ = path.write_text(json.dumps(document), encoding="utf-8")

    # When / Then
    with pytest.raises(HostError, match="recovery_manifest_paths"):
        host.load_recovery_manifest()


def test_recovery_manifest_rejects_created_file_omission(
    tmp_path: Path,
) -> None:
    # Given
    host, _external, _pin, _candidate = _authority_host(tmp_path)
    registry = load_customer_registry(host.paths.registry, host.paths.profile)
    runtime = next(item for item in registry.customers if item.spec.enabled)
    events = runtime.wizard_root / "events.jsonl"
    events.unlink(missing_ok=True)
    host.capture_preflight()
    host.write_recovery_manifest()
    path = host.paths.execution_root / "recovery-manifest.json"
    document = _OBJECT.validate_json(path.read_bytes())
    created = document.get("created_files")
    assert isinstance(created, list)
    assert created
    document["created_files"] = created[1:]
    path.chmod(0o600)
    _ = path.write_text(json.dumps(document), encoding="utf-8")

    # When / Then
    with pytest.raises(HostError, match="recovery_manifest_paths"):
        host.load_recovery_manifest()


def test_stale_pin_is_rejected_by_production_loader(tmp_path: Path) -> None:
    host, external, pin, candidate = _authority_host(tmp_path)
    _ = append_external_authority(
        external,
        source_id="dualcoach-v111-disposable",
        candidate_digest=_SUCCESSOR,
        action="qualify",
        historical_pass_digest=_PASS,
        reason="stale pin boundary",
    )
    _ = candidate.write_text(_SUCCESSOR + "\n", encoding="utf-8")
    candidate.chmod(0o600)

    with pytest.raises(ValueError, match="pin is stale"):
        _ = _load(host, pin, candidate)


@pytest.mark.parametrize(
    "interrupted_name",
    ["registry.json", "qualification-ledger.json"],
)
def test_paired_publication_recovers_each_document_boundary(
    tmp_path: Path,
    interrupted_name: str,
) -> None:
    host, external, pin, candidate = _authority_host(tmp_path)
    real_replace = os.replace
    interrupted = False

    def interrupt_once(source: str | Path, destination: str | Path) -> None:
        nonlocal interrupted
        if Path(destination).name == interrupted_name and not interrupted:
            interrupted = True
            raise OSError(f"interrupted:{interrupted_name}")
        real_replace(source, destination)

    with (
        patch(
            "gateway.platforms.task26_candidate_authority.os.replace",
            side_effect=interrupt_once,
        ),
        pytest.raises(OSError, match="interrupted"),
    ):
        _ = append_external_authority(
            external,
            source_id="dualcoach-v111-disposable",
            candidate_digest=_SUCCESSOR,
            action="qualify",
            historical_pass_digest=_PASS,
            reason="paired interruption recovery",
        )

    assert (external / "runtime-authority-transition.json").is_file()
    recovered = recover_external_authority(external)
    assert recovered is not None
    assert recovered["current_qualified_candidate"] == _SUCCESSOR
    publish_runtime_authority_pin(pin, build_runtime_authority_pin(external))
    _ = candidate.write_text(_SUCCESSOR + "\n", encoding="utf-8")
    candidate.chmod(0o600)
    _source, loaded = _load(host, pin, candidate)
    assert loaded == _SUCCESSOR
    assert not (external / "runtime-authority-transition.json").exists()


@pytest.mark.parametrize(
    "credential_name",
    ["task26-authority-pin.json", "task26-candidate-digest"],
)
def test_credential_publication_failure_rolls_back_authority(
    tmp_path: Path,
    credential_name: str,
) -> None:
    host, _external, pin, candidate = _authority_host(tmp_path)
    real_atomic_write = atomic_write
    interrupted = False

    def interrupt_once(
        path: Path,
        payload: bytes,
        mode: int = 0o600,
    ) -> None:
        nonlocal interrupted
        if path.name == credential_name and not interrupted:
            interrupted = True
            raise OSError(f"interrupted:{credential_name}")
        real_atomic_write(path, payload, mode)

    with (
        patch(
            "scripts.nutricoach_v150_host_operations.atomic_write",
            side_effect=interrupt_once,
        ),
        pytest.raises(OSError, match="interrupted"),
    ):
        _ = execute_disposable(APPROVAL_PHRASE, tmp_path, host)

    source, loaded = _load(host, pin, candidate)
    assert loaded == _PREDECESSOR
    with source.authorize(loaded, "activation") as snapshot:
        assert snapshot["event_count"] == 4
    assert host.service.running
    assert not host.successor_root.exists()


def test_precommit_failure_restores_external_authority(tmp_path: Path) -> None:
    host, _external, pin, candidate = _authority_host(
        tmp_path,
        fault=HostError("precommit"),
        fault_stage="switch_unit_dropin",
    )

    with pytest.raises(HostError, match="precommit"):
        _ = execute_disposable(APPROVAL_PHRASE, tmp_path, host)

    source, loaded = _load(host, pin, candidate)
    assert loaded == _PREDECESSOR
    with source.authorize(loaded, "activation") as snapshot:
        assert snapshot["event_count"] == 4


def test_postcommit_manual_qa_rollback_restores_authority(tmp_path: Path) -> None:
    host, _external, pin, candidate = _authority_host(tmp_path)
    _ = execute_disposable(APPROVAL_PHRASE, tmp_path, host)

    rollback_committed(host, "Telegram startup rejected successor")

    source, loaded = _load(host, pin, candidate)
    assert loaded == _PREDECESSOR
    with source.authorize(loaded, "activation") as snapshot:
        assert snapshot["event_count"] == 4
    assert host.service.running
    assert not host.successor_root.exists()


def test_consumed_replay_does_not_mutate_authority(tmp_path: Path) -> None:
    host, external, _pin, _candidate = _authority_host(tmp_path)
    _ = execute_disposable(APPROVAL_PHRASE, tmp_path, host)
    registry = external / "candidate-authority/registry.json"
    ledger = external / "candidate-authority/qualification-ledger.json"
    before = registry.read_bytes(), ledger.read_bytes()

    with pytest.raises(SealedControllerError, match="already_used"):
        _ = execute_disposable(APPROVAL_PHRASE, tmp_path, host)

    assert (registry.read_bytes(), ledger.read_bytes()) == before


def test_revoked_candidate_is_rejected_by_production_loader(tmp_path: Path) -> None:
    host, external, pin, candidate = _authority_host(tmp_path)
    _ = append_external_authority(
        external,
        source_id="dualcoach-v111-disposable",
        candidate_digest=_PREDECESSOR,
        action="revoke",
        historical_pass_digest=_PASS,
        reason="revoked candidate negative",
    )
    publish_runtime_authority_pin(pin, build_runtime_authority_pin(external))

    with pytest.raises(ValueError, match="not current or was revoked"):
        _ = _load(host, pin, candidate)
    with pytest.raises(ValueError, match="candidate digest is revoked"):
        _ = verify_candidate_authority(external, _PREDECESSOR)
