#!/usr/bin/env python3
"""Controlled public smoke for extension ConfigCASSave preparation.

The smoke uses no storage coordinates.  It creates one extension saved-state
copy, verifies readback, rolls it back by opaque receipt, then proves that the
same public selector is immediately ready for another prepare.
"""

from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def rpc(base_url: str, method: str, payload: dict, timeout: float) -> dict:
    request = Request(
        base_url.rstrip("/") + "/rpc",
        data=json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8"),
        method="POST",
        headers={"Content-Type": "application/json; charset=utf-8"},
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            result = json.loads(response.read().decode("utf-8"))
    except (HTTPError, URLError) as exc:
        raise AssertionError(f"{method} transport failure: {exc}") from exc
    if not isinstance(result, dict):
        raise AssertionError(f"{method} returned a non-object response")
    return result


def require(condition: bool, message: str) -> None:
    if not condition:
        raise AssertionError(message)


def main() -> int:
    parser = argparse.ArgumentParser(description="Public extension saved-state prepare/rollback smoke.")
    parser.add_argument("--base-url", default="http://docker.cin.su:8011")
    parser.add_argument("--base-id", default="upo_test")
    parser.add_argument("--extension", default="фс_ДоработкиОбщее")
    parser.add_argument("--ref", default="Catalog.Номенклатура")
    parser.add_argument("--timeout", type=float, default=120.0)
    parser.add_argument("--report", type=Path)
    parser.add_argument("--apply", action="store_true", help="Perform the controlled SQL prepare and rollback.")
    args = parser.parse_args()
    report: dict = {
        "schema": "onec_extension_saved_state_prepare_smoke.v1",
        "base_url": args.base_url,
        "base_id": args.base_id,
        "extension": args.extension,
        "ref": args.ref,
        "status": "pending",
        "passed": False,
    }
    target = {
        "base_id": args.base_id,
        "extension": args.extension,
        "ref": args.ref,
        "layer": "extension_saved_state",
    }
    try:
        initial = rpc(args.base_url, "metadata.saved_state.prepare", target | {"mode": "plan"}, args.timeout)
        report["initial_plan"] = {"status": initial.get("status"), "counts": initial.get("counts")}
        require(initial.get("status") == "plan_ready", f"initial plan must be plan_ready, got {initial.get('status')}")
        if not args.apply:
            report["status"] = "plan_ready"
            report["passed"] = True
        else:
            applied = rpc(
                args.base_url,
                "metadata.saved_state.prepare",
                target | {"mode": "apply_and_verify", "allow_sql_saved_state_prepare": True},
                args.timeout,
            )
            report["prepare"] = {"status": applied.get("status"), "counts": applied.get("counts"), "verification": applied.get("verification")}
            require(applied.get("status") == "verified" and applied.get("applied") is True, "prepare must be verified")
            receipt_id = str(applied.get("prepare_receipt_id") or "")
            require(receipt_id, "prepare response must include an opaque receipt")
            rolled_back = rpc(
                args.base_url,
                "metadata.saved_state.ensure.rollback",
                {"base_id": args.base_id, "prepare_receipt_id": receipt_id, "allow_sql_saved_state_rollback": True},
                args.timeout,
            )
            report["rollback"] = {"status": rolled_back.get("status"), "counts": rolled_back.get("counts")}
            require(rolled_back.get("status") == "rolled_back" and rolled_back.get("applied") is True, "prepare rollback must succeed")
            started = time.monotonic()
            final_plan = rpc(args.base_url, "metadata.saved_state.prepare", target | {"mode": "plan"}, args.timeout)
            elapsed_ms = round((time.monotonic() - started) * 1000, 1)
            report["post_rollback_plan"] = {"status": final_plan.get("status"), "counts": final_plan.get("counts"), "elapsed_ms": elapsed_ms}
            require(final_plan.get("status") == "plan_ready", f"post-rollback plan must be plan_ready, got {final_plan.get('status')}")
            require(int((final_plan.get("counts") or {}).get("existing_saved_records") or 0) == 0, "rollback must leave no saved records")
            report["status"] = "verified_and_rolled_back"
            report["passed"] = True
    except Exception as exc:
        report["status"] = "failed"
        report["error"] = str(exc)
    if args.report:
        args.report.parent.mkdir(parents=True, exist_ok=True)
        args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 0 if report["passed"] else 1


if __name__ == "__main__":
    sys.exit(main())
