# #region Test.LLMProvider [C:3] [TYPE Module] [SEMANTICS test,llm,provider,mask,encryption]
# @BRIEF Tests for services/llm_provider.py — mask_api_key, is_masked_or_placeholder, LLMProviderService.
# @RELATION BINDS_TO -> [llm_provider]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

import os
os.environ.setdefault("ENCRYPTION_KEY", "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0=")

from unittest.mock import MagicMock, patch
import pytest


class TestMaskApiKey:
    """mask_api_key — safe display of API keys."""

    def test_none_returns_empty(self):
        from src.services.llm_provider import mask_api_key
        assert mask_api_key(None) == ""

    def test_empty_returns_empty(self):
        from src.services.llm_provider import mask_api_key
        assert mask_api_key("") == ""

    def test_short_key_returns_four_asterisks(self):
        from src.services.llm_provider import mask_api_key
        assert mask_api_key("abc") == "****"
        assert mask_api_key("abcd") == "****"

    def test_medium_key_shows_two_each_side(self):
        from src.services.llm_provider import mask_api_key
        result = mask_api_key("abcdefgh")  # 8 chars
        assert result == "ab...gh"

    def test_long_key_shows_four_each_side(self):
        from src.services.llm_provider import mask_api_key
        result = mask_api_key("sk-1234567890abcdef")  # 18 chars
        assert result == "sk-1...cdef"

    def test_key_length_five_shows_two_each_side(self):
        from src.services.llm_provider import mask_api_key
        result = mask_api_key("abcde")  # 5 chars
        assert result == "ab...de"

    def test_key_length_nine_shows_four_each_side(self):
        from src.services.llm_provider import mask_api_key
        result = mask_api_key("abcdefghi")  # 9 chars
        assert result == "abcd...fghi"


class TestIsMaskedOrPlaceholder:
    """is_masked_or_placeholder — detect masked/placeholder keys."""

    def test_none_returns_true(self):
        from src.services.llm_provider import is_masked_or_placeholder
        assert is_masked_or_placeholder(None) is True

    def test_empty_returns_true(self):
        from src.services.llm_provider import is_masked_or_placeholder
        assert is_masked_or_placeholder("") is True

    def test_placeholder_returns_true(self):
        from src.services.llm_provider import is_masked_or_placeholder
        assert is_masked_or_placeholder("********") is True

    def test_partial_mask_returns_true(self):
        from src.services.llm_provider import is_masked_or_placeholder
        assert is_masked_or_placeholder("sk-...abc") is True

    def test_real_key_returns_false(self):
        from src.services.llm_provider import is_masked_or_placeholder
        assert is_masked_or_placeholder("sk-real-key-12345") is False


class TestLLMProviderService:
    """LLMProviderService — CRUD with encrypted API keys."""

    @pytest.fixture
    def db(self):
        return MagicMock()

    @pytest.fixture
    def service(self, db):
        from src.services.llm_provider import LLMProviderService
        return LLMProviderService(db)

    def test_get_all_providers(self, service, db):
        from src.models.llm import LLMProvider
        expected = [MagicMock(spec=LLMProvider)]
        db.query.return_value.all.return_value = expected
        result = service.get_all_providers()
        assert result == expected
        db.query.assert_called_once_with(LLMProvider)

    def test_get_provider_found(self, service, db):
        from src.models.llm import LLMProvider
        expected = MagicMock(spec=LLMProvider)
        db.query.return_value.filter.return_value.first.return_value = expected
        result = service.get_provider("prov-1")
        assert result is expected
        db.query.assert_called_once_with(LLMProvider)

    def test_get_provider_not_found(self, service, db):
        db.query.return_value.filter.return_value.first.return_value = None
        result = service.get_provider("nonexistent")
        assert result is None

    def test_create_provider(self, service, db):
        from src.services.llm_provider import LLMProviderService
        from src.models.llm import LLMProvider
        # Build a proper config mock with enum-like provider_type
        config = MagicMock()
        config.name = "GPT-4"
        config.provider_type = MagicMock()
        config.provider_type.value = "openai"
        config.api_key = "sk-real-key-12345"
        config.base_url = "https://api.openai.com"
        config.is_active = True
        config.is_multimodal = True
        config.default_model = "gpt-4"
        config.max_images = None
        config.context_window = 8192
        config.max_output_tokens = 4096

        with patch.object(service.encryption, "encrypt", return_value="encrypted-key"):
            with patch("src.services.llm_provider.LLMProvider") as mock_provider_cls:
                mock_provider = MagicMock(spec=LLMProvider)
                mock_provider_cls.return_value = mock_provider
                db.add.return_value = None
                db.commit.return_value = None
                db.refresh.return_value = None

                result = service.create_provider(config)
                assert result is mock_provider
                db.add.assert_called_once()

    def test_update_provider(self, service, db):
        from src.models.llm import LLMProvider
        existing = MagicMock(spec=LLMProvider)
        db.query.return_value.filter.return_value.first.return_value = existing

        config = MagicMock()
        config.name = "GPT-4 Turbo"
        config.provider_type = MagicMock()
        config.provider_type.value = "openai"
        config.api_key = "sk-new-key"
        config.base_url = "https://api.openai.com"
        config.is_active = True
        config.is_multimodal = True
        config.default_model = "gpt-4-turbo"
        config.max_images = None
        config.context_window = 128000
        config.max_output_tokens = 4096

        with patch.object(service.encryption, "encrypt", return_value="new-encrypted"):
            result = service.update_provider("prov-1", config)
            db.commit.assert_called_once()

    def test_delete_provider(self, service, db):
        from src.models.llm import LLMProvider
        existing = MagicMock(spec=LLMProvider)
        db.query.return_value.filter.return_value.first.return_value = existing

        result = service.delete_provider("prov-1")
        db.delete.assert_called_once_with(existing)
        db.commit.assert_called_once()
        assert result is True

    def test_delete_provider_not_found(self, service, db):
        db.query.return_value.filter.return_value.first.return_value = None
        result = service.delete_provider("nonexistent")
        assert result is False

    def test_set_max_images(self, service, db):
        from src.models.llm import LLMProvider
        existing = MagicMock(spec=LLMProvider)
        db.query.return_value.filter.return_value.first.return_value = existing
        result = service.set_max_images("prov-1", 5)
        assert existing.max_images == 5
        db.commit.assert_called_once()
        assert result is existing

    def test_set_max_images_not_found(self, service, db):
        db.query.return_value.filter.return_value.first.return_value = None
        result = service.set_max_images("nonexistent", 5)
        assert result is None

    def test_get_decrypted_api_key(self, service, db):
        from src.models.llm import LLMProvider
        existing = MagicMock(spec=LLMProvider)
        existing.api_key_encrypted = "encrypted-value"
        db.query.return_value.filter.return_value.first.return_value = existing
        with patch.object(service.encryption, "decrypt", return_value="decrypted-key"):
            result = service.get_decrypted_api_key("prov-1")
            assert result == "decrypted-key"

    def test_get_decrypted_api_key_decryption_failure(self, service, db):
        from src.models.llm import LLMProvider
        existing = MagicMock(spec=LLMProvider)
        existing.api_key_encrypted = "corrupted"
        db.query.return_value.filter.return_value.first.return_value = existing
        with patch.object(service.encryption, "decrypt", side_effect=Exception("decrypt failed")):
            result = service.get_decrypted_api_key("prov-1")
            assert result is None

    def test_get_decrypted_api_key_not_found(self, service, db):
        db.query.return_value.filter.return_value.first.return_value = None
        result = service.get_decrypted_api_key("nonexistent")
        assert result is None

    def test_update_provider_skips_encryption_for_masked_key(self):
        """Cover line 151: masked keys skip encryption."""
        from src.services.llm_provider import LLMProviderService
        from src.models.llm import LLMProvider
        db = MagicMock()
        existing = MagicMock(spec=LLMProvider)
        db.query.return_value.filter.return_value.first.return_value = existing
        service = LLMProviderService(db)

        config = MagicMock()
        config.api_key = "********"  # masked
        config.name = "Test"
        config.provider_type = MagicMock()
        config.provider_type.value = "test"
        config.base_url = "http://test"
        config.is_active = True
        config.is_multimodal = False
        config.default_model = None
        config.max_images = None
        config.context_window = None
        config.max_output_tokens = None

        with patch.object(service.encryption, "encrypt") as mock_encrypt:
            result = service.update_provider("prov-1", config)
            mock_encrypt.assert_not_called()
            db.commit.assert_called_once()


class TestLLMProviderServiceAdditional:
    """Additional edge cases for LLMProviderService."""

    def test_update_provider_not_found(self):
        """Line 145: update_provider returns None when provider not found."""
        from src.services.llm_provider import LLMProviderService
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        svc = LLMProviderService(db)
        config = MagicMock()
        result = svc.update_provider("nonexistent", config)
        assert result is None

    def test_decrypt_invalid_tag(self):
        """Lines 226-230: InvalidTag during decryption returns None."""
        from src.services.llm_provider import LLMProviderService
        from src.models.llm import LLMProvider
        from cryptography.exceptions import InvalidTag
        db = MagicMock()
        existing = MagicMock(spec=LLMProvider)
        existing.api_key = "encrypted"
        db.query.return_value.filter.return_value.first.return_value = existing
        svc = LLMProviderService(db)
        with patch.object(svc.encryption, "decrypt", side_effect=InvalidTag):
            result = svc.get_decrypted_api_key("prov-1")
        assert result is None

    def test_decrypt_value_error(self):
        """Lines 232-236: ValueError during decryption returns None."""
        from src.services.llm_provider import LLMProviderService
        from src.models.llm import LLMProvider
        db = MagicMock()
        existing = MagicMock(spec=LLMProvider)
        existing.api_key = "bad-format"
        db.query.return_value.filter.return_value.first.return_value = existing
        svc = LLMProviderService(db)
        with patch.object(svc.encryption, "decrypt", side_effect=ValueError("bad format")):
            result = svc.get_decrypted_api_key("prov-1")
        assert result is None

    def test_get_provider_token_config_not_found(self):
        """Lines 254-257: not found returns None fallback dict."""
        from src.services.llm_provider import LLMProviderService
        db = MagicMock()
        db.query.return_value.filter.return_value.first.return_value = None
        svc = LLMProviderService(db)
        result = svc.get_provider_token_config("nonexistent")
        assert result == {"model": None, "context_window": None, "max_output_tokens": None}

    def test_get_provider_token_config_found(self):
        """Lines 257-261: found returns provider config."""
        from src.services.llm_provider import LLMProviderService
        from src.models.llm import LLMProvider
        db = MagicMock()
        existing = MagicMock(spec=LLMProvider)
        existing.default_model = "gpt-4"
        existing.context_window = 8192
        existing.max_output_tokens = 4096
        db.query.return_value.filter.return_value.first.return_value = existing
        svc = LLMProviderService(db)
        result = svc.get_provider_token_config("prov-1")
        assert result["model"] == "gpt-4"
        assert result["context_window"] == 8192

    def test_get_provider_token_config_default_model_fallback(self):
        """Line 258: model falls back to gpt-4o-mini when None."""
        from src.services.llm_provider import LLMProviderService
        from src.models.llm import LLMProvider
        db = MagicMock()
        existing = MagicMock(spec=LLMProvider)
        existing.default_model = None
        existing.context_window = None
        existing.max_output_tokens = None
        db.query.return_value.filter.return_value.first.return_value = existing
        svc = LLMProviderService(db)
        result = svc.get_provider_token_config("prov-1")
        assert result["model"] == "gpt-4o-mini"
# #endregion Test.LLMProvider
