"""SP17 — `cyclone backup` CLI subcommand tests.

Uses Click's CliRunner + monkeypatching of Keychain + DB env so the
subcommands can run without the operator's machine state.
"""
from __future__ import annotations

from click.testing import CliRunner
from datetime import datetime, timezone
import pytest


@pytest.fixture
def _cli_env(tmp_path, monkeypatch):
    """Fresh sqlite DB + in-memory Keychain stub."""
    from cyclone import db
    from cyclone import backup_service as svc_mod
    from cyclone import secrets as secrets_mod
    from cyclone.db import Batch
    import uuid

    monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
    db._reset_for_tests()
    db.init_db()

    with db.SessionLocal()() as s:
        s.add(Batch(
            id=str(uuid.uuid4()),
            kind="837P",
            input_filename="seed.x12",
            parsed_at=datetime.now(timezone.utc),
            totals_json=None,
            validation_json=None,
            raw_result_json={"envelope": {"control_number": "1"}, "claims": [], "summary": {"passed": 0, "failed": 0, "failed_claim_ids": []}},
        ))
        s.commit()

    # In-memory Keychain so passphrase + salt persist across
    # separate CliRunner invocations within one test (each
    # subprocess-like invocation would otherwise generate a fresh
    # random salt and fail to decrypt).
    store: dict[str, str] = {}
    store[svc_mod.KEYCHAIN_BACKUP_PASSPHRASE_ACCOUNT] = "cli-test-passphrase"
    # Pre-populate a stable salt so the very first invocation
    # doesn't generate a new random one (which the next invocation
    # would then fail to reproduce).
    store[svc_mod.KEYCHAIN_BACKUP_SALT_ACCOUNT] = "0123456789abcdef0123456789abcdef"

    def _get(name):
        return store.get(name)
    def _set(name, value):
        store[name] = value
        return True

    monkeypatch.setattr(secrets_mod, "get_secret", _get)
    monkeypatch.setattr(secrets_mod, "set_secret", _set)

    backup_dir = tmp_path / "backups"
    monkeypatch.setenv("CYCLONE_BACKUP_DIR", str(backup_dir))
    monkeypatch.setenv("CYCLONE_BACKUP_RETENTION_DAYS", "7")

    yield backup_dir
    db._reset_for_tests()


def _run(args, env):
    from cyclone.cli import main
    runner = CliRunner()
    return runner.invoke(main, args, catch_exceptions=False)


def test_backup_create_list_verify_status(_cli_env):
    """Happy path: create → list → verify → status."""
    backup_dir = _cli_env

    # create
    r = _run(["backup", "create"], _cli_env)
    assert r.exit_code == 0, r.output
    assert "created backup id=" in r.output

    # list
    r = _run(["backup", "list"], _cli_env)
    assert r.exit_code == 0, r.output
    assert ".bin" in r.output

    # verify (we don't know the id, parse it from the list output)
    import re
    m = re.search(r"^\s*(\d+)\s+ok\s+", r.output, re.MULTILINE)
    assert m, r.output
    backup_id = int(m.group(1))
    r = _run(["backup", "verify", str(backup_id)], _cli_env)
    assert r.exit_code == 0, r.output
    assert r.output.startswith("OK:")

    # status
    r = _run(["backup", "status"], _cli_env)
    assert r.exit_code == 0, r.output
    assert '"totals"' in r.output
    assert '"ok": 1' in r.output


def test_backup_verify_fails_on_tampered_ciphertext(_cli_env):
    from cyclone import backup as backup_mod
    from cyclone import backup_service as svc_mod
    from cyclone import secrets as secrets_mod

    # Create a backup.
    r = _run(["backup", "create"], _cli_env)
    assert r.exit_code == 0
    # Tamper.
    bin_path = next(_cli_env.glob("*.bin"))
    data = bytearray(bin_path.read_bytes())
    data[backup_mod.NONCE_LEN + 5] ^= 0x01
    bin_path.write_bytes(bytes(data))

    # Verify should fail.
    r = _run(["backup", "verify", "1"], _cli_env)
    assert r.exit_code == 1
    assert "FAIL" in r.output


def test_backup_restore_requires_yes_flag(_cli_env):
    """Without --yes, an interactive confirm blocks and the command aborts."""
    r = _run(["backup", "create"], _cli_env)
    assert r.exit_code == 0
    # Click's runner auto-declines the confirm prompt; expect abort.
    r = _run(["backup", "restore", "1"], _cli_env, )
    # CliRunner auto-aborts confirm prompts by default → exit code != 0.
    assert r.exit_code != 0


def test_backup_prune_aborts_without_yes(_cli_env):
    r = _run(["backup", "create"], _cli_env)
    assert r.exit_code == 0
    # Same auto-abort for the prune confirm.
    r = _run(["backup", "prune"], _cli_env)
    assert r.exit_code != 0


def test_backup_init_passphrase_rejects_short(_cli_env):
    """init-passphrase enforces a 12-char minimum."""
    r = _run(["backup", "init-passphrase", "--passphrase", "short"], _cli_env)
    assert r.exit_code != 0
    assert "12 characters" in r.output
