# region test_llm_provider [TYPE Module]
# @RELATION  BINDS_TO -> [LLMProvider]
# @SEMANTICS: tests, llm-provider, encryption, contract
# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager
# endregion test_llm_provider

import os
import pytest
from unittest.mock import MagicMock

from cryptography.fernet import Fernet
from sqlalchemy.orm import Session

from src.models.llm import LLMProvider
from src.plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType
from src.services.llm_provider import (
    EncryptionManager,
    LLMProviderService,
    is_masked_or_placeholder,
    mask_api_key,
)

# region _test_encryption_key_fixture [TYPE Global]
# @PURPOSE: Ensure encryption-dependent provider tests run with a valid Fernet key.
# @RELATION  DEPENDS_ON -> [EXT:Library:pytest]
os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode())
# endregion _test_encryption_key_fixture


# region test_provider_type_enum_values [TYPE Function]
# @RELATION  BINDS_TO -> [LLMProviderType]
# @PURPOSE: Regression guard — prevent accidental value drift when enum variants are added or renamed.
# @INVARIANT  Every LLMProviderType member must have a value matching its lowercase name.
def test_provider_type_enum_values():
    """Verify all LLMProviderType enum values match their expected strings."""
    assert LLMProviderType.OPENAI.value == "openai"
    assert LLMProviderType.OPENROUTER.value == "openrouter"
    assert LLMProviderType.KILO.value == "kilo"
    assert LLMProviderType.LITELLM.value == "litellm"
    # If new providers are added, extend this list — enum value drift breaks string comparisons
    # across executor.py, preview.py, ProviderConfig.svelte, and the DB layer.
    expected_count = 4
    assert len(LLMProviderType) == expected_count, (
        f"Expected {expected_count} provider types, got {len(LLMProviderType)}. "
        "If you added a new one, add its value assertion above and update expected_count."
    )
# endregion test_provider_type_enum_values


# @TEST_CONTRACT  EncryptionManagerModel -> Invariants
# @TEST_INVARIANT  symmetric_encryption
# region test_encryption_cycle [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify EncryptionManager round-trip encryption/decryption invariant for non-empty secrets.
def test_encryption_cycle():
    """Verify encrypted data can be decrypted back to original string."""
    manager = EncryptionManager()
    original = "secret_api_key_123"
    encrypted = manager.encrypt(original)
    assert encrypted != original
    assert manager.decrypt(encrypted) == original


# @TEST_EDGE  empty_string_encryption
# endregion test_encryption_cycle


# region test_empty_string_encryption [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify EncryptionManager preserves empty-string payloads through encrypt/decrypt cycle.
def test_empty_string_encryption():
    manager = EncryptionManager()
    original = ""
    encrypted = manager.encrypt(original)
    assert manager.decrypt(encrypted) == ""


# @TEST_EDGE  decrypt_invalid_data
# endregion test_empty_string_encryption


# region test_decrypt_invalid_data [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure decrypt rejects invalid ciphertext input by raising an exception.
def test_decrypt_invalid_data():
    manager = EncryptionManager()
    with pytest.raises(Exception):
        manager.decrypt("not-encrypted-string")


# @TEST_FIXTURE  mock_db_session
# endregion test_decrypt_invalid_data


# region mock_db [TYPE Fixture]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.
# @INVARIANT  Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.
@pytest.fixture
def mock_db():
    # @RISK: query() returns unconstrained MagicMock — chain beyond query() has no spec protection. Consider create_autospec(Session) for full chain safety.
    return MagicMock(spec=Session)


# endregion mock_db


# region service [TYPE Fixture]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests.
@pytest.fixture
def service(mock_db):
    return LLMProviderService(db=mock_db)


# endregion service


# region test_get_all_providers [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify provider list retrieval issues query/all calls on the backing DB session.
def test_get_all_providers(service, mock_db):
    service.get_all_providers()
    mock_db.query.assert_called()
    mock_db.query().all.assert_called()


# endregion test_get_all_providers


# region test_create_provider [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure provider creation persists entity and stores API key in encrypted form.
def test_create_provider(service, mock_db):
    config = LLMProviderConfig(
        provider_type=LLMProviderType.OPENAI,
        name="Test OpenAI",
        base_url="https://api.openai.com",
        api_key="sk-test",
        default_model="gpt-4",
        is_active=True,
    )

    provider = service.create_provider(config)

    mock_db.add.assert_called()
    mock_db.commit.assert_called()
    # Verify API key was encrypted
    assert provider.api_key != "sk-test"
    # Decrypt to verify it matches
    assert EncryptionManager().decrypt(provider.api_key) == "sk-test"


# endregion test_create_provider


# region test_get_decrypted_api_key [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify service decrypts stored provider API key for an existing provider record.
def test_get_decrypted_api_key(service, mock_db):
    # Setup mock provider
    encrypted_key = EncryptionManager().encrypt("secret-value")
    mock_provider = LLMProvider(id="p1", api_key=encrypted_key)
    mock_db.query().filter().first.return_value = mock_provider

    key = service.get_decrypted_api_key("p1")
    assert key == "secret-value"


# endregion test_get_decrypted_api_key


# region test_get_decrypted_api_key_not_found [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify missing provider lookup returns None instead of attempting decryption.
def test_get_decrypted_api_key_not_found(service, mock_db):
    mock_db.query().filter().first.return_value = None
    assert service.get_decrypted_api_key("missing") is None


# endregion test_get_decrypted_api_key_not_found


# region test_update_provider_ignores_masked_placeholder_api_key [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure masked placeholder API keys do not overwrite previously encrypted provider secrets.
def test_update_provider_ignores_masked_placeholder_api_key(service, mock_db):
    existing_encrypted = EncryptionManager().encrypt("secret-value")
    mock_provider = LLMProvider(
        id="p1",
        provider_type="openai",
        name="Existing",
        base_url="https://api.openai.com/v1",
        api_key=existing_encrypted,
        default_model="gpt-4o",
        is_active=True,
    )
    mock_db.query().filter().first.return_value = mock_provider
    config = LLMProviderConfig(
        id="p1",
        provider_type=LLMProviderType.OPENAI,
        name="Existing",
        base_url="https://api.openai.com/v1",
        api_key="********",
        default_model="gpt-4o",
        is_active=False,
    )

    updated = service.update_provider("p1", config)

    assert updated is mock_provider
    assert updated.api_key == existing_encrypted
    assert EncryptionManager().decrypt(updated.api_key) == "secret-value"
    assert updated.is_active is False


# endregion test_update_provider_ignores_masked_placeholder_api_key


# region test_mask_api_key [TYPE Function]
# @RELATION  BINDS_TO -> [mask_api_key]
# @PURPOSE: Verify mask_api_key produces correct masked strings for various key lengths and edge cases.
# @INVARIANT  mask_api_key never reveals more than 4 chars from any position.
# @TEST_EDGE  None -> ""
# @TEST_EDGE  "" -> ""
# @TEST_EDGE  "abcd" -> "****"
# @TEST_EDGE  "abcdef" -> "ab...ef"
# @TEST_EDGE  "abcdefgh" -> "ab...gh"
# @TEST_EDGE  "sk-test-key-1234abcd" -> "sk-t...abcd"
def test_mask_api_key():
    assert mask_api_key(None) == ""
    assert mask_api_key("") == ""
    assert mask_api_key("abcd") == "****"
    assert mask_api_key("abcdef") == "ab...ef"
    assert mask_api_key("abcdefgh") == "ab...gh"
    assert mask_api_key("sk-test-key-1234abcd") == "sk-t...abcd"


# endregion test_mask_api_key


# region test_is_masked_or_placeholder [TYPE Function]
# @RELATION  BINDS_TO -> [is_masked_or_placeholder]
# @PURPOSE: Verify predicate correctly identifies all forms of masked/placeholder API keys.
# @INVARIANT  Real API keys (no "..." pattern) always return False.
# @TEST_EDGE  None -> True
# @TEST_EDGE  "" -> True
# @TEST_EDGE  "********" -> True
# @TEST_EDGE  "sk-...abcd" -> True
# @TEST_EDGE  "...xyz" -> True
# @TEST_EDGE  "sk-real-key-1234" -> False
# @TEST_EDGE  "short" -> False
def test_is_masked_or_placeholder():
    assert is_masked_or_placeholder(None) is True
    assert is_masked_or_placeholder("") is True
    assert is_masked_or_placeholder("********") is True
    assert is_masked_or_placeholder("sk-...abcd") is True
    assert is_masked_or_placeholder("...xyz") is True
    assert is_masked_or_placeholder("sk-real-key-1234") is False
    assert is_masked_or_placeholder("short") is False


# endregion test_is_masked_or_placeholder


# region test_create_provider_with_multimodal_flag [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify provider creation persists the is_multimodal flag.
def test_create_provider_with_multimodal_flag(service, mock_db):
    """Verify is_multimodal=True is stored during provider creation."""
    config = LLMProviderConfig(
        provider_type=LLMProviderType.OPENAI,
        name="Vision Provider",
        base_url="https://api.openai.com",
        api_key="sk-vision-key",
        default_model="gpt-4o",
        is_active=True,
        is_multimodal=True,
    )

    provider = service.create_provider(config)

    assert provider.is_multimodal is True
    mock_db.add.assert_called()
    mock_db.commit.assert_called()


# endregion test_create_provider_with_multimodal_flag


# region test_create_provider_without_multimodal_flag [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify is_multimodal defaults to False when not specified.
def test_create_provider_without_multimodal_flag(service, mock_db):
    """Verify is_multimodal defaults to False."""
    config = LLMProviderConfig(
        provider_type=LLMProviderType.OPENAI,
        name="Text Provider",
        base_url="https://api.openai.com",
        api_key="sk-text-key",
        default_model="gpt-4.1-mini",
        is_active=True,
    )

    provider = service.create_provider(config)

    assert provider.is_multimodal is False


# endregion test_create_provider_without_multimodal_flag


# region test_update_provider_preserves_is_multimodal [TYPE Function]
# @RELATION  BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify updating a provider preserves and correctly sets is_multimodal.
def test_update_provider_preserves_is_multimodal(service, mock_db):
    """Verify is_multimodal is updated correctly."""
    existing_encrypted = EncryptionManager().encrypt("secret-value")
    mock_provider = LLMProvider(
        id="p1",
        provider_type="openai",
        name="Vision",
        base_url="https://api.openai.com/v1",
        api_key=existing_encrypted,
        default_model="gpt-4o",
        is_active=True,
        is_multimodal=True,
    )
    mock_db.query().filter().first.return_value = mock_provider

    # Update with is_multimodal=False
    config = LLMProviderConfig(
        id="p1",
        provider_type=LLMProviderType.OPENAI,
        name="Vision",
        base_url="https://api.openai.com/v1",
        api_key="********",
        default_model="gpt-4o",
        is_active=True,
        is_multimodal=False,
    )

    updated = service.update_provider("p1", config)

    assert updated.is_multimodal is False


# endregion test_update_provider_preserves_is_multimodal


# region test_llm_provider_config_multimodal_default [TYPE Function]
# @RELATION  BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig.is_multimodal defaults to False for schema stability.
def test_llm_provider_config_multimodal_default():
    """Verify default is_multimodal is False in schema."""
    config = LLMProviderConfig(
        provider_type=LLMProviderType.OPENAI,
        name="Default Test",
        base_url="https://api.openai.com/v1",
        api_key="sk-test",
        default_model="gpt-4",
    )
    assert config.is_multimodal is False


# endregion test_llm_provider_config_multimodal_default


# region test_llm_provider_config_multimodal_explicit [TYPE Function]
# @RELATION  BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig accepts explicit is_multimodal=True.
def test_llm_provider_config_multimodal_explicit():
    """Verify setting is_multimodal=True explicitly works."""
    config = LLMProviderConfig(
        provider_type=LLMProviderType.OPENAI,
        name="Vision Test",
        base_url="https://api.openai.com/v1",
        api_key="sk-test",
        default_model="gpt-4o",
        is_multimodal=True,
    )
    assert config.is_multimodal is True
