from __future__ import annotations

from pathlib import Path

import pytest

from iac.cli.formatting import build_table, ensure_output_format
from iac.runbook.cel_eval import CelEngine
from iac.runbook.deps import resolve_install_command
from iac.runbook.discovery import (
    book_id_for_path,
    list_books,
    parse_book_dirs,
    resolve_book,
    split_book_step,
)
from iac.runbook.loader import load_book, parse_vars_kv, resolve_inputs
from iac.runbook.model import parse_book
from iac.runbook.sensitive import (
    REDACTION,
    SecretRedactor,
    redact_vars_map,
    sensitive_values,
)


def test_formatting_helpers() -> None:
    assert ensure_output_format("json") == "json"
    with pytest.raises(Exception):
        ensure_output_format("nope")
    t = build_table(["A", "B"], [[None, True], ["x", False]])
    s = t.get_string()
    assert "true" in s and "false" in s and "x" in s


def test_discovery_and_resolution(tmp_path: Path) -> None:
    d1 = tmp_path / "books"
    d2 = tmp_path / "other"
    d1.mkdir()
    d2.mkdir()
    (d1 / "a.yaml").write_text(
        "kind: pyplines.runbook.v1\nname: a\nsteps: [{name: s, sh: ['echo','hi']}]\n",
        encoding="utf-8",
    )
    (d2 / "b").mkdir()
    (d2 / "b" / "book.yaml").write_text(
        "kind: pyplines.runbook.v1\nname: b\nsteps: [{name: s, sh: ['echo','hi']}]\n",
        encoding="utf-8",
    )

    dirs = parse_book_dirs(f"{d1}:{d2}:{d1}")
    books = list_books(dirs)
    assert len(books) >= 2
    assert book_id_for_path(d2 / "b" / "book.yaml") == "b"
    assert resolve_book("a", [d1]).name == "a.yaml"
    assert resolve_book("b", [d2]).name == "book.yaml"
    assert split_book_step("a@x") == ("a", "x")
    with pytest.raises(ValueError):
        split_book_step("broken")


def test_loader_and_parse_helpers(tmp_path: Path) -> None:
    assert parse_vars_kv(["a=1"]) == {"a": "1"}
    with pytest.raises(ValueError):
        parse_vars_kv(["bad"])

    p = tmp_path / "book.yaml"
    p.write_text(
        "kind: pyplines.runbook.v1\nname: b\nsteps: [{name: s, sh: ['echo','hi']}]\n",
        encoding="utf-8",
    )
    b = load_book(p)
    assert b.meta.name == "b"


def test_model_validation_and_sensitive() -> None:
    data = {
        "kind": "pyplines.runbook.v1",
        "name": "x",
        "inputs": {"secret": {"type": "string", "default": "abc"}},
        "steps": [
            {
                "name": "s",
                "sh": ["bash", "-lc", "echo hi"],
                "retries": 1,
                "retry_delay": 0.1,
            }
        ],
    }
    b = parse_book(data)
    vals = sensitive_values(b, vars_map={"secret": "abc"})
    red = SecretRedactor.from_values(vals)
    assert red.redact_text("abc") == "abc"
    assert redact_vars_map(b, {"secret": "abc"})["secret"] == "abc"

    with pytest.raises(ValueError):
        parse_book({"version": 1, "steps": [{"name": "s", "sh": ["echo", "x"]}]})
    with pytest.raises(ValueError):
        parse_book(
            {
                "kind": "pyplines.runbook.v1",
                "steps": [{"name": "s", "sh": ["echo", "x"], "retries": -1}],
            }
        )
    with pytest.raises(ValueError):
        parse_book(
            {
                "kind": "pyplines.runbook.v1",
                "steps": [{"name": "s", "sh": ["echo", "x"], "retry_delay": -1}],
            }
        )
    with pytest.raises(ValueError):
        parse_book(
            {
                "kind": "pyplines.runbook.v1",
                "steps": [{"name": "s", "sh": ["echo", "x"], "timeout": 0}],
            }
        )


def test_resolve_install_command() -> None:
    target, cmd, tried = resolve_install_command({"darwin": "brew i"})
    assert isinstance(tried, list)
    if target:
        assert cmd


def test_packages_parse_defaults() -> None:
    b = parse_book(
        {
            "kind": "pyplines.runbook.v1",
            "name": "x",
            "packages": {
                "os": [
                    {
                        "name": "bash",
                        "min_version": ">=3.2.0",
                        "check": "bash --version",
                    }
                ],
                "py": [{"name": "requests", "min_version": ">=2.32.5"}],
            },
            "steps": [{"name": "s", "sh": ["echo", "x"]}],
        }
    )
    assert len(b.requires) == 2
    bash = next(r for r in b.requires if r.name == "bash")
    reqs = next(r for r in b.requires if r.name == "requests")
    assert bash.req_type == "os"
    assert bash.install_cmd == "dnf install -y bash"
    assert reqs.req_type == "py"
    assert reqs.install_cmd == "python3 -m pip install requests>=2.32.5"


def test_kind_v1_parse_and_input_resolution_and_cel() -> None:
    book = parse_book(
        {
            "kind": "pyplines.runbook.v1",
            "name": "billing",
            "inputs": {
                "month": {"type": "string", "regex": r"^\d{4}-\d{2}$"},
                "accounts": {"type": "array", "default": ["prod"], "minItems": 1},
                "include_pdf": {"type": "boolean", "default": False},
            },
            "packages": {
                "os": [{"name": "bash", "check": "bash --version"}],
                "py": [{"name": "requests", "min_version": ">=2.32.5"}],
            },
            "steps": [
                {
                    "name": "export",
                    "sh": ["python3", "-c", "print('ok')"],
                    "vars": {"MONTH": "inputs.month"},
                    "outputs": {"status": '"ok"'},
                }
            ],
            "outputs": {"report_url": {"type": "string", "value": '"https://x"'}},
        }
    )
    assert book.meta.kind == "pyplines.runbook.v1"
    assert book.steps[0].mode == "argv"
    assert book.steps[0].vars_map["MONTH"] == "inputs.month"
    assert book.outputs["report_url"]["type"] == "string"

    vals = resolve_inputs(
        book.inputs,
        input_cli=["month=2026-03", 'accounts=["prod","dev"]', "include_pdf=true"],
    )
    assert vals["month"] == "2026-03"
    assert vals["accounts"] == ["prod", "dev"]
    assert vals["include_pdf"] is True

    cel = CelEngine.default()
    got = cel.eval(
        "size(inputs.accounts) > 0", {"inputs": vals, "steps": {}, "run": {}}
    )
    assert bool(got) is True
    parsed = cel.eval(
        'json("{\\"x\\":1}").x',
        {"inputs": vals, "steps": {}, "run": {}},
    )
    assert parsed == 1
    s = cel.eval("to_json(inputs.accounts)", {"inputs": vals, "steps": {}, "run": {}})
    assert "prod" in s


def test_resolve_inputs_error_paths() -> None:
    specs = {
        "month": {"type": "string", "regex": r"^\d{4}-(0[1-9]|1[0-2])$"},
        "flag": {"type": "boolean"},
        "items": {"type": "array", "minItems": 1},
        "n": {"type": "integer"},
        "f": {"type": "number"},
    }
    with pytest.raises(ValueError):
        resolve_inputs(
            specs, input_cli=["month=bad", "flag=true", "items=a", "n=1", "f=1.2"]
        )
    with pytest.raises(ValueError):
        resolve_inputs(
            specs, input_cli=["month=2026-03", "flag=maybe", "items=a", "n=1", "f=1.2"]
        )
    with pytest.raises(ValueError):
        resolve_inputs(
            specs, input_cli=["month=2026-03", "flag=true", "items=", "n=1", "f=1.2"]
        )
    with pytest.raises(ValueError):
        resolve_inputs(
            specs, input_cli=["month=2026-03", "flag=true", "items=a", "n=x", "f=1.2"]
        )
    with pytest.raises(ValueError):
        resolve_inputs(
            specs, input_cli=["month=2026-03", "flag=true", "items=a", "n=1", "f=x"]
        )
