# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

from __future__ import annotations

import json
import subprocess
from pathlib import Path
from typing import Any

import pytest

from solstone.think.backup import runner


def test_run_restic_builds_safe_argv_and_minimal_env(
    monkeypatch: pytest.MonkeyPatch,
):
    captured: dict[str, Any] = {}

    def fake_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        captured["argv"] = argv
        captured["env"] = kwargs["env"]
        captured["pass_fds"] = kwargs["pass_fds"]
        return subprocess.CompletedProcess(argv, 0, stdout="{}", stderr="")

    monkeypatch.setenv("PATH", "/bin")
    monkeypatch.setenv("HOME", "/home/test")
    monkeypatch.setenv("TMPDIR", "/tmp/test")
    monkeypatch.setenv("UNRELATED_SECRET", "do-not-copy")
    monkeypatch.setattr(runner.subprocess, "run", fake_run)

    result = runner.run_restic(
        ["snapshots"],
        repository="s3:safe-bucket/path",
        password="repo-password",
        restic_path=Path("/usr/bin/restic"),
        backend_env={
            "AWS_ACCESS_KEY_ID": "access-key",
            "AWS_SECRET_ACCESS_KEY": "backend-secret",
        },
        json=True,
        max_repack_size="1G",
    )

    assert result.argv == (
        "/usr/bin/restic",
        "snapshots",
        "--json",
        "--max-repack-size",
        "1G",
    )
    assert "--insecure-tls" not in result.argv
    assert all("repo-password" not in token for token in result.argv)
    assert all("backend-secret" not in token for token in result.argv)
    assert captured["env"] == {
        "PATH": "/bin",
        "HOME": "/home/test",
        "TMPDIR": "/tmp/test",
        "RESTIC_REPOSITORY": "s3:safe-bucket/path",
        "RESTIC_PASSWORD": "repo-password",
        "AWS_ACCESS_KEY_ID": "access-key",
        "AWS_SECRET_ACCESS_KEY": "backend-secret",
    }
    assert captured["pass_fds"] == ()


def test_run_restic_threads_pass_fds(monkeypatch: pytest.MonkeyPatch):
    captured: dict[str, Any] = {}

    def fake_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        captured["argv"] = argv
        captured["pass_fds"] = kwargs["pass_fds"]
        return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")

    monkeypatch.setattr(runner.subprocess, "run", fake_run)

    result = runner.run_restic(
        ["key", "add", "--new-password-file", "/dev/fd/17"],
        repository="s3:safe-bucket/path",
        password="repo-password",
        restic_path=Path("/usr/bin/restic"),
        pass_fds=(17,),
    )

    assert result.returncode == 0
    assert captured["argv"] == [
        "/usr/bin/restic",
        "key",
        "add",
        "--new-password-file",
        "/dev/fd/17",
    ]
    assert captured["pass_fds"] == (17,)


def test_run_restic_scrubs_success_output_and_json(
    monkeypatch: pytest.MonkeyPatch,
):
    secrets = ("repo-password", "backend-secret", "access-key", "SESS-TOKEN")

    def fake_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        stdout = json.dumps(
            {
                "message": (
                    "repo-password backend-secret access-key "
                    "SESS-TOKEN should be hidden"
                )
            }
        )
        stderr = "stderr has repo-password backend-secret and SESS-TOKEN"
        return subprocess.CompletedProcess(argv, 0, stdout=stdout, stderr=stderr)

    monkeypatch.setattr(runner.subprocess, "run", fake_run)

    result = runner.run_restic(
        ["snapshots"],
        repository="s3:safe-bucket/path",
        password="repo-password",
        restic_path=Path("/usr/bin/restic"),
        backend_env={
            "AWS_ACCESS_KEY_ID": "access-key",
            "AWS_SECRET_ACCESS_KEY": "backend-secret",
            "AWS_SESSION_TOKEN": "SESS-TOKEN",
        },
        json=True,
    )

    json_text = json.dumps(result.json)
    for secret in secrets:
        assert secret not in result.stdout
        assert secret not in result.stderr
        assert secret not in json_text
        assert all(secret not in token for token in result.argv)
    assert result.json == {
        "message": "[redacted] [redacted] [redacted] [redacted] should be hidden"
    }


def test_run_restic_returns_scrubbed_nonzero_result(
    monkeypatch: pytest.MonkeyPatch,
):
    secrets = ("repo-password", "backend-secret")

    def fake_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        return subprocess.CompletedProcess(
            argv,
            42,
            stdout="",
            stderr="failed with repo-password and backend-secret",
        )

    monkeypatch.setattr(runner.subprocess, "run", fake_run)

    result = runner.run_restic(
        ["backup", "/tmp/data"],
        repository="s3:safe-bucket/path",
        password="repo-password",
        restic_path=Path("/usr/bin/restic"),
        backend_env={"AWS_SECRET_ACCESS_KEY": "backend-secret"},
    )

    assert result.returncode == 42
    json_text = json.dumps(result.json)
    for secret in secrets:
        assert secret not in result.stdout
        assert secret not in result.stderr
        assert secret not in json_text
        assert all(secret not in token for token in result.argv)
    assert "[redacted]" in result.stderr


def test_empty_backend_values_are_not_scrubbed(
    monkeypatch: pytest.MonkeyPatch,
):
    def fake_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        assert kwargs["env"]["EMPTY"] == ""
        assert "NONE" not in kwargs["env"]
        return subprocess.CompletedProcess(argv, 0, stdout="abc", stderr="")

    monkeypatch.setattr(runner.subprocess, "run", fake_run)

    result = runner.run_restic(
        ["snapshots"],
        repository="s3:safe-bucket/path",
        password="repo-password",
        restic_path=Path("/usr/bin/restic"),
        backend_env={"EMPTY": "", "NONE": None},
    )

    assert result.stdout == "abc"


def test_run_restic_rejects_insecure_tls(monkeypatch: pytest.MonkeyPatch):
    def fail_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        raise AssertionError("subprocess should not be called")

    monkeypatch.setattr(runner.subprocess, "run", fail_run)

    with pytest.raises(RuntimeError, match="--insecure-tls"):
        runner.run_restic(
            ["backup", "--insecure-tls", "/tmp/data"],
            repository="s3:safe-bucket/path",
            password="repo-password",
            restic_path=Path("/usr/bin/restic"),
        )


def test_run_restic_rejects_secret_in_argv(monkeypatch: pytest.MonkeyPatch):
    def fail_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        raise AssertionError("subprocess should not be called")

    monkeypatch.setattr(runner.subprocess, "run", fail_run)

    with pytest.raises(RuntimeError, match="argv contains a secret"):
        runner.run_restic(
            ["backup", "/tmp/repo-password/data"],
            repository="s3:safe-bucket/path",
            password="repo-password",
            restic_path=Path("/usr/bin/restic"),
        )


def test_run_restic_timeout_returns_scrubbed_result(
    monkeypatch: pytest.MonkeyPatch,
):
    def fake_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        raise subprocess.TimeoutExpired(
            argv,
            timeout=1,
            output=b"stdout repo-password backend-secret SESS-TOKEN",
            stderr=b"stderr repo-password backend-secret SESS-TOKEN",
        )

    monkeypatch.setattr(runner.subprocess, "run", fake_run)

    result = runner.run_restic(
        ["backup", "/tmp/data"],
        repository="s3:safe-bucket/path",
        password="repo-password",
        restic_path=Path("/usr/bin/restic"),
        backend_env={
            "AWS_SECRET_ACCESS_KEY": "backend-secret",
            "AWS_SESSION_TOKEN": "SESS-TOKEN",
        },
        timeout=1,
    )

    assert result.returncode == 124
    assert "repo-password" not in result.stdout
    assert "backend-secret" not in result.stdout
    assert "SESS-TOKEN" not in result.stdout
    assert "repo-password" not in result.stderr
    assert "backend-secret" not in result.stderr
    assert "SESS-TOKEN" not in result.stderr
    assert result.json is None


def test_parse_json_lines_from_scrubbed_stdout(
    monkeypatch: pytest.MonkeyPatch,
):
    def fake_run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        stdout = '{"message":"backend-secret"}\n{"message":"ok"}\n'
        return subprocess.CompletedProcess(argv, 0, stdout=stdout, stderr="")

    monkeypatch.setattr(runner.subprocess, "run", fake_run)

    result = runner.run_restic(
        ["snapshots"],
        repository="s3:safe-bucket/path",
        password="repo-password",
        restic_path=Path("/usr/bin/restic"),
        backend_env={"AWS_SECRET_ACCESS_KEY": "backend-secret"},
        json=True,
    )

    assert result.json == [{"message": "[redacted]"}, {"message": "ok"}]


@pytest.mark.parametrize(
    ("returncode", "reason"),
    [
        (3, "incomplete"),
        (10, "repo_missing"),
        (11, "locked"),
        (12, "auth_failed"),
        (124, "timeout"),
        (77, "failed"),
    ],
)
def test_reason_for_returncode(returncode: int, reason: str) -> None:
    assert runner.reason_for_returncode(returncode) == reason


def test_select_summary_from_dict_or_json_lines() -> None:
    assert runner.select_summary({"message_type": "summary", "snapshot_id": "one"}) == {
        "message_type": "summary",
        "snapshot_id": "one",
    }
    assert runner.select_summary(
        [
            {"message_type": "status", "percent_done": 50},
            {"message_type": "summary", "snapshot_id": "two"},
        ]
    ) == {"message_type": "summary", "snapshot_id": "two"}
    assert runner.select_summary({"message_type": "status"}) is None
