"""Unit tests for djb.secrets.gpg module."""

from __future__ import annotations

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from djb.secrets.gpg import (
    GpgError,
    check_gpg_installed,
    gpg_decrypt_file,
    gpg_encrypt_file,
    is_gpg_encrypted,
    setup_gpg_tty,
)


class TestCheckGpgInstalled:
    """Tests for check_gpg_installed."""

    def test_gpg_installed(self):
        """Test returns True when gpg is available."""
        with patch("shutil.which", return_value="/usr/bin/gpg"):
            assert check_gpg_installed() is True

    def test_gpg_not_installed(self):
        """Test returns False when gpg is not available."""
        with patch("shutil.which", return_value=None):
            assert check_gpg_installed() is False


class TestSetupGpgTty:
    """Tests for setup_gpg_tty."""

    def test_sets_gpg_tty_from_tty_command(self):
        """Test GPG_TTY is set from tty command output."""
        mock_result = MagicMock()
        mock_result.stdout = "/dev/ttys001\n"

        with patch("subprocess.run", return_value=mock_result):
            env = setup_gpg_tty()
            assert env["GPG_TTY"] == "/dev/ttys001"

    def test_falls_back_to_dev_tty_on_error(self):
        """Test falls back to /dev/tty when tty command fails."""
        with patch(
            "subprocess.run", side_effect=subprocess.CalledProcessError(1, "tty")
        ):
            with patch.dict("os.environ", {}, clear=True):
                env = setup_gpg_tty()
                assert env["GPG_TTY"] == "/dev/tty"

    def test_preserves_existing_gpg_tty(self):
        """Test preserves existing GPG_TTY if set."""
        with patch(
            "subprocess.run", side_effect=subprocess.CalledProcessError(1, "tty")
        ):
            with patch.dict("os.environ", {"GPG_TTY": "/dev/pts/0"}):
                env = setup_gpg_tty()
                assert env["GPG_TTY"] == "/dev/pts/0"


class TestIsGpgEncrypted:
    """Tests for is_gpg_encrypted."""

    def test_returns_false_for_nonexistent_file(self, tmp_path: Path):
        """Test returns False when file doesn't exist."""
        assert is_gpg_encrypted(tmp_path / "nonexistent.gpg") is False

    def test_returns_true_for_gpg_file(self, tmp_path: Path):
        """Test returns True when gpg --list-packets succeeds."""
        test_file = tmp_path / "test.gpg"
        test_file.write_text("fake gpg content")

        mock_result = MagicMock()
        mock_result.returncode = 0

        with patch("subprocess.run", return_value=mock_result):
            assert is_gpg_encrypted(test_file) is True

    def test_returns_false_for_plain_file(self, tmp_path: Path):
        """Test returns False when gpg --list-packets fails."""
        test_file = tmp_path / "test.txt"
        test_file.write_text("plain text")

        mock_result = MagicMock()
        mock_result.returncode = 1

        with patch("subprocess.run", return_value=mock_result):
            assert is_gpg_encrypted(test_file) is False

    def test_returns_false_on_gpg_error(self, tmp_path: Path):
        """Test returns False when gpg command fails."""
        test_file = tmp_path / "test.gpg"
        test_file.write_text("content")

        with patch("subprocess.run", side_effect=FileNotFoundError):
            assert is_gpg_encrypted(test_file) is False


class TestGpgEncryptFile:
    """Tests for gpg_encrypt_file."""

    def test_encrypts_file_successfully(self, tmp_path: Path):
        """Test successful encryption."""
        input_file = tmp_path / "secret.txt"
        input_file.write_text("secret data")
        output_file = tmp_path / "secret.txt.gpg"

        mock_result = MagicMock()
        mock_result.returncode = 0

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            with patch(
                "djb.secrets.gpg.setup_gpg_tty", return_value={"GPG_TTY": "/dev/tty"}
            ):
                # Create the temp file that would be created by gpg
                temp_file = output_file.with_suffix(".gpg.tmp")
                temp_file.write_text("encrypted")

                result = gpg_encrypt_file(input_file, output_file)

                assert result == output_file
                mock_run.assert_called_once()
                call_args = mock_run.call_args
                assert "gpg" in call_args[0][0]
                assert "--symmetric" in call_args[0][0]

    def test_raises_on_encryption_failure(self, tmp_path: Path):
        """Test raises GpgError on encryption failure."""
        input_file = tmp_path / "secret.txt"
        input_file.write_text("secret data")
        output_file = tmp_path / "secret.txt.gpg"

        mock_result = MagicMock()
        mock_result.returncode = 1
        mock_result.stderr = "encryption failed"

        with patch("subprocess.run", return_value=mock_result):
            with patch(
                "djb.secrets.gpg.setup_gpg_tty", return_value={"GPG_TTY": "/dev/tty"}
            ):
                with pytest.raises(GpgError, match="encryption failed"):
                    gpg_encrypt_file(input_file, output_file)

    def test_uses_armor_by_default(self, tmp_path: Path):
        """Test uses ASCII armor by default."""
        input_file = tmp_path / "secret.txt"
        input_file.write_text("secret data")
        output_file = tmp_path / "secret.txt.gpg"

        mock_result = MagicMock()
        mock_result.returncode = 0

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            with patch(
                "djb.secrets.gpg.setup_gpg_tty", return_value={"GPG_TTY": "/dev/tty"}
            ):
                temp_file = output_file.with_suffix(".gpg.tmp")
                temp_file.write_text("encrypted")

                gpg_encrypt_file(input_file, output_file)

                call_args = mock_run.call_args[0][0]
                assert "--armor" in call_args


class TestGpgDecryptFile:
    """Tests for gpg_decrypt_file."""

    def test_decrypts_file_successfully(self, tmp_path: Path):
        """Test successful decryption to file."""
        input_file = tmp_path / "secret.txt.gpg"
        input_file.write_text("encrypted data")
        output_file = tmp_path / "secret.txt"

        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = ""

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            with patch(
                "djb.secrets.gpg.setup_gpg_tty", return_value={"GPG_TTY": "/dev/tty"}
            ):
                # Create output file that would be created by gpg
                output_file.write_text("decrypted")

                gpg_decrypt_file(input_file, output_file)

                mock_run.assert_called_once()
                call_args = mock_run.call_args
                assert "gpg" in call_args[0][0]
                assert "--decrypt" in call_args[0][0]

    def test_returns_content_without_output_path(self, tmp_path: Path):
        """Test returns decrypted content when no output path specified."""
        input_file = tmp_path / "secret.txt.gpg"
        input_file.write_text("encrypted data")

        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "decrypted content"

        with patch("subprocess.run", return_value=mock_result):
            with patch(
                "djb.secrets.gpg.setup_gpg_tty", return_value={"GPG_TTY": "/dev/tty"}
            ):
                result = gpg_decrypt_file(input_file)
                assert result == "decrypted content"

    def test_raises_on_decryption_failure(self, tmp_path: Path):
        """Test raises GpgError on decryption failure."""
        input_file = tmp_path / "secret.txt.gpg"
        input_file.write_text("encrypted data")

        mock_result = MagicMock()
        mock_result.returncode = 1
        mock_result.stderr = "decryption failed"

        with patch("subprocess.run", return_value=mock_result):
            with patch(
                "djb.secrets.gpg.setup_gpg_tty", return_value={"GPG_TTY": "/dev/tty"}
            ):
                with pytest.raises(GpgError, match="decryption failed"):
                    gpg_decrypt_file(input_file)
