from __future__ import annotations

import json
from pathlib import Path

import yaml

from iac.cli import app
from conftest import runner, write_book


def _env(book_dir: Path) -> dict[str, str]:
    return {"IAC_BOOK_DIRS": str(book_dir)}


def test_run_book_step_selection_and_reports(tmp_path: Path) -> None:
    books = tmp_path / "books"
    write_book(books / "sample.yaml", name="sample")

    report_dir = tmp_path / "reports"
    artifacts_dir = tmp_path / "artifacts"

    r = runner.invoke(
        app,
        [
            "run",
            "book",
            "sample",
            "--only-step",
            "prepare",
            "--report-dir",
            str(report_dir),
            "--artifacts-dir",
            str(artifacts_dir),
            "--output",
            "json",
        ],
        env=_env(books),
    )
    assert r.exit_code == 0
    payload = json.loads(r.stdout)
    assert payload["summary"]["success"] == 1
    assert payload["results"][0]["step"] == "prepare"
    assert payload["results"][0]["attempts"] == 1

    r = runner.invoke(
        app,
        [
            "run",
            "book",
            "sample",
            "--from-step",
            "echo_secret",
            "--to-step",
            "optional",
            "--skip-step",
            "optional",
            "--output",
            "table",
        ],
        env=_env(books),
    )
    assert r.exit_code == 0
    assert "echo_secret" in r.stdout
    assert "optional" not in r.stdout or "skipped" not in r.stdout


def test_run_book_retry_and_failure_hint(tmp_path: Path) -> None:
    books = tmp_path / "books"
    write_book(books / "sample.yaml", name="sample")

    # run flaky only -> should succeed due retries=1
    r = runner.invoke(
        app,
        ["run", "book", "sample", "--only-step", "flaky", "--output", "table"],
        env=_env(books),
    )
    assert r.exit_code == 0
    assert "1/2" in r.stdout or "2/2" in r.stdout

    # failing step with no retries to trigger failure hint
    r = runner.invoke(
        app,
        ["run", "step", "sample@always_fail", "--retries", "0", "--output", "table"],
        env=_env(books),
    )
    assert r.exit_code != 0
    assert "Rerun Hint:" in r.stdout


def test_run_table_default_is_concise_and_verbose_expands(tmp_path: Path) -> None:
    books = tmp_path / "books"
    write_book(books / "sample.yaml", name="sample")

    r = runner.invoke(
        app,
        ["run", "book", "sample", "--only-step", "prepare", "--output", "table"],
        env=_env(books),
    )
    assert r.exit_code == 0
    assert "COMMAND" not in r.stdout
    assert "STDOUT" not in r.stdout
    assert "DURATION_MS" in r.stdout

    r = runner.invoke(
        app,
        [
            "run",
            "book",
            "sample",
            "--only-step",
            "prepare",
            "--verbose",
            "--output",
            "table",
        ],
        env=_env(books),
    )
    assert r.exit_code == 0
    assert "COMMAND" in r.stdout
    assert "STDOUT" in r.stdout


def test_run_step_and_invalid_selection_flags(tmp_path: Path) -> None:
    books = tmp_path / "books"
    write_book(books / "sample.yaml", name="sample")

    r = runner.invoke(
        app,
        ["run", "step", "sample@optional", "--quiet", "--output", "json"],
        env=_env(books),
    )
    assert r.exit_code == 0
    payload = json.loads(r.stdout)
    assert payload["result"]["status"] in ("success", "skipped")

    r = runner.invoke(
        app,
        [
            "run",
            "book",
            "sample",
            "--only-step",
            "prepare",
            "--from-step",
            "echo_secret",
        ],
        env=_env(books),
    )
    assert r.exit_code != 0
    assert "cannot be combined" in (r.stdout + r.stderr)


def test_run_step_blank_cwd_is_treated_as_unset(tmp_path: Path) -> None:
    books = tmp_path / "books"
    write_book(books / "sample.yaml", name="sample")

    data = {
        "kind": "pyplines.runbook.v1",
        "name": "blank_cwd",
        "steps": [
            {
                "name": "hello",
                "cwd": "",
                "sh": ["bash", "-lc", "echo hello"],
            }
        ],
    }
    (books / "blank_cwd.yaml").write_text(
        yaml.safe_dump(data, sort_keys=False), encoding="utf-8"
    )

    env = {"IAC_BOOK_DIRS": str(books)}
    r = runner.invoke(
        app,
        ["run", "step", "blank_cwd@hello", "--quiet", "--output", "json"],
        env=env,
    )
    assert r.exit_code == 0
    payload = json.loads(r.stdout)
    assert payload["result"]["status"] == "success"
    assert payload["result"]["cwd"] == ""


def test_run_cmd_modes_and_stdio(tmp_path: Path) -> None:
    out = tmp_path / "cmd.stdout"
    err = tmp_path / "cmd.stderr"

    r = runner.invoke(app, ["run", "cmd", "echo hi", "--quiet", "--output", "json"])
    assert r.exit_code == 0
    payload = json.loads(r.stdout)
    assert payload["kind"] == "cmd"
    assert payload["status"] == "success"

    r = runner.invoke(
        app,
        [
            "run",
            "cmd",
            "cat",
            "--no-shell",
            "--stdin-text",
            "abc",
            "--quiet",
            "--output",
            "json",
        ],
    )
    assert r.exit_code == 0
    payload = json.loads(r.stdout)
    assert payload["stdin_source"] == "text"

    r = runner.invoke(
        app,
        [
            "run",
            "cmd",
            "cat",
            "--no-shell",
            "--stdin-json",
            '{"x":1}',
            "--quiet",
            "--output",
            "json",
        ],
    )
    assert r.exit_code == 0

    r = runner.invoke(
        app,
        [
            "run",
            "cmd",
            'python3 -c \'import sys; print("ok"); print("bad", file=sys.stderr)\'',
            "--stdout-file",
            str(out),
            "--stderr-file",
            str(err),
            "--file-mode",
            "truncate",
            "--quiet",
            "--output",
            "table",
        ],
    )
    assert r.exit_code == 0
    assert out.read_text(encoding="utf-8").strip() == "ok"
    assert err.read_text(encoding="utf-8").strip() == "bad"

    r = runner.invoke(app, ["run", "cmd", "echo hi", "--no-shell"])
    assert r.exit_code != 0


def test_run_cmd_stdin_validation_errors(tmp_path: Path) -> None:
    missing = tmp_path / "missing.txt"
    r = runner.invoke(
        app, ["run", "cmd", "cat", "--no-shell", "--stdin-file", str(missing)]
    )
    assert r.exit_code != 0

    r = runner.invoke(
        app,
        ["run", "cmd", "cat", "--no-shell", "--stdin-text", "x", "--stdin-json", "{}"],
    )
    assert r.exit_code != 0


def test_run_module_mode_step(tmp_path: Path) -> None:
    books = tmp_path / "books"
    books.mkdir(parents=True, exist_ok=True)
    data = {
        "kind": "pyplines.runbook.v1",
        "name": "module_book",
        "inputs": {
            "artifact_dir": {"type": "string", "default": str(tmp_path / "artifacts")}
        },
        "steps": [
            {
                "name": "hello",
                "fn": "ops@run",
                "timeout": 30,
            }
        ],
    }
    (books / "module_book.yaml").write_text(
        yaml.safe_dump(data, sort_keys=False), encoding="utf-8"
    )

    modules = tmp_path / "modules"
    modules.mkdir(parents=True, exist_ok=True)
    (modules / "ops.py").write_text(
        "\n".join(
            [
                "from iac.runbook.module_types import StepContext, StepResult",
                "from pathlib import Path",
                "def run(ctx: StepContext) -> StepResult:",
                "    '''write artifact for contract-compliant module step'''",
                "    out = Path(ctx.var('artifact_dir')) / 'ok.txt'",
                "    out.parent.mkdir(parents=True, exist_ok=True)",
                "    out.write_text('ok', encoding='utf-8')",
                "    return StepResult(message=f'wrote {out}')",
                "",
            ]
        ),
        encoding="utf-8",
    )

    env = {"IAC_BOOK_DIRS": str(books), "IAC_MODULE_DIRS": str(modules)}
    r = runner.invoke(
        app,
        ["run", "step", "module_book@hello", "--quiet", "--output", "json"],
        env=env,
    )
    assert r.exit_code == 0
    payload = json.loads(r.stdout)
    assert payload["result"]["status"] == "success"
    assert "wrote" in payload["result"]["message"]


def test_run_fn_command_and_top_level_iac_import(tmp_path: Path) -> None:
    modules = tmp_path / "modules"
    modules.mkdir(parents=True, exist_ok=True)
    (modules / "ops.py").write_text(
        "\n".join(
            [
                "from iac import StepContext, StepResult",
                "def run(ctx: StepContext) -> StepResult:",
                "    '''return greeting'''",
                "    out = ctx.var('target', 'none')",
                "    return StepResult(message=f'hello {out}')",
                "",
            ]
        ),
        encoding="utf-8",
    )

    env = {"IAC_MODULE_DIRS": str(modules)}
    r = runner.invoke(
        app,
        [
            "run",
            "fn",
            "ops@run",
            "--var",
            "target=world",
            "--quiet",
            "--output",
            "json",
        ],
        env=env,
    )
    assert r.exit_code == 0
    payload = json.loads(r.stdout)
    assert payload["kind"] == "fn"
    assert payload["result"]["status"] == "success"
    assert payload["result"]["message"] == "hello world"
