from __future__ import annotations

from pathlib import Path

import pytest

from iac.cli.formatting import emit_structured
from iac.runbook.discovery import book_id_for_path, resolve_book, split_book_step
from iac.runbook.expr import (
    WhenContext,
    WhenEvaluator,
    _fn_contains,
    _fn_ends_with,
    _fn_get,
    _fn_has,
    _fn_starts_with,
    _sanitize,
)
from iac.runbook.loader import load_book, parse_vars_kv
from iac.runbook.sensitive import SecretRedactor
from iac.runbook.template import TemplateEngine, _parse_filter_call, _split_args


def test_emit_structured_all_paths(capsys: pytest.CaptureFixture[str]) -> None:
    emit_structured({"a": 1}, "json")
    out = capsys.readouterr().out
    assert '"a": 1' in out

    emit_structured({"a": 1}, "yaml")
    out = capsys.readouterr().out
    assert "a: 1" in out

    with pytest.raises(ValueError):
        emit_structured({"a": 1}, "table")  # type: ignore[arg-type]


def test_discovery_edge_paths(tmp_path: Path) -> None:
    f = tmp_path / "custom.book"
    f.write_text("x", encoding="utf-8")
    assert book_id_for_path(f) == "custom"

    direct = tmp_path / "direct.yaml"
    direct.write_text(
        "version: 1\nsteps: [{name: s, shell: 'echo hi'}]\n", encoding="utf-8"
    )
    assert resolve_book(str(direct), []) == direct.resolve()

    d = tmp_path / "d"
    d.mkdir()
    (d / "book.yaml").write_text(
        "version: 1\nsteps: [{name: s, shell: 'echo hi'}]\n", encoding="utf-8"
    )
    assert resolve_book(str(d), []) == (d / "book.yaml").resolve()
    (d / "book.yaml").unlink()
    with pytest.raises(FileNotFoundError):
        resolve_book(str(d), [])

    with pytest.raises(FileNotFoundError):
        resolve_book("missing", [tmp_path])

    with pytest.raises(ValueError):
        split_book_step("book@")


def test_loader_edge_paths(tmp_path: Path) -> None:
    bad = tmp_path / "bad.yaml"
    bad.write_text("- 1\n- 2\n", encoding="utf-8")
    with pytest.raises(ValueError):
        load_book(bad)

    with pytest.raises(ValueError):
        parse_vars_kv([" =x"])


def test_sensitive_edge_paths() -> None:
    red = SecretRedactor.from_values(["", "abc", "abc", "x"])
    assert red.redact_text("abc x") == "******** ********"
    # direct constructor to exercise empty-byte guard
    custom = SecretRedactor(_text_values=["x"], _byte_values=[b"", b"x"])
    assert custom.redact_bytes(b"x") == b"********"


def test_expr_edge_paths() -> None:
    wc = WhenContext(vars={"x": 1}, env={}, ctx={})
    ev = WhenEvaluator()
    # non-bool result path uses truthiness
    assert ev.eval("1", wc) is True

    class O:
        pass

    sanitized = _sanitize({"k": [1, (2, O())]})
    assert isinstance(sanitized["k"][1][1], str)

    assert _fn_has({}, "k") is False
    assert _fn_has({"k": 1}, "k") is True
    assert _fn_has([], "k") is False
    assert _fn_get([], "k", "d") == "d"
    assert _fn_get({"k": 2}, "k", "d") == 2
    assert _fn_contains("abc", "a") is True
    assert _fn_starts_with("abc", "a") is True
    assert _fn_ends_with("abc", "c") is True


def test_template_edge_paths() -> None:
    t = TemplateEngine.default()
    # non-container passthrough branch
    assert t.render_obj(42, vars_map={}, env_map={}) == 42
    # invalid base identifier branch
    assert t.render_str("{{ 1bad }}", vars_map={}, env_map={}) == ""
    # unknown filter branch
    assert (
        t.render_str("{{ name | missing }}", vars_map={"name": "x"}, env_map={}) == ""
    )

    # filter parser invalid call
    assert _parse_filter_call("@@") == ("", [])
    # bare arg path
    assert _parse_filter_call("replace(a,b)") == ("replace", ["a", "b"])
    # arg splitter coverage
    assert _split_args("'a',b,\"c\"") == ["'a'", "b", '"c"']


def test_loader_prefixed_var_empty_suffix() -> None:
    from iac.runbook.loader import _parse_prefixed_vars

    vals = _parse_prefixed_vars({"IAC_VAR_": "x", "IAC_VAR_NAME": "y"})
    assert vals == {"name": "y"}
