#
#   Imandra Inc.
#
#   speclogician/utils/testing.py
#

from .__init__ import JSONObject, JSONValue

import json
from typing import cast
from click.testing import Result

# -----------------------
# JSON parsing + narrowing helpers
# -----------------------

def parse_json_result(res : Result) -> JSONObject:
    """
    Parse JSON from CLI output.

    If stdout is empty (Typer/Pydantic failed before we could emit JSON),
    return a synthetic JSON error object so tests can assert consistently.
    """
    out = (res.stdout or "").strip()

    if not out:
        # Pre-command failure (argument parsing / Pydantic validation / Typer)
        # Normalize into the same "result envelope" your CLI emits.
        exc_s = ""
        if res.exception is not None:
            exc_s = str(res.exception)
        elif res.output:
            exc_s = str(res.output)

        return cast(
            JSONObject,
            {
                "ok": False,
                "stage": "cli_parse",
                "error": exc_s,
            },
        )

    try:
        data = json.loads(out)
    except json.JSONDecodeError as e:
        raise AssertionError(
            "Failed to decode JSON from stdout.\n"
            f"exit_code={res.exit_code}\n"
            f"stdout={res.stdout!r}\n"
            f"stderr={res.stderr!r}\n"
            f"output={res.output!r}\n"
            f"exception={res.exception!r}\n"
            f"json_error={e}\n"
        ) from e

    # Ensure it's a dict-like JSON object
    if not isinstance(data, dict):
        raise AssertionError(f"Expected JSON object, got: {type(data).__name__}: {data!r}")

    return cast(JSONObject, data)

def get_change(data: JSONObject) -> JSONObject:
    """
    New CLI JSON wraps the change in {"ok": ..., "change": {...}, ...}.
    Also accept legacy where the change was top-level.
    """
    chg = data.get("change")
    if isinstance(chg, dict):
        return cast(JSONObject, chg)
    return data


def _as_object(v: JSONValue, *, ctx: str = "value") -> JSONObject:
    if not isinstance(v, dict):
        raise AssertionError(f"Expected object for {ctx}, got {type(v).__name__}: {v!r}")
    return cast(JSONObject, v)


def _as_str(v: JSONValue, *, ctx: str = "value") -> str:
    if not isinstance(v, str):
        raise AssertionError(f"Expected string for {ctx}, got {type(v).__name__}: {v!r}")
    return v


def _as_bool(v: JSONValue, *, ctx: str = "value") -> bool:
    if not isinstance(v, bool):
        raise AssertionError(f"Expected bool for {ctx}, got {type(v).__name__}: {v!r}")
    return v


def _as_str_list(v: JSONValue, *, ctx: str = "value") -> list[str]:
    if not isinstance(v, list) or not all(isinstance(x, str) for x in v):
        raise AssertionError(f"Expected list[str] for {ctx}, got: {v!r}")
    return cast(list[str], v)


# -----------------------
# Change payload accessors
# -----------------------

def chg_kind(chg: JSONObject) -> str | None:
    v = chg.get("kind")
    if v is None:
        return None
    return _as_str(v, ctx="change.kind")


def chg_scenario_name(chg: JSONObject) -> str:
    return _as_str(chg.get("scenario_name"), ctx="change.scenario_name")


def delta_add(chg: JSONObject, field: str) -> list[str]:
    """
    field is one of: "given", "when", "then"
    """
    obj = _as_object(chg.get(field), ctx=f"change.{field}")
    return _as_str_list(obj.get("add"), ctx=f"change.{field}.add")


def delta_remove(chg: JSONObject, field: str) -> list[str]:
    obj = _as_object(chg.get(field), ctx=f"change.{field}")
    return _as_str_list(obj.get("remove"), ctx=f"change.{field}.remove")


def result_ok(data: JSONObject) -> bool:
    return _as_bool(data.get("ok"), ctx="result.ok")


def result_stage(data: JSONObject) -> str | None:
    v = data.get("stage")
    if v is None:
        return None
    return _as_str(v, ctx="result.stage")


def result_error_str(data: JSONObject) -> str:
    """
    Your JSON-safe error is a JSONObject (per your typing), but for tests we usually
    want a stable string. This extracts a string in a robust way.
    Adjust if your error schema is richer (e.g. {"message": "...", "type": "..."}).
    """
    err = data.get("error")
    if err is None:
        return ""
    if isinstance(err, str):
        return err
    if isinstance(err, dict) and isinstance(err.get("message"), str):
        return cast(str, err["message"])
    # fallback: stable stringify
    return json.dumps(cast(JSONValue, err))