"""P2 value-verb CLI contract (SPEC §4, §6.3) — TTY/argv guards, add/set, exec, etc."""

from __future__ import annotations

import contextlib
import io
import json
import shutil
import subprocess

import pytest

from mgf.secrets import cli
from mgf.secrets import store as S
from tests._store import make_store_env

_HAS_GIT = shutil.which("git") is not None


class _TtyStdout(io.StringIO):
    def isatty(self) -> bool:
        return True


def _run(env, argv, *, stdin_bytes=None, tty=False):
    """Run the CLI with this env's registry; capture stdout/stderr as text."""
    out = _TtyStdout() if tty else io.StringIO()
    err = io.StringIO()
    real_stdin = None
    if stdin_bytes is not None:
        real_stdin = _install_stdin(stdin_bytes)
    try:
        with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
            code = cli.main([*env.common(), *argv], registry=env.registry)
    finally:
        if real_stdin is not None:
            _restore_stdin(real_stdin)
    return code, out.getvalue(), err.getvalue()


def _install_stdin(data: bytes):
    import sys

    saved = sys.stdin
    holder = type("_S", (), {"buffer": io.BytesIO(data)})()
    sys.stdin = holder  # type: ignore[assignment]
    return saved


def _restore_stdin(saved) -> None:
    import sys

    sys.stdin = saved


def _add(env, entry_id, value: bytes):
    code, out, err = _run(env, ["add", entry_id, "--value-file", "-"], stdin_bytes=value)
    assert code == 0, err
    return out


# -- add / set --------------------------------------------------------------- #


def test_add_via_stdin_then_roundtrip(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"STDIN_VALUE_1")
    assert (
        S.store_get("test_token", root=env.store_root, identity_path=env.operator_key)
        == b"STDIN_VALUE_1"
    )


def test_add_via_value_file(tmp_path) -> None:
    env = make_store_env(tmp_path)
    vf = tmp_path / "v.txt"
    vf.write_bytes(b"FILE_VALUE_2")
    code, out, err = _run(env, ["add", "test_token", "--value-file", str(vf)])
    assert code == 0, err
    assert (
        S.store_get("test_token", root=env.store_root, identity_path=env.operator_key)
        == b"FILE_VALUE_2"
    )


def test_add_via_hidden_prompt(tmp_path, monkeypatch) -> None:
    env = make_store_env(tmp_path)
    monkeypatch.setattr("mgf.secrets.cli.getpass.getpass", lambda prompt: "PROMPT_VALUE_3")
    code, out, err = _run(env, ["add", "test_token"])
    assert code == 0, err
    assert (
        S.store_get("test_token", root=env.store_root, identity_path=env.operator_key)
        == b"PROMPT_VALUE_3"
    )


def test_add_value_on_argv_is_rejected(tmp_path) -> None:
    env = make_store_env(tmp_path)
    code, out, err = _run(env, ["add", "test_token", "--value", "SECRET_ON_ARGV"])
    assert code == 2
    assert "argv" in err
    assert "SECRET_ON_ARGV" not in err  # the rejected value is not echoed


def test_add_refuses_held_entry(tmp_path) -> None:
    env = make_store_env(tmp_path)
    code, out, err = _run(env, ["add", "held_secret", "--value-file", "-"], stdin_bytes=b"x")
    assert code == 2
    assert "held" in err
    assert not S.cipher_path(env.store_root, "held_secret").is_file()


def test_add_then_add_again_requires_set(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"v1")
    code, out, err = _run(env, ["add", "test_token", "--value-file", "-"], stdin_bytes=b"v2")
    assert code == 2 and "use `set`" in err


def test_set_requires_existing(tmp_path) -> None:
    env = make_store_env(tmp_path)
    code, out, err = _run(env, ["set", "test_token", "--value-file", "-"], stdin_bytes=b"v")
    assert code == 2 and "use `add`" in err


def test_set_updates_existing(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"v1")
    code, out, err = _run(env, ["set", "test_token", "--value-file", "-"], stdin_bytes=b"v2")
    assert code == 0, err
    assert S.store_get("test_token", root=env.store_root, identity_path=env.operator_key) == b"v2"


def test_add_json_is_value_free(tmp_path) -> None:
    env = make_store_env(tmp_path)
    code, out, err = _run(
        env, ["add", "test_token", "--value-file", "-", "--json"], stdin_bytes=b"JSON_SECRET_v"
    )
    assert code == 0, err
    data = json.loads(out)
    assert data["id"] == "test_token" and "sha256" in data
    assert "JSON_SECRET_v" not in out


def test_add_unknown_id_scaffolds(tmp_path) -> None:
    env = make_store_env(tmp_path)
    code, out, err = _run(env, ["add", "nonexistent", "--value-file", "-"], stdin_bytes=b"x")
    assert code == 2 and "declare it in SECRETS.toml" in err


# -- show --value (TTY / json guards) ---------------------------------------- #


def test_show_value_refuses_tty(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"TTY_SECRET")
    code, out, err = _run(
        env, ["show", "test_token", "--value", "--identity", str(env.operator_key)], tty=True
    )
    assert code == 2
    assert "TTY" in err or "tty" in err
    assert "TTY_SECRET" not in out and "TTY_SECRET" not in err


def test_show_value_tty_ok_allows(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"TTY_OK_SECRET")
    code, out, err = _run(
        env,
        ["show", "test_token", "--value", "--tty-ok", "--identity", str(env.operator_key)],
        tty=True,
    )
    assert code == 0, err
    assert "TTY_OK_SECRET" in out


def test_show_value_refuses_json(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"x")
    code, out, err = _run(env, ["show", "test_token", "--value", "--json"])
    assert code == 2 and "JSON" in err


def test_show_value_surfaces_to_pipe(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"PIPE_VALUE_ok")
    # non-tty (StringIO) → allowed; it is one of the two paths that may surface a value.
    code, out, err = _run(
        env, ["show", "test_token", "--value", "--identity", str(env.operator_key)]
    )
    assert code == 0, err
    assert out == "PIPE_VALUE_ok"


# -- exec -------------------------------------------------------------------- #


def test_exec_injects_env_and_passes_exit(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"EXEC_SECRET")
    # The child asserts the injected var equals the value → exit 0 proves injection,
    # with the value never leaving the child (no capture needed here).
    code, _out, _err = _run(
        env,
        [
            "exec",
            "--with",
            "test_token:MYSECRET",
            "--identity",
            str(env.operator_key),
            "--",
            "sh",
            "-c",
            'test "$MYSECRET" = "EXEC_SECRET"',
        ],
    )
    assert code == 0


def test_exec_default_env_var_name(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"DEFAULTENV")
    code, _o, _e = _run(
        env,
        [
            "exec",
            "--with",
            "test_token",
            "--identity",
            str(env.operator_key),
            "--",
            "sh",
            "-c",
            'test "$TEST_TOKEN" = "DEFAULTENV"',
        ],
    )
    assert code == 0


def test_exec_surfaces_value_in_child(tmp_path, capfd) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"CHILD_SURFACED")
    # exec IS allowed to surface the value (into the child) — prove it via fd capture.
    cli.main(
        [
            *env.common(),
            "exec",
            "--with",
            "test_token:X",
            "--identity",
            str(env.operator_key),
            "--",
            "printenv",
            "X",
        ],
        registry=env.registry,
    )
    assert "CHILD_SURFACED" in capfd.readouterr().out


def test_exec_needs_command(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"x")
    code, _o, err = _run(env, ["exec", "--with", "test_token", "--"])
    assert code == 2 and "command" in err


# -- export-offline ---------------------------------------------------------- #


def test_export_offline_writes_founder_bundle(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"BUNDLE_ME")
    outp = tmp_path / "bundle.age"
    code, out, err = _run(env, ["export-offline", "--out", str(outp)])
    assert code == 0, err
    blob = outp.read_bytes()
    assert S.is_age_ciphertext(blob)
    # Founder key decrypts the whole bundle; operator's own key would NOT (founder-only).
    import tarfile

    tar_bytes = S.decrypt(blob, env.founder_key)
    names = tarfile.open(fileobj=io.BytesIO(tar_bytes)).getnames()
    assert "manifest.json" in names and "secrets/test_token.age" in names


def test_export_offline_refuses_stdout(tmp_path) -> None:
    env = make_store_env(tmp_path)
    code, out, err = _run(env, ["export-offline", "--out", "-"])
    assert code == 2 and "refuses to print" in err


# -- history ----------------------------------------------------------------- #


def _git(store_root, *args):
    env = {
        "GIT_AUTHOR_NAME": "t",
        "GIT_AUTHOR_EMAIL": "t@example.org",
        "GIT_COMMITTER_NAME": "t",
        "GIT_COMMITTER_EMAIL": "t@example.org",
        "GIT_CONFIG_GLOBAL": "/dev/null",
        "GIT_CONFIG_SYSTEM": "/dev/null",
        "HOME": str(store_root),
    }
    subprocess.run(["git", "-C", str(store_root), *args], check=True, capture_output=True, env=env)


@pytest.mark.skipif(not _HAS_GIT, reason="git not in the CI test image (git-less-CI class)")
def test_history_lists_revisions(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"histval")
    _git(env.store_root, "init", "-q")
    _git(env.store_root, "add", "-A")
    _git(env.store_root, "commit", "-q", "-m", "add test_token to store")
    code, out, err = _run(env, ["history", "test_token", "--json"])
    assert code == 0, err
    data = json.loads(out)
    assert data["file"] == "secrets/test_token.age"
    assert len(data["history"]) == 1
    assert data["history"][0]["subject"] == "add test_token to store"


def test_history_no_git_errors(tmp_path) -> None:
    env = make_store_env(tmp_path)  # store is not a git repo (or git absent) → exit 2 either way
    _add(env, "test_token", b"x")
    code, out, err = _run(env, ["history", "test_token"])
    assert code == 2


def test_history_git_absent_is_graceful(tmp_path, monkeypatch) -> None:
    """git-less box: the history verb degrades to a clean exit-2 message, not a traceback.

    Simulate the git-less CI image by making the child spawn raise FileNotFoundError
    (exactly what `subprocess.run(["git", …])` raises when the binary is absent). The
    verb must catch it: exit 2, a pointed message, NO traceback, and (redaction) no
    stored value in the output.
    """
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"GITLESS_SECRET_zzz")

    def _no_git(*_a, **_k):
        raise FileNotFoundError(2, "No such file or directory", "git")

    monkeypatch.setattr(cli.subprocess, "run", _no_git)
    code, out, err = _run(env, ["history", "test_token"])
    assert code == 2
    assert "git not found" in err
    assert "Traceback" not in err
    assert "GITLESS_SECRET_zzz" not in (out + err)


# -- materialize CLI --------------------------------------------------------- #


def test_materialize_plan_prints_no_value(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"PLAN_SECRET_ZZZ")
    code, out, err = _run(env, ["materialize"])  # default --plan
    assert code == 0, err
    assert "PLAN_SECRET_ZZZ" not in out
    assert "test_token" in out


def test_materialize_apply_writes(tmp_path) -> None:
    env = make_store_env(tmp_path)
    _add(env, "test_token", b"APPLY_ME")
    code, out, err = _run(env, ["materialize", "--apply", "--identity", str(env.operator_key)])
    assert code == 0, err
    target = env.fed_root / "mgf" / "my_stuff" / "tokens" / "test_token.txt"
    assert target.read_bytes() == b"APPLY_ME"
    assert "APPLY_ME" not in out  # apply output is metadata only
