from __future__ import annotations

import time
from collections.abc import Generator
from datetime import UTC, datetime
from decimal import Decimal

import pytest
from auth_helpers import install_legacy_header_auth_override
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

from app.api.deps import get_db
from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentAssetType
from app.db.base import Base
from app.main import create_app
from app.models.agent_asset import AgentAsset, AgentAssetTestRun, AgentAssetVersion
from app.models.agent_asset_release_telemetry import AgentAssetReleaseObservation
from app.models.audit_log import AuditLog
from app.models.financial_record import ExpenseClaim
from app.models.golden_case import GoldenCase
from app.services.agent_asset_release_guard import (
    AgentAssetReleaseGuardService,
    ReleaseEvaluationInput,
    ReleaseGuardPolicy,
)
from app.services.agent_asset_release_monitor import AgentAssetReleaseMonitor
from app.services.agent_asset_release_monitor_auth import build_release_monitor_signature
from app.services.agent_asset_release_telemetry import (
    AgentAssetReleaseTelemetryService,
    ReleaseObservationInput,
)
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
from app.services.agent_assets import AgentAssetService
from app.services.expense_claim_risk_rule_loader import _is_candidate_route
from app.services.expense_claims import ExpenseClaimService


def _session_factory() -> sessionmaker[Session]:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    return sessionmaker(bind=engine, autoflush=False, autocommit=False)


def _manifest(*, name: str, enabled: bool) -> dict:
    return {
        "schema_version": "2.0",
        "rule_code": "risk.release.runtime",
        "name": name,
        "description": name,
        "evaluator": "template_rule",
        "enabled": enabled,
        "applies_to": {
            "domains": ["expense"],
            "business_stages": ["reimbursement"],
            "expense_categories": ["travel"],
        },
        "template_key": "composite_rule_v1",
        "params": {
            "template_key": "composite_rule_v1",
            "field_keys": ["claim.reason"],
            "conditions": [
                {
                    "id": "missing_exception_reason",
                    "operator": "not_contains_any",
                    "fields": ["claim.reason"],
                    "keywords": ["专项审批"],
                }
            ],
            "hit_logic": {"all": ["missing_exception_reason"]},
            "message_template": name,
        },
        "outcomes": {
            "pass": {"action": "continue"},
            "fail": {"severity": "high", "action": "manual_review"},
        },
    }


def _seed_risk_release(
    db: Session,
    manager: AgentAssetRuleLibraryManager,
    *,
    with_golden: bool = True,
    with_report: bool = True,
) -> AgentAsset:
    base_file = "risk.release.runtime.json"
    candidate_file = "risk.release.runtime.v2.json"
    manager.write_rule_library_json(
        library=RISK_RULES_LIBRARY,
        file_name=base_file,
        payload=_manifest(name="基线规则", enabled=True),
    )
    manager.write_rule_library_json(
        library=RISK_RULES_LIBRARY,
        file_name=candidate_file,
        payload=_manifest(name="候选规则", enabled=False),
    )
    asset = AgentAsset(
        id="risk-release-runtime",
        tenant_id="tenant-a",
        scope="tenant",
        asset_type=AgentAssetType.RULE.value,
        code="risk.release.runtime",
        name="基线规则",
        description="基线规则",
        domain=AgentAssetDomain.EXPENSE.value,
        scenario_json=["差旅费"],
        owner="finance",
        status=AgentAssetStatus.ACTIVE.value,
        current_version="v1",
        published_version="v1",
        working_version="v2",
        config_json={
            "tenant_id": "tenant-a",
            "detail_mode": "json_risk",
            "enabled": True,
            "rule_library": RISK_RULES_LIBRARY,
            "rule_document": {"file_name": base_file},
            "revision_draft": {
                "version": "v2",
                "base_version": "v1",
                "generation_status": "completed",
                "rule_document": {"file_name": candidate_file},
            },
        },
    )
    db.add(asset)
    db.add_all(
        [
            AgentAssetVersion(
                asset_id=asset.id,
                tenant_id="tenant-a",
                scope="tenant",
                version=version,
                content=f"# {version}",
                content_type="markdown",
                created_by="pytest",
            )
            for version in ("v1", "v2")
        ]
    )
    if with_report:
        db.add(
            AgentAssetTestRun(
                asset_id=asset.id,
                tenant_id="tenant-a",
                scope="tenant",
                version="v2",
                test_type="report",
                status="passed",
                passed=True,
                summary="candidate report passed",
                created_by="pytest",
            )
        )
    if with_golden:
        db.add(
            GoldenCase(
                case_key="risk-release-runtime-hit",
                rule_code=asset.code,
                name="普通报销应命中",
                values_json={"claim.reason": "普通差旅报销"},
                expected_hit=True,
                expected_severity="high",
                status="active",
            )
        )
    db.commit()
    return asset


def _claim(claim_id: str) -> ExpenseClaim:
    return ExpenseClaim(
        id=claim_id,
        claim_no=f"RE-{claim_id}",
        employee_name="张三",
        department_name="研发部",
        expense_type="travel",
        reason="普通差旅报销",
        location="上海",
        amount=Decimal("1200"),
        currency="CNY",
        invoice_count=0,
        occurred_at=datetime(2026, 7, 16, tzinfo=UTC),
        status="draft",
        approval_stage="待提交",
        risk_flags_json=[],
    )


def _policy() -> ReleaseGuardPolicy:
    return ReleaseGuardPolicy(
        shadow_min_samples=1,
        canary_min_samples=1,
        max_error_rate=0.1,
        min_precision=0.9,
        max_precision_drop=0.05,
        canary_traffic_percent=50,
        recall_gate_enabled=False,
    )


def test_risk_rule_runtime_consumes_shadow_canary_active_and_rollback(
    tmp_path,
    monkeypatch,
) -> None:
    factory = _session_factory()
    manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
    with factory() as db:
        asset = _seed_risk_release(db, manager)
        from app.services import expense_claim_platform_risk

        monkeypatch.setattr(
            expense_claim_platform_risk,
            "AgentAssetRuleLibraryManager",
            lambda: manager,
        )
        guard = AgentAssetReleaseGuardService(db, rule_library_manager=manager)
        guard.start_shadow(
            asset.id,
            "v2",
            actor="manager",
            tenant_id="tenant-a",
            policy=_policy(),
        )

        # 启动后改写候选文件，运行时仍使用启动时冻结的可信快照。
        manager.write_rule_library_json(
            library=RISK_RULES_LIBRARY,
            file_name="risk.release.runtime.v2.json",
            payload=_manifest(name="被篡改但不应生效", enabled=True),
        )
        shadow = ExpenseClaimService(db).evaluate_platform_risk_rules(
            _claim("shadow-claim"),
            tenant_id="tenant-a",
            business_stage="reimbursement",
        )
        assert [item["label"] for item in shadow["flags"]] == ["基线规则"]
        assert shadow["shadow_evaluations"] == [
            {
                "asset_id": asset.id,
                "rule_code": asset.code,
                "rule_version": "v2",
                "release_stage": "shadow",
                "hit": True,
                "severity": "high",
            }
        ]
        shadow_sample = db.query(AgentAssetReleaseObservation).filter_by(
            tenant_id="tenant-a",
            asset_id=asset.id,
            stage="shadow",
            version="v2",
        ).one()
        assert shadow_sample.candidate_hit is True
        assert shadow_sample.baseline_hit is True

        guard.record_evaluation(
            asset.id,
            ReleaseEvaluationInput(total=1, failure_count=0, precision=0.99),
            actor="monitor",
            tenant_id="tenant-a",
        )
        guard.promote(asset.id, actor="manager", tenant_id="tenant-a")
        canary_key = next(
            f"canary-{index}"
            for index in range(1000)
            if _is_candidate_route(asset.id, f"canary-{index}", 50)
        )
        canary = ExpenseClaimService(db).evaluate_platform_risk_rules(
            _claim(canary_key),
            tenant_id="tenant-a",
            business_stage="reimbursement",
        )
        assert canary["flags"][0]["label"] == "候选规则"
        assert canary["flags"][0]["rule_version"] == "v2"
        assert canary["flags"][0]["release_stage"] == "canary"
        canary_sample = db.query(AgentAssetReleaseObservation).filter_by(
            tenant_id="tenant-a",
            asset_id=asset.id,
            stage="canary",
            version="v2",
        ).one()
        assert canary_sample.candidate_hit is True

        guard.record_evaluation(
            asset.id,
            ReleaseEvaluationInput(total=1, failure_count=0, precision=0.99),
            actor="monitor",
            tenant_id="tenant-a",
        )
        guard.promote(asset.id, actor="manager", tenant_id="tenant-a")
        active = ExpenseClaimService(db).evaluate_platform_risk_rules(
            _claim("active-claim"),
            tenant_id="tenant-a",
            business_stage="reimbursement",
        )
        assert active["flags"][0]["label"] == "候选规则"
        active_sample = db.query(AgentAssetReleaseObservation).filter_by(
            tenant_id="tenant-a",
            asset_id=asset.id,
            stage="active",
            version="v2",
        ).one()
        assert active_sample.candidate_hit is True
        assert db.get(AgentAsset, asset.id).published_version == "v2"

        result = guard.record_evaluation(
            asset.id,
            ReleaseEvaluationInput(
                total=10,
                failure_count=2,
                precision=0.7,
                baseline_precision=0.99,
            ),
            actor="monitor",
            tenant_id="tenant-a",
        )
        rolled_back = ExpenseClaimService(db).evaluate_platform_risk_rules(
            _claim("rolled-back-claim"),
            tenant_id="tenant-a",
            business_stage="reimbursement",
        )
        refreshed = db.get(AgentAsset, asset.id)
        assert result["release_stage"] == "rolled_back"
        assert rolled_back["flags"][0]["label"] == "基线规则"
        assert refreshed.published_version == "v1"
        assert refreshed.config_json["rule_document"]["file_name"] == ("risk.release.runtime.json")


def test_risk_rule_runtime_blocks_when_candidate_and_stable_snapshots_are_corrupt(
    tmp_path,
    monkeypatch,
) -> None:
    factory = _session_factory()
    manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
    with factory() as db:
        asset = _seed_risk_release(db, manager)
        from app.services import expense_claim_platform_risk

        monkeypatch.setattr(
            expense_claim_platform_risk,
            "AgentAssetRuleLibraryManager",
            lambda: manager,
        )
        guard = AgentAssetReleaseGuardService(db, rule_library_manager=manager)
        guard.start_shadow(
            asset.id,
            "v2",
            actor="manager",
            tenant_id="tenant-a",
            policy=_policy(),
        )

        refreshed = db.get(AgentAsset, asset.id)
        config = dict(refreshed.config_json or {})
        state = dict(config["release_guard"])
        state["artifacts"] = {
            version: {**dict(artifact), "sha256": "0" * 64}
            for version, artifact in dict(state["artifacts"]).items()
        }
        config["release_guard"] = state
        refreshed.config_json = config
        db.add(refreshed)
        db.commit()

        review = ExpenseClaimService(db).evaluate_platform_risk_rules(
            _claim("corrupt-release"),
            tenant_id="tenant-a",
            business_stage="reimbursement",
        )

        assert len(review["flags"]) == 1
        assert review["flags"][0]["rule_code"] == asset.code
        assert review["flags"][0]["severity"] == "critical"
        assert review["flags"][0]["action"] == "block"
        assert review["flags"][0]["evidence"]["failed_version"] == "v1"
        assert review["blocking_reasons"]

        result = AgentAssetReleaseMonitor(db).evaluate_current(
            tenant_id="tenant-a",
            asset_id=asset.id,
            actor="artifact-integrity-monitor",
        )
        assert result["status"] == "failed"
        assert result["rolled_back"] is True
        assert result["metrics"]["runtime_failure_count"] == 1


def test_risk_release_fails_closed_without_report_or_golden(tmp_path) -> None:
    manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
    with _session_factory()() as db:
        asset = _seed_risk_release(db, manager, with_report=False)
        with pytest.raises(PermissionError, match="测试报告"):
            AgentAssetReleaseGuardService(db, rule_library_manager=manager).start_shadow(
                asset.id,
                "v2",
                actor="manager",
                tenant_id="tenant-a",
            )

    manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules-2")
    with _session_factory()() as db:
        asset = _seed_risk_release(db, manager, with_golden=False)
        with pytest.raises(PermissionError, match="golden case"):
            AgentAssetReleaseGuardService(db, rule_library_manager=manager).start_shadow(
                asset.id,
                "v2",
                actor="manager",
                tenant_id="tenant-a",
            )
        failed = db.query(AgentAssetTestRun).filter_by(test_type="golden").one()
        assert failed.passed is False


def _build_http_client() -> tuple[TestClient, sessionmaker[Session]]:
    factory = _session_factory()
    app = create_app()
    install_legacy_header_auth_override(app)

    def override_db() -> Generator[Session, None, None]:
        with factory() as db:
            yield db

    app.dependency_overrides[get_db] = override_db
    return TestClient(app), factory


def _seed_task_release(factory: sessionmaker[Session]) -> None:
    with factory() as db:
        asset = AgentAsset(
            id="task-release-api",
            tenant_id="tenant-a",
            scope="tenant",
            asset_type=AgentAssetType.TASK.value,
            code="task.release.api",
            name="发布 API 测试",
            domain=AgentAssetDomain.EXPENSE.value,
            owner="manager",
            status=AgentAssetStatus.ACTIVE.value,
            current_version="v1",
            published_version="v1",
            working_version="v2",
            config_json={"tenant_id": "tenant-a"},
        )
        db.add(asset)
        db.add_all(
            [
                AgentAssetVersion(
                    asset_id=asset.id,
                    tenant_id="tenant-a",
                    scope="tenant",
                    version=version,
                    content="{}",
                    content_type="json",
                    created_by="pytest",
                )
                for version in ("v1", "v2")
            ]
        )
        db.commit()


def _seed_global_task_release(factory: sessionmaker[Session]) -> None:
    with factory() as db:
        asset = AgentAsset(
            id="global-task-release-api",
            tenant_id="platform",
            scope="platform",
            asset_type=AgentAssetType.TASK.value,
            code="task.release.global",
            name="平台共享发布 API 测试",
            domain=AgentAssetDomain.EXPENSE.value,
            owner="platform",
            status=AgentAssetStatus.ACTIVE.value,
            current_version="v1",
            published_version="v1",
            working_version="v2",
            config_json={},
        )
        db.add(asset)
        db.add_all(
            [
                AgentAssetVersion(
                    asset_id=asset.id,
                    tenant_id="platform",
                    scope="platform",
                    version=version,
                    content="{}",
                    content_type="json",
                    created_by="pytest",
                )
                for version in ("v1", "v2")
            ]
        )
        db.commit()


def _seed_monitor_risk_release(
    factory: sessionmaker[Session],
    *,
    asset_id: str = "risk-monitor-api",
    tenant_id: str = "tenant-a",
    min_precision: float = 0.9,
) -> None:
    with factory() as db:
        previous_config = {
            "tenant_id": tenant_id,
            "detail_mode": "json_risk",
            "enabled": True,
        }
        db.add(
            AgentAsset(
                id=asset_id,
                tenant_id=tenant_id,
                scope="tenant",
                asset_type=AgentAssetType.RULE.value,
                code=f"risk.{asset_id}",
                name="真实发布监控 API 测试",
                domain=AgentAssetDomain.EXPENSE.value,
                owner="manager",
                status=AgentAssetStatus.ACTIVE.value,
                current_version="v1",
                published_version="v1",
                working_version="v2",
                config_json={
                    **previous_config,
                    "release_guard": {
                        "release_id": f"release-{asset_id}",
                        "stage": "shadow",
                        "candidate_version": "v2",
                        "previous_version": "v1",
                        "started_by": "username:manager",
                        "previous_config": previous_config,
                        "policy": {
                            "shadow_min_samples": 1,
                            "canary_min_samples": 1,
                            "max_error_rate": 1.0,
                            "min_precision": min_precision,
                            "max_precision_drop": 1.0,
                            "canary_traffic_percent": 10,
                        },
                        "history": [],
                    },
                },
            )
        )
        db.commit()


def _append_release_review_samples(
    factory: sessionmaker[Session],
    *,
    asset_id: str,
    labels: list[str],
    tenant_id: str = "tenant-a",
) -> dict:
    with factory() as db:
        asset = db.get(AgentAsset, asset_id)
        assert asset is not None
        state = dict((asset.config_json or {})["release_guard"])
        telemetry = AgentAssetReleaseTelemetryService(db)
        for index, label in enumerate(labels):
            observation = telemetry.record_observation(
                ReleaseObservationInput(
                    tenant_id=tenant_id,
                    asset_id=asset.id,
                    release_id=str(state["release_id"]),
                    stage=str(state["stage"]),  # type: ignore[arg-type]
                    version=str(state["candidate_version"]),
                    rule_code=asset.code,
                    source_key=f"{state['stage']}-sample-{index}",
                    candidate_hit=True,
                    baseline_hit=True,
                )
            )
            telemetry.record_review_label(
                tenant_id=tenant_id,
                observation_id=observation.id,
                label=label,  # type: ignore[arg-type]
                request_id=f"{state['release_id']}:{state['stage']}:review:{index}",
                actor_id="trusted-release-reviewer",
            )
        db.commit()
        return state


def _headers(*, role: str = "manager", tenant: str = "tenant-a") -> dict[str, str]:
    return {
        "x-auth-username": role,
        "x-auth-name": role,
        "x-auth-role-codes": role,
        "x-auth-tenant-id": tenant,
    }


_TEST_MONITOR_SECRET = "release-monitor-test-secret-32-bytes-minimum"


def _monitor_headers(
    *,
    payload: dict,
    state: dict,
    role: str = "manager",
    tenant: str = "tenant-a",
    asset_id: str = "task-release-api",
) -> dict[str, str]:
    timestamp = str(int(time.time()))
    return {
        **_headers(role=role, tenant=tenant),
        "x-release-monitor-timestamp": timestamp,
        "x-release-monitor-signature": build_release_monitor_signature(
            timestamp=timestamp,
            tenant_id=tenant,
            asset_id=asset_id,
            release_id=str(state.get("release_id") or ""),
            stage=str(state.get("stage") or ""),
            payload=payload,
            secret=_TEST_MONITOR_SECRET,
        ),
    }


def test_release_management_http_permissions_tenant_and_auto_rollback(monkeypatch) -> None:
    monkeypatch.setenv("AGENT_RELEASE_MONITOR_SECRET", _TEST_MONITOR_SECRET)
    client, factory = _build_http_client()
    _seed_task_release(factory)
    path = "/api/v1/agent-assets/task-release-api/release"

    assert client.get(path, headers=_headers(role="finance")).status_code == 403
    assert client.get(path, headers=_headers(tenant="tenant-b")).status_code == 404

    started = client.post(
        f"{path}/shadow",
        headers=_headers(),
        json={
            "candidate_version": "v2",
            "policy": {
                "shadow_min_samples": 1,
                "canary_min_samples": 1,
                "max_error_rate": 0.1,
                "min_precision": 0.9,
                "max_precision_drop": 0.05,
                "canary_traffic_percent": 10,
            },
        },
    )
    assert started.status_code == 200
    assert started.json()["stage"] == "shadow"
    release_state = started.json()

    unsigned = client.post(
        f"{path}/evaluations",
        headers=_headers(),
        json={},
    )
    assert unsigned.status_code == 401
    fabricated = {"total": 1, "failure_count": 0, "precision": 1.0}
    assert (
        client.post(
            f"{path}/evaluations",
            headers=_monitor_headers(payload=fabricated, state=release_state),
            json=fabricated,
        ).status_code
        == 422
    )

    _seed_monitor_risk_release(factory)
    risk_path = "/api/v1/agent-assets/risk-monitor-api/release"
    risk_state = _append_release_review_samples(
        factory,
        asset_id="risk-monitor-api",
        labels=["confirmed", "false_positive"],
    )
    trigger: dict = {}
    failed = client.post(
        f"{risk_path}/evaluations",
        headers=_monitor_headers(
            payload=trigger,
            state=risk_state,
            asset_id="risk-monitor-api",
        ),
        json=trigger,
    )
    assert failed.status_code == 200
    assert failed.json()["status"] == "failed"
    assert failed.json()["release_stage"] == "rolled_back"
    assert failed.json()["metrics"]["precision"] == 0.5
    plan = client.get(f"{risk_path}/serving-plan", headers=_headers())
    assert plan.json()["primary_version"] == "v1"

    with factory() as db:
        assert db.query(AuditLog).filter_by(resource_id="risk-monitor-api").count() >= 1


def test_release_review_queue_is_tenant_safe_and_requires_independent_reviewer() -> None:
    client, factory = _build_http_client()
    _seed_monitor_risk_release(factory, asset_id="risk-review-api", min_precision=0.5)
    with factory() as db:
        asset = db.get(AgentAsset, "risk-review-api")
        assert asset is not None
        state = dict((asset.config_json or {})["release_guard"])
        observation = AgentAssetReleaseTelemetryService(db).record_observation(
            ReleaseObservationInput(
                tenant_id="tenant-a",
                asset_id=asset.id,
                release_id=str(state["release_id"]),
                stage="shadow",
                version="v2",
                rule_code=asset.code,
                source_key="claim-review-api",
                candidate_hit=True,
                baseline_hit=True,
            )
        )
        db.commit()
        observation_id = observation.id

    path = "/api/v1/agent-assets/risk-review-api/release/review-queue"
    assert client.get(path, headers=_headers(tenant="tenant-b")).status_code == 404
    queued = client.get(path, headers=_headers())
    assert queued.status_code == 200
    assert queued.json()["pending_total"] == 1
    assert "source_fingerprint" not in queued.text
    item = queued.json()["items"][0]
    assert item["source_document_id"] == "claim-review-api"
    assert item["prediction_blinded"] is True
    assert "candidate_hit" not in item
    assert "baseline_hit" not in item

    self_review = client.post(
        f"{path}/{observation_id}/labels",
        headers={**_headers(), "x-request-id": "self-review"},
        json={"label": "confirmed"},
    )
    assert self_review.status_code == 400
    reviewed = client.post(
        f"{path}/{observation_id}/labels",
        headers={**_headers(role="admin"), "x-request-id": "independent-review"},
        json={"label": "confirmed"},
    )
    assert reviewed.status_code == 200
    assert reviewed.json()["monitor"]["status"] == "passed"
    assert client.get(path, headers=_headers(role="admin")).json()["pending_total"] == 0


def test_only_platform_admin_can_manage_global_release_asset() -> None:
    client, factory = _build_http_client()
    _seed_global_task_release(factory)
    path = "/api/v1/agent-assets/global-task-release-api/release"

    assert client.get(path, headers=_headers(role="manager")).status_code == 404
    assert client.get(path, headers=_headers(role="admin")).status_code == 200
    started = client.post(
        f"{path}/shadow",
        headers=_headers(role="admin"),
        json={"candidate_version": "v2"},
    )
    assert started.status_code == 200
    assert started.json()["stage"] == "shadow"


def test_existing_publish_http_cannot_bypass_shadow_and_canary(
    tmp_path,
    monkeypatch,
) -> None:
    monkeypatch.setenv("AGENT_RELEASE_MONITOR_SECRET", _TEST_MONITOR_SECRET)
    client, factory = _build_http_client()
    manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
    with factory() as db:
        _seed_risk_release(db, manager)

    original_init = AgentAssetService.__init__

    def patched_init(self, db, *, current_user=None) -> None:
        original_init(self, db, current_user=current_user)
        self.rule_library_manager = manager

    monkeypatch.setattr(AgentAssetService, "__init__", patched_init)
    asset_path = "/api/v1/agent-assets/risk-release-runtime"
    assert (
        client.post(
            f"{asset_path}/publish",
            headers=_headers(tenant="tenant-b"),
        ).status_code
        == 404
    )
    published = client.post(f"{asset_path}/publish", headers=_headers())

    assert published.status_code == 200
    assert published.json()["published_version"] == "v1"
    assert published.json()["current_version"] == "v1"
    assert published.json()["config_json"]["release_guard"]["stage"] == "shadow"
    assert client.post(f"{asset_path}/activate", headers=_headers()).status_code == 400
    assert (
        client.post(
            f"{asset_path}/risk-rule-enabled",
            headers=_headers(),
            json={"enabled": True},
        ).status_code
        == 400
    )
    assert (
        client.patch(
            asset_path,
            headers=_headers(),
            json={"published_version": "v2"},
        ).status_code
        == 400
    )

    release_path = f"{asset_path}/release"
    for total, expected_stage in ((20, "canary"), (100, "active")):
        release_state = client.get(release_path, headers=_headers()).json()
        _append_release_review_samples(
            factory,
            asset_id="risk-release-runtime",
            labels=["confirmed"] * total,
        )
        payload: dict = {}
        evaluated = client.post(
            f"{release_path}/evaluations",
            headers=_monitor_headers(
                payload=payload,
                state=release_state,
                asset_id="risk-release-runtime",
            ),
            json=payload,
        )
        assert evaluated.status_code == 200
        promoted = client.post(f"{release_path}/promote", headers=_headers())
        assert promoted.status_code == 200
        assert promoted.json()["stage"] == expected_stage

    with factory() as db:
        active = db.get(AgentAsset, "risk-release-runtime")
        assert active is not None
        assert active.published_version == "v2"
