"""Tests for the core injection logic."""

from unittest.mock import Mock, patch

import pytest

from scottzach1.onepassword_cli.exceptions import (
    OnePasswordNotAuthenticatedError,
    OnePasswordNotInstalledError,
    SecretInjectionError,
)
from scottzach1.onepassword_cli.injector import (
    inject_dictionary_1password,
    inject_string_1password,
)


class TestCheckCLIAvailable:
    """Tests for CLI availability checking."""

    @patch("subprocess.run")
    def test_cli_available(self, mock_run: Mock) -> None:
        """Test successful CLI availability check."""
        mock_run.return_value = Mock(returncode=0, stdout="2.0.0", stderr="")
        # Should not raise
        inject_string_1password("no secrets", check_cli=True)

    @patch("subprocess.run")
    def test_cli_not_installed(self, mock_run: Mock) -> None:
        """Test exception when CLI is not installed."""
        mock_run.side_effect = FileNotFoundError()
        with pytest.raises(OnePasswordNotInstalledError):
            inject_string_1password("op://vault/item/field", check_cli=True)

    @patch("subprocess.run")
    def test_cli_returns_error(self, mock_run: Mock) -> None:
        """Test exception when CLI command fails."""
        mock_run.return_value = Mock(returncode=1, stdout="", stderr="error")
        with pytest.raises(OnePasswordNotInstalledError):
            inject_string_1password("op://vault/item/field", check_cli=True)


class TestCheckAuthenticated:
    """Tests for authentication checking."""

    @patch("subprocess.run")
    def test_authenticated(self, mock_run: Mock) -> None:
        """Test successful authentication check."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="account list", stderr=""),  # op account list
        ]
        # Should not raise
        inject_string_1password("no secrets", check_cli=True)

    @patch("subprocess.run")
    def test_not_authenticated(self, mock_run: Mock) -> None:
        """Test exception when not authenticated."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=1, stdout="", stderr="not signed in"),  # op account list
        ]
        with pytest.raises(OnePasswordNotAuthenticatedError):
            inject_string_1password("op://vault/item/field", check_cli=True)


class TestInjectString:
    """Tests for inject_string_1password function."""

    def test_no_secrets(self) -> None:
        """Test that strings without secrets are returned unchanged."""
        result = inject_string_1password("Just a regular string", check_cli=False)
        assert result == "Just a regular string"

    def test_no_secrets_no_cli_call(self) -> None:
        """Test that no CLI calls are made when there are no secrets."""
        with patch("subprocess.run") as mock_run:
            result = inject_string_1password("No secrets here", check_cli=False)
            assert result == "No secrets here"
            mock_run.assert_not_called()

    @patch("subprocess.run")
    def test_inject_single_secret(self, mock_run: Mock) -> None:
        """Test injection of a single secret."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(returncode=0, stdout="my_secret_value", stderr=""),  # op inject
        ]
        result = inject_string_1password("op://vault/item/password", check_cli=True)
        assert result == "my_secret_value"

    @patch("subprocess.run")
    def test_inject_multiple_secrets(self, mock_run: Mock) -> None:
        """Test injection of multiple secrets in one string."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(returncode=0, stdout="user:pass@host", stderr=""),  # op inject
        ]
        result = inject_string_1password(
            "op://vault/db/username:op://vault/db/password@op://vault/db/host",
            check_cli=True,
        )
        assert result == "user:pass@host"

    @patch("subprocess.run")
    def test_inject_with_surrounding_text(self, mock_run: Mock) -> None:
        """Test injection with surrounding text."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(returncode=0, stdout="The password is secret123", stderr=""),  # op inject
        ]
        result = inject_string_1password("The password is op://vault/item/password", check_cli=True)
        assert result == "The password is secret123"

    @patch("subprocess.run")
    def test_injection_failure(self, mock_run: Mock) -> None:
        """Test exception when injection fails."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(returncode=1, stdout="", stderr="item not found"),  # op inject
        ]
        with pytest.raises(SecretInjectionError) as exc_info:
            inject_string_1password("op://vault/item/password", check_cli=True)
        assert "item not found" in str(exc_info.value)

    @patch("subprocess.run")
    def test_skip_cli_check(self, mock_run: Mock) -> None:
        """Test that CLI checks are skipped when check_cli=False."""
        mock_run.return_value = Mock(returncode=0, stdout="my_secret", stderr="")
        result = inject_string_1password("op://vault/item/field", check_cli=False)
        assert result == "my_secret"
        # Only op inject should be called, not version or account checks
        assert mock_run.call_count == 1


class TestInjectDictionary:
    """Tests for inject_dictionary_1password function."""

    def test_no_secrets(self) -> None:
        """Test that dictionaries without secrets are returned unchanged."""
        data = {"key1": "value1", "key2": "value2", "port": 5432}
        result = inject_dictionary_1password(data, check_cli=False)
        assert result == data
        assert result is not data  # Should be a copy

    def test_no_secrets_no_cli_call(self) -> None:
        """Test that no CLI calls are made when there are no secrets."""
        with patch("subprocess.run") as mock_run:
            data = {"key": "value", "number": 42}
            result = inject_dictionary_1password(data, check_cli=False)
            assert result == data
            mock_run.assert_not_called()

    @patch("subprocess.run")
    def test_inject_single_secret(self, mock_run: Mock) -> None:
        """Test injection of a single secret in dictionary."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(returncode=0, stdout='{"password": "my_secret"}', stderr=""),  # op inject
        ]
        data = {"password": "op://vault/item/password"}
        result = inject_dictionary_1password(data, check_cli=True)
        assert result["password"] == "my_secret"

    @patch("subprocess.run")
    def test_type_preservation(self, mock_run: Mock) -> None:
        """Test that non-string types are preserved."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(
                returncode=0,
                stdout='{"password": "secret", "port": 5432, "enabled": true, "timeout": null}',
                stderr="",
            ),
        ]
        data = {
            "password": "op://vault/item/password",
            "port": 5432,
            "enabled": True,
            "timeout": None,
        }
        result = inject_dictionary_1password(data, check_cli=True)
        assert result["password"] == "secret"
        assert result["port"] == 5432
        assert isinstance(result["port"], int)
        assert result["enabled"] is True
        assert isinstance(result["enabled"], bool)
        assert result["timeout"] is None

    @patch("subprocess.run")
    def test_nested_dictionary(self, mock_run: Mock) -> None:
        """Test injection in nested dictionaries."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(
                returncode=0,
                stdout='{"database": {"password": "secret", "port": 5432}}',
                stderr="",
            ),
        ]
        data = {"database": {"password": "op://vault/db/password", "port": 5432}}
        result = inject_dictionary_1password(data, check_cli=True)
        assert result["database"]["password"] == "secret"
        assert result["database"]["port"] == 5432

    @patch("subprocess.run")
    def test_list_values(self, mock_run: Mock) -> None:
        """Test handling of list values in dictionary."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(
                returncode=0,
                stdout='{"hosts": ["secret1", "secret2"], "ports": [80, 443]}',
                stderr="",
            ),
        ]
        data = {
            "hosts": ["op://vault/item/host1", "op://vault/item/host2"],
            "ports": [80, 443],
        }
        result = inject_dictionary_1password(data, check_cli=True)
        assert result["hosts"] == ["secret1", "secret2"]
        assert result["ports"] == [80, 443]

    @patch("subprocess.run")
    def test_in_place_modification(self, mock_run: Mock) -> None:
        """Test in-place modification of dictionary."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(returncode=0, stdout='{"password": "secret"}', stderr=""),  # op inject
        ]
        data = {"password": "op://vault/item/password"}
        result = inject_dictionary_1password(data, check_cli=True, in_place=True)
        assert result is data  # Should be the same object
        assert data["password"] == "secret"

    @patch("subprocess.run")
    def test_skip_cli_check(self, mock_run: Mock) -> None:
        """Test that CLI checks are skipped when check_cli=False."""
        mock_run.return_value = Mock(returncode=0, stdout='{"key": "secret"}', stderr="")
        data = {"key": "op://vault/item/field"}
        result = inject_dictionary_1password(data, check_cli=False)
        assert result["key"] == "secret"
        # Only op inject should be called
        assert mock_run.call_count == 1

    @patch("subprocess.run")
    def test_mixed_secrets_and_plain_values(self, mock_run: Mock) -> None:
        """Test dictionary with both secrets and plain values."""
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
            Mock(
                returncode=0,
                stdout='{"secret": "injected_secret", "plain": "plain_value", "number": 42}',
                stderr="",
            ),
        ]
        data = {"secret": "op://vault/item/field", "plain": "plain_value", "number": 42}
        result = inject_dictionary_1password(data, check_cli=True)
        assert result["secret"] == "injected_secret"
        assert result["plain"] == "plain_value"
        assert result["number"] == 42
