"""Store crypto + manifest + guards (SPEC §6.1, §6.5, §6.7) — TEST keys only."""

from __future__ import annotations

import json
import random

import pytest
from pyrage import x25519

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

# -- §6.1 roundtrip property: both recipients independently decrypt ----------- #


@pytest.mark.property
def test_roundtrip_both_recipients_decrypt(tmp_path) -> None:
    env = make_store_env(tmp_path)
    recipients = [env.founder_pub, env.operator_pub]
    rng = random.Random(20260718)
    for _ in range(40):
        n = rng.choice([0, 1, 16, 63, 64, 255, 1024, rng.randint(0, 4096)])
        payload = bytes(rng.getrandbits(8) for _ in range(n))
        ct = S.encrypt(payload, recipients)
        assert S.is_age_ciphertext(ct), "ciphertext must carry the age header"
        # Either secret key independently recovers the exact bytes (multi-recipient).
        assert S.decrypt(ct, env.operator_key) == payload
        assert S.decrypt(ct, env.founder_key) == payload


def test_encrypt_needs_a_recipient(tmp_path) -> None:
    from mgf.secrets.errors import UsageError

    with pytest.raises(UsageError):
        S.encrypt(b"x", [])


def test_encrypt_rejects_a_bad_recipient() -> None:
    from mgf.secrets.errors import UsageError

    with pytest.raises(UsageError):
        S.encrypt(b"x", ["not-an-age-key"])


def test_wrong_key_decrypt_errors_without_leaking(tmp_path) -> None:
    env = make_store_env(tmp_path)
    canary = b"CANARY_ROUNDTRIP_leak_5a1c"
    ct = S.encrypt(canary, [env.operator_pub])  # only operator can read
    stranger = x25519.Identity.generate()
    wk = tmp_path / "stranger.key"
    wk.write_text(str(stranger) + "\n", encoding="utf-8")
    from mgf.secrets.errors import CredentialError

    with pytest.raises(CredentialError) as exc:
        S.decrypt(ct, wk)  # founder-less recipient set → no match
    assert canary.decode() not in str(exc.value)


# -- §6.7 no-plaintext store guard -------------------------------------------- #


def test_is_age_ciphertext_positive_and_negative(tmp_path) -> None:
    env = make_store_env(tmp_path)
    ct = S.encrypt(b"hi", [env.operator_pub])
    assert S.is_age_ciphertext(ct) is True
    assert S.is_age_ciphertext(b"plaintext token value") is False
    assert S.is_age_ciphertext(b"") is False


def test_plaintext_guard_flags_a_non_age_file(tmp_path) -> None:
    env = make_store_env(tmp_path)
    # A legit ciphertext passes; a plaintext blob under secrets/ is caught.
    S.store_put("test_token", b"v", root=env.store_root, encrypted_at="2026-07-18T00:00:00Z")
    assert S.plaintext_guard(env.store_root) == []
    (S.secrets_dir(env.store_root) / "leak.age").write_bytes(b"this is not encrypted")
    offenders = S.plaintext_guard(env.store_root)
    assert offenders == ["secrets/leak.age"]


# -- §6.5 manifest consistency ------------------------------------------------ #


def test_manifest_verify_clean_after_put(tmp_path) -> None:
    env = make_store_env(tmp_path)
    S.store_put(
        "test_token",
        b"value",
        root=env.store_root,
        encrypted_at="2026-07-18T00:00:00Z",
        family="mgf",
        kind="api-token",
    )
    assert S.manifest_verify(env.store_root) == []


def test_manifest_orphan_age_file_fails(tmp_path) -> None:
    env = make_store_env(tmp_path)
    ct = S.encrypt(b"v", [env.operator_pub])
    (S.secrets_dir(env.store_root) / "ghost.age").write_bytes(ct)  # no manifest row
    problems = S.manifest_verify(env.store_root)
    assert any("orphan secrets/ghost.age" in p for p in problems)


def test_manifest_orphan_row_fails(tmp_path) -> None:
    env = make_store_env(tmp_path)
    manifest = S.empty_manifest()
    manifest["entries"]["phantom"] = {"file": "secrets/phantom.age", "sha256": "x"}
    S.manifest_save(env.store_root, manifest)  # row, but no .age file
    problems = S.manifest_verify(env.store_root)
    assert any("phantom" in p and "no secrets" in p for p in problems)


def test_manifest_unlisted_recipient_fails(tmp_path) -> None:
    env = make_store_env(tmp_path)
    stranger = str(x25519.Identity.generate().to_public())
    # Store encrypted to a recipient NOT in recipients.txt, and record it.
    ct = S.encrypt(b"v", [stranger])
    (S.secrets_dir(env.store_root) / "test_token.age").write_bytes(ct)
    manifest = S.empty_manifest()
    manifest["entries"]["test_token"] = {
        "file": "secrets/test_token.age",
        "sha256": __import__("hashlib").sha256(ct).hexdigest(),
        "recipients": [stranger],
    }
    S.manifest_save(env.store_root, manifest)
    problems = S.manifest_verify(env.store_root)
    assert any("absent from recipients.txt" in p for p in problems)


def test_manifest_sha_mismatch_fails(tmp_path) -> None:
    env = make_store_env(tmp_path)
    S.store_put("test_token", b"v", root=env.store_root, encrypted_at="2026-07-18T00:00:00Z")
    # Corrupt the manifest sha.
    mpath = S.manifest_path(env.store_root)
    data = json.loads(mpath.read_text(encoding="utf-8"))
    data["entries"]["test_token"]["sha256"] = "deadbeef"
    mpath.write_text(json.dumps(data), encoding="utf-8")
    assert any("sha256" in p for p in S.manifest_verify(env.store_root))


# -- recipients parsing ------------------------------------------------------- #


def test_parse_recipients_roles_comments_and_bare(tmp_path) -> None:
    text = "# comment\nfounder  age1aaa\noperator age1bbb\n\nage1ccc\n"
    pairs = S.parse_recipients(text)
    assert pairs == [("founder", "age1aaa"), ("operator", "age1bbb"), ("", "age1ccc")]


def test_founder_recipient_selected(tmp_path) -> None:
    env = make_store_env(tmp_path)
    assert S.founder_recipient(env.store_root) == env.founder_pub


def test_store_get_missing_value_errors(tmp_path) -> None:
    env = make_store_env(tmp_path)
    from mgf.secrets.errors import CredentialError

    with pytest.raises(CredentialError):
        S.store_get("test_token", root=env.store_root, identity_path=env.operator_key)


# -- location + identity resolution + the resolve() Python API ---------------- #


def test_store_root_resolution_order(tmp_path, monkeypatch) -> None:
    monkeypatch.delenv("MGF_SECRETS_STORE", raising=False)
    monkeypatch.delenv("MGF_ROOT", raising=False)
    # explicit wins
    assert S.store_root("/x/store") == __import__("pathlib").Path("/x/store")
    # env next
    monkeypatch.setenv("MGF_SECRETS_STORE", str(tmp_path / "envstore"))
    assert S.store_root() == tmp_path / "envstore"
    monkeypatch.delenv("MGF_SECRETS_STORE")
    # fed_root fallback → <fed>/mgf-secrets-store
    assert S.store_root(fed_root=tmp_path) == tmp_path / "mgf-secrets-store"


def test_default_identity_resolution_order(tmp_path, monkeypatch) -> None:
    monkeypatch.delenv("MGF_SECRETS_IDENTITY", raising=False)
    assert S.default_identity_path("/x/id.key") == __import__("pathlib").Path("/x/id.key")
    monkeypatch.setenv("MGF_SECRETS_IDENTITY", str(tmp_path / "envid.key"))
    assert S.default_identity_path() == tmp_path / "envid.key"
    monkeypatch.delenv("MGF_SECRETS_IDENTITY")
    assert S.default_identity_path().name == "operator.key"


def test_resolve_python_api_via_env(tmp_path, monkeypatch) -> None:
    from mgf.secrets import resolve

    env = make_store_env(tmp_path)
    canary = b"RESOLVE_API_value_7c3d"
    S.store_put(
        "test_token",
        canary,
        root=env.store_root,
        encrypted_at="2026-07-18T00:00:00Z",
        recipients=[env.founder_pub, env.operator_pub],
    )
    monkeypatch.setenv("MGF_SECRETS_STORE", str(env.store_root))
    monkeypatch.setenv("MGF_SECRETS_IDENTITY", str(env.operator_key))
    assert resolve("test_token") == canary
