"""Tests for Pydantic Settings integration."""

from unittest.mock import Mock, patch

import pytest
from pydantic import Field, ValidationError

from scottzach1.onepassword_cli.exceptions import (
    OnePasswordNotAuthenticatedError,
    OnePasswordNotInstalledError,
)
from scottzach1.onepassword_cli.settings import OnePasswordSettings


class TestOnePasswordSettings:
    """Tests for OnePasswordSettings class."""

    @patch("subprocess.run")
    def test_settings_without_secrets(self, mock_run: Mock) -> None:
        """Test settings initialization without any secrets."""

        class AppSettings(OnePasswordSettings):
            host: str = "localhost"
            port: int = 8080
            debug: bool = True

        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
        ]

        settings = AppSettings()
        assert settings.host == "localhost"
        assert settings.port == 8080
        assert settings.debug is True

    @patch("subprocess.run")
    def test_settings_with_secrets(self, mock_run: Mock) -> None:
        """Test settings initialization with secret injection."""

        class AppSettings(OnePasswordSettings):
            database_url: str
            api_key: str
            port: int = 5432

        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_url": "postgres://user:pass@host/db", "api_key": "secret_key_123", "port": 5432}',
                stderr="",
            ),
        ]

        settings = AppSettings(
            database_url="op://vault/database/url",
            api_key="op://vault/api/key",
        )
        assert settings.database_url == "postgres://user:pass@host/db"
        assert settings.api_key == "secret_key_123"
        assert settings.port == 5432

    @patch("subprocess.run")
    def test_settings_validation_after_injection(self, mock_run: Mock) -> None:
        """Test that Pydantic validation runs after secret injection."""

        class AppSettings(OnePasswordSettings):
            port: int
            email: str

        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='{"port": "not_a_number", "email": "test@example.com"}',
                stderr="",
            ),
        ]

        # Should fail validation because injected port value is not convertible to int
        with pytest.raises(ValidationError):
            AppSettings(
                port="op://vault/app/port",
                email="op://vault/app/email",
            )

    @patch("subprocess.run")
    def test_settings_with_dict_values(self, mock_run: Mock) -> None:
        """Test settings with dictionary values containing secrets."""

        class AppSettings(OnePasswordSettings):
            config: dict[str, str | int]

        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='{"config": {"host": "db.example.com", "port": 5432, "password": "secret_pass"}}',
                stderr="",
            ),
        ]

        settings = AppSettings(
            config={
                "host": "op://vault/db/host",
                "port": 5432,
                "password": "op://vault/db/password",
            }
        )
        assert settings.config["host"] == "db.example.com"
        assert settings.config["password"] == "secret_pass"
        assert settings.config["port"] == 5432

    @patch("subprocess.run")
    def test_settings_with_field_validators(self, mock_run: Mock) -> None:
        """Test that field validators work after secret injection."""

        class AppSettings(OnePasswordSettings):
            api_key: str = Field(..., min_length=10)
            port: int = Field(..., ge=1, le=65535)

        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='{"api_key": "secret_key_123456", "port": 8080}',
                stderr="",
            ),
        ]

        settings = AppSettings(
            api_key="op://vault/api/key",
            port="op://vault/app/port",
        )
        assert settings.api_key == "secret_key_123456"
        assert settings.port == 8080

    @patch("subprocess.run")
    def test_settings_not_installed_error(self, mock_run: Mock) -> None:
        """Test that OnePasswordNotInstalledError is propagated."""

        class AppSettings(OnePasswordSettings):
            secret: str

        mock_run.side_effect = FileNotFoundError()

        with pytest.raises(OnePasswordNotInstalledError):
            AppSettings(secret="op://vault/item/field")

    @patch("subprocess.run")
    def test_settings_not_authenticated_error(self, mock_run: Mock) -> None:
        """Test that OnePasswordNotAuthenticatedError is propagated."""

        class AppSettings(OnePasswordSettings):
            secret: str

        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):
            AppSettings(secret="op://vault/item/field")

    @patch("subprocess.run")
    def test_settings_with_env_prefix(self, mock_run: Mock, monkeypatch: pytest.MonkeyPatch) -> None:
        """Test settings with environment variable prefix."""

        class AppSettings(OnePasswordSettings):
            model_config = {"env_prefix": "APP_"}
            secret: str

        monkeypatch.setenv("APP_SECRET", "op://vault/item/field")

        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_value"}', stderr=""),  # op inject
        ]

        settings = AppSettings()
        assert settings.secret == "injected_value"

    @patch("subprocess.run")
    def test_settings_extra_allow(self, mock_run: Mock) -> None:
        """Test that extra fields are allowed by default."""

        class AppSettings(OnePasswordSettings):
            known_field: str = "value"

        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='{"known_field": "value", "extra_field": "extra_value"}',
                stderr="",
            ),
        ]

        settings = AppSettings(extra_field="op://vault/item/field")
        assert settings.known_field == "value"
        assert settings.extra_field == "extra_value"  # type: ignore[attr-defined]

    @patch("subprocess.run")
    def test_settings_with_no_secrets(self, mock_run: Mock) -> None:
        """Test that settings without secrets work correctly."""

        class AppSettings(OnePasswordSettings):
            value: str = "default"
            number: int = 42

        # Mock CLI checks since they'll be called even with no secrets
        mock_run.side_effect = [
            Mock(returncode=0, stdout="2.0.0", stderr=""),  # op --version
            Mock(returncode=0, stdout="accounts", stderr=""),  # op account list
        ]

        settings = AppSettings()
        assert settings.value == "default"
        assert settings.number == 42
