"""Tests for AWS Secrets Manager integration."""

import json
import os
import sys
from unittest.mock import MagicMock, patch

import pytest

from lumenova_beacon.core.secrets import (
    _traverse_secret,
    resolve_api_key_from_aws,
    resolve_aws_secret,
)
from lumenova_beacon.exceptions import ConfigurationError


class TestTraverseSecret:
    """Test dot-notation secret traversal."""

    def test_simple_key(self):
        data = {"api_key": "sk-123"}
        assert _traverse_secret(data, "api_key") == "sk-123"

    def test_nested_key(self):
        data = {"beacon": {"api_key": "sk-456"}}
        assert _traverse_secret(data, "beacon.api_key") == "sk-456"

    def test_deep_nesting(self):
        data = {"level1": {"level2": {"level3": "deep-value"}}}
        assert _traverse_secret(data, "level1.level2.level3") == "deep-value"

    def test_missing_key_returns_none(self):
        data = {"beacon": {"api_key": "sk-123"}}
        assert _traverse_secret(data, "beacon.missing") is None

    def test_missing_intermediate_key_returns_none(self):
        data = {"beacon": {"api_key": "sk-123"}}
        assert _traverse_secret(data, "wrong.api_key") is None

    def test_non_dict_intermediate_returns_none(self):
        data = {"beacon": "not-a-dict"}
        assert _traverse_secret(data, "beacon.api_key") is None

    def test_none_value_returns_none(self):
        data = {"api_key": None}
        assert _traverse_secret(data, "api_key") is None

    def test_numeric_value_converted_to_string(self):
        data = {"port": 8080}
        assert _traverse_secret(data, "port") == "8080"


class TestResolveAwsSecret:
    """Test AWS Secrets Manager retrieval."""

    @patch("boto3.client")
    def test_simple_json_secret(self, mock_client_factory):
        mock_sm = MagicMock()
        mock_client_factory.return_value = mock_sm
        mock_sm.get_secret_value.return_value = {
            "SecretString": json.dumps({"api_key": "sk-from-aws"})
        }

        result = resolve_aws_secret("my-secret", "api_key")
        assert result == "sk-from-aws"
        mock_client_factory.assert_called_once_with("secretsmanager", region_name=None)

    @patch("boto3.client")
    def test_nested_json_secret(self, mock_client_factory):
        mock_sm = MagicMock()
        mock_client_factory.return_value = mock_sm
        mock_sm.get_secret_value.return_value = {
            "SecretString": json.dumps({"beacon": {"api_key": "sk-nested"}})
        }

        result = resolve_aws_secret("my-secret", "beacon.api_key")
        assert result == "sk-nested"

    @patch("boto3.client")
    def test_raw_secret_when_no_key(self, mock_client_factory):
        mock_sm = MagicMock()
        mock_client_factory.return_value = mock_sm
        mock_sm.get_secret_value.return_value = {
            "SecretString": "raw-api-key-string"
        }

        result = resolve_aws_secret("my-secret", None)
        assert result == "raw-api-key-string"

    @patch("boto3.client")
    def test_region_passed_to_boto3(self, mock_client_factory):
        mock_sm = MagicMock()
        mock_client_factory.return_value = mock_sm
        mock_sm.get_secret_value.return_value = {
            "SecretString": json.dumps({"api_key": "sk-123"})
        }

        resolve_aws_secret("my-secret", "api_key", region="eu-west-1")
        mock_client_factory.assert_called_once_with(
            "secretsmanager", region_name="eu-west-1"
        )

    def test_boto3_not_installed_raises_configuration_error(self):
        with patch.dict(sys.modules, {"boto3": None, "botocore": None}):
            with pytest.raises(ConfigurationError, match="boto3 is required"):
                resolve_aws_secret("my-secret", "api_key")

    @patch("boto3.client")
    def test_client_error_returns_none(self, mock_client_factory):
        from botocore.exceptions import ClientError

        mock_sm = MagicMock()
        mock_client_factory.return_value = mock_sm
        mock_sm.get_secret_value.side_effect = ClientError(
            {"Error": {"Code": "ResourceNotFoundException", "Message": "Secret not found"}},
            "GetSecretValue",
        )

        result = resolve_aws_secret("nonexistent-secret", "api_key")
        assert result is None

    @patch("boto3.client")
    def test_non_json_secret_with_key_returns_none(self, mock_client_factory):
        mock_sm = MagicMock()
        mock_client_factory.return_value = mock_sm
        mock_sm.get_secret_value.return_value = {
            "SecretString": "not-json-content"
        }

        result = resolve_aws_secret("my-secret", "api_key")
        assert result is None

    @patch("boto3.client")
    def test_no_secret_string_returns_none(self, mock_client_factory):
        mock_sm = MagicMock()
        mock_client_factory.return_value = mock_sm
        mock_sm.get_secret_value.return_value = {"SecretString": None}

        result = resolve_aws_secret("my-secret", "api_key")
        assert result is None


class TestResolveApiKeyFromAws:
    """Test env-var-driven API key resolution."""

    def test_returns_none_when_no_env_vars(self):
        with patch.dict(os.environ, {}, clear=True):
            result = resolve_api_key_from_aws()
            assert result is None

    @patch("lumenova_beacon.core.secrets.resolve_aws_secret")
    def test_returns_key_when_configured(self, mock_resolve):
        mock_resolve.return_value = "sk-from-aws"

        with patch.dict(os.environ, {"BEACON_AWS_SECRET_NAME": "my-secret"}):
            result = resolve_api_key_from_aws()

        assert result == "sk-from-aws"
        mock_resolve.assert_called_once_with("my-secret", "api_key", None)

    @patch("lumenova_beacon.core.secrets.resolve_aws_secret")
    def test_defaults_secret_key_to_api_key(self, mock_resolve):
        mock_resolve.return_value = "sk-123"

        with patch.dict(os.environ, {"BEACON_AWS_SECRET_NAME": "my-secret"}):
            resolve_api_key_from_aws()

        mock_resolve.assert_called_once_with("my-secret", "api_key", None)

    @patch("lumenova_beacon.core.secrets.resolve_aws_secret")
    def test_custom_secret_key(self, mock_resolve):
        mock_resolve.return_value = "sk-nested"

        with patch.dict(os.environ, {
            "BEACON_AWS_SECRET_NAME": "my-secret",
            "BEACON_AWS_SECRET_KEY": "beacon.api_key",
        }):
            resolve_api_key_from_aws()

        mock_resolve.assert_called_once_with("my-secret", "beacon.api_key", None)

    @patch("lumenova_beacon.core.secrets.resolve_aws_secret")
    def test_region_env_var_passed(self, mock_resolve):
        mock_resolve.return_value = "sk-123"

        with patch.dict(os.environ, {
            "BEACON_AWS_SECRET_NAME": "my-secret",
            "BEACON_AWS_REGION": "us-east-1",
        }):
            resolve_api_key_from_aws()

        mock_resolve.assert_called_once_with("my-secret", "api_key", "us-east-1")

    def test_secret_name_not_set_skips_boto3(self):
        """Verify no boto3 call when BEACON_AWS_SECRET_NAME is not set."""
        with patch.dict(os.environ, {}, clear=True):
            with patch("lumenova_beacon.core.secrets.resolve_aws_secret") as mock_resolve:
                result = resolve_api_key_from_aws()
                mock_resolve.assert_not_called()
                assert result is None
