from __future__ import annotations

import json
import runpy
from pathlib import Path

import pytest

from iac.runbook import module_exec
from iac.runbook.module_contract import load_validated_callable
from iac.runbook.module_types import StepContext, StepResult


def _write_module(path: Path, src: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(src, encoding="utf-8")


def test_step_context_and_result_helpers() -> None:
    ctx = StepContext(
        vars={"a": 1},
        env={},
        book_path="book.yaml",
        step_name="s",
        run_id="r",
        cwd=".",
    )
    assert ctx.var("a") == 1
    assert ctx.var("missing", "d") == "d"
    assert ctx.require_var("a") == 1
    with pytest.raises(KeyError):
        ctx.require_var("missing")

    r = StepResult(message="ok", outputs={"x": 1})
    assert r.message == "ok"
    assert r.outputs["x"] == 1


def test_module_exec_main_error_paths(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    rc = module_exec.main([])
    assert rc == 2
    assert "missing module ref" in capsys.readouterr().err

    rc = module_exec.main(["--ref", "ops@run"])
    assert rc == 2
    assert "missing step name" in capsys.readouterr().err

    rc = module_exec.main(
        ["--ref", "ops@run", "--step-name", "s", "--vars-json", "[1]"]
    )
    assert rc == 2
    assert "invalid --vars-json" in capsys.readouterr().err
    # no-op path write helper branch
    module_exec._write_result(None, {"message": "x"})


def test_module_exec_main_success_and_result_shapes(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    modules = tmp_path / "modules"
    _write_module(
        modules / "ops.py",
        "\n".join(
            [
                "from iac.runbook.module_types import StepContext, StepResult",
                "def returns_result(ctx: StepContext) -> StepResult:",
                "    '''return a valid result'''",
                "    return StepResult(message='ok', outputs={'a': 1})",
                "def returns_empty(ctx: StepContext) -> StepResult:",
                "    '''return empty result'''",
                "    return StepResult()",
                "def returns_dict(ctx: StepContext) -> StepResult:",
                "    '''invalid runtime return'''",
                "    return {'message': 'dict'}  # type: ignore[return-value]",
                "def boom(ctx: StepContext) -> StepResult:",
                "    '''raise runtime error'''",
                "    raise RuntimeError('boom')",
                "",
            ]
        ),
    )
    monkeypatch.setenv("IAC_MODULE_DIRS", str(modules))

    def run_one(fn: str) -> dict:
        result_file = tmp_path / f"{fn}.json"
        rc = module_exec.main(
            [
                "--ref",
                f"ops@{fn}",
                "--step-name",
                "s1",
                "--book-path",
                str(tmp_path / "book.yaml"),
                "--run-id",
                "rid",
                "--vars-json",
                '{"x":"1"}',
                "--result-file",
                str(result_file),
            ]
        )
        assert rc == 0
        return json.loads(result_file.read_text(encoding="utf-8"))

    assert run_one("returns_result") == {"message": "ok", "outputs": {"a": 1}}
    assert run_one("returns_empty") == {"message": "", "outputs": {}}

    rc = module_exec.main(["--ref", "ops@returns_dict", "--step-name", "s1"])
    assert rc == 1
    rc = module_exec.main(["--ref", "ops@boom", "--step-name", "s1"])
    assert rc == 1


def test_module_contract_load_callable_validation(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    modules = tmp_path / "modules"
    _write_module(
        modules / "bad.py",
        "\n".join(
            [
                "value = 1",
                "from iac.runbook.module_types import StepContext, StepResult",
                "def bad_sig(ctx: StepContext, extra) -> StepResult:",
                "    '''bad signature'''",
                "    return StepResult()",
                "def camelCase(ctx: StepContext) -> StepResult:",
                "    '''bad name'''",
                "    return StepResult()",
                "def no_doc(ctx: StepContext) -> StepResult:",
                "    return StepResult()",
                "def bad_param(ctx: str) -> StepResult:",
                "    '''bad param'''",
                "    return StepResult()",
                "def bad_return(ctx: StepContext) -> dict:",
                "    '''bad return'''",
                "    return {}",
                "",
            ]
        ),
    )
    monkeypatch.setenv("IAC_MODULE_DIRS", str(modules))

    with pytest.raises(AttributeError):
        load_validated_callable("bad", "missing")
    with pytest.raises(TypeError):
        load_validated_callable("bad", "bad_sig")
    with pytest.raises(ValueError):
        load_validated_callable("bad", "camelCase")
    with pytest.raises(ValueError):
        load_validated_callable("bad", "no_doc")
    with pytest.raises(TypeError):
        load_validated_callable("bad", "bad_param")
    with pytest.raises(TypeError):
        load_validated_callable("bad", "bad_return")


def test_module_can_import_step_types_from_iac(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    modules = tmp_path / "modules"
    _write_module(
        modules / "imports_iac.py",
        "\n".join(
            [
                "from iac import StepContext, StepResult",
                "def run(ctx: StepContext) -> StepResult:",
                "    '''imports through iac root'''",
                "    return StepResult(message='ok')",
                "",
            ]
        ),
    )
    monkeypatch.setenv("IAC_MODULE_DIRS", str(modules))
    fn = load_validated_callable("imports_iac", "run")
    assert callable(fn)


def test_module_exec_main_module_path_and_package_init_coverage(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    # Execute package __init__ file directly so coverage records iac/__init__.py lines.
    init_ns = runpy.run_path("iac/__init__.py")
    assert "StepContext" in init_ns
    assert "StepResult" in init_ns

    # Execute module_exec as __main__ and catch the SystemExit.
    monkeypatch.setenv("IAC_MODULE_REF", "")
    monkeypatch.setenv("IAC_MODULE_STEP_NAME", "")
    with pytest.raises(SystemExit):
        runpy.run_module("iac.runbook.module_exec", run_name="__main__")
