"""
Unit tests for LLM key CRUD API endpoints.

Tests:
  - test_create_llm_key: POST encrypts key and returns {id, provider, label, key_hint, created_at}
    with no api_key in the response
  - test_list_llm_keys_redacted: GET returns list without full key, only key_hint (last 4 chars)
  - test_delete_llm_key: DELETE removes key, subsequent GET no longer includes it
  - test_create_duplicate_provider: POST with same tenant_id+provider returns 409 Conflict
  - test_delete_nonexistent_key: DELETE with unknown key_id returns 404
"""

from __future__ import annotations

import uuid
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import HTTPException

from shared.api.llm_keys import (
    LlmKeyCreate,
    LlmKeyResponse,
    create_llm_key,
    delete_llm_key,
    list_llm_keys,
)
from shared.models.billing import TenantLlmKey


def _make_mock_key(
    tenant_id: uuid.UUID,
    provider: str,
    label: str,
    api_key: str,
) -> TenantLlmKey:
    """Create a mock TenantLlmKey instance."""
    key = MagicMock(spec=TenantLlmKey)
    key.id = uuid.uuid4()
    key.tenant_id = tenant_id
    key.provider = provider
    key.label = label
    key.encrypted_key = f"ENCRYPTED_{api_key}"
    key.key_hint = api_key[-4:]
    key.key_version = 1
    from datetime import datetime, timezone
    key.created_at = datetime.now(tz=timezone.utc)
    return key


@pytest.fixture()
def tenant_id() -> uuid.UUID:
    return uuid.uuid4()


@pytest.fixture()
def mock_session() -> AsyncMock:
    session = AsyncMock()
    session.execute.return_value = MagicMock(
        scalar_one_or_none=MagicMock(return_value=None),
        scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[]))),
    )
    return session


@pytest.mark.asyncio
async def test_create_llm_key(tenant_id: uuid.UUID, mock_session: AsyncMock) -> None:
    """POST encrypts the key and returns {id, provider, label, key_hint, created_at} — no api_key."""
    api_key = "sk-test-openai-key-ABCD"
    body = LlmKeyCreate(provider="openai", label="Production Key", api_key=api_key)

    # Mock: no existing key for this tenant+provider
    mock_session.execute.return_value = MagicMock(
        scalar_one_or_none=MagicMock(return_value=None)
    )

    created_key = _make_mock_key(tenant_id, "openai", "Production Key", api_key)

    # Intercept session.add to capture the key and inject the mock
    def _capture_add(obj: object) -> None:
        # Mimic the ORM populating the key attributes after add+refresh
        pass

    mock_session.add.side_effect = _capture_add

    # Patch refresh to update the "obj" with values from created_key
    async def _mock_refresh(obj: object) -> None:
        obj.id = created_key.id  # type: ignore[union-attr]
        obj.created_at = created_key.created_at  # type: ignore[union-attr]

    mock_session.refresh = AsyncMock(side_effect=_mock_refresh)

    with patch("shared.api.llm_keys._get_encryption_service") as mock_enc:
        enc = MagicMock()
        enc.encrypt.return_value = "FERNET_CIPHERTEXT"
        mock_enc.return_value = enc

        with patch("shared.api.llm_keys._get_tenant_or_404", new_callable=AsyncMock):
            response = await create_llm_key(
                tenant_id=tenant_id,
                body=body,
                session=mock_session,
            )

    assert isinstance(response, LlmKeyResponse)
    assert response.provider == "openai"
    assert response.label == "Production Key"
    assert response.key_hint == api_key[-4:]
    # Response must NOT contain the raw api_key
    response_dict = response.model_dump()
    assert "api_key" not in response_dict
    assert "encrypted_key" not in response_dict


@pytest.mark.asyncio
async def test_list_llm_keys_redacted(tenant_id: uuid.UUID, mock_session: AsyncMock) -> None:
    """GET returns list with key_hint (last 4 chars) — never full key or encrypted_key."""
    api_key1 = "sk-openai-secret-WXYZ"
    api_key2 = "sk-anthropic-key-1234"

    key1 = _make_mock_key(tenant_id, "openai", "OpenAI Key", api_key1)
    key2 = _make_mock_key(tenant_id, "anthropic", "Anthropic Key", api_key2)

    mock_session.execute.return_value = MagicMock(
        scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[key1, key2])))
    )

    with patch("shared.api.llm_keys._get_tenant_or_404", new_callable=AsyncMock):
        response = await list_llm_keys(tenant_id=tenant_id, session=mock_session)

    assert len(response) == 2
    for item in response:
        assert isinstance(item, LlmKeyResponse)
        item_dict = item.model_dump()
        # Must not contain full key or encrypted data
        assert "api_key" not in item_dict
        assert "encrypted_key" not in item_dict
        # Must contain key_hint
        assert item_dict["key_hint"] is not None
        assert len(item_dict["key_hint"]) <= 4


@pytest.mark.asyncio
async def test_delete_llm_key(tenant_id: uuid.UUID, mock_session: AsyncMock) -> None:
    """DELETE removes the key and returns 204-equivalent (None)."""
    key_id = uuid.uuid4()
    existing_key = _make_mock_key(tenant_id, "openai", "Key to delete", "sk-DELETE-ABCD")

    mock_session.execute.return_value = MagicMock(
        scalar_one_or_none=MagicMock(return_value=existing_key)
    )

    with patch("shared.api.llm_keys._get_tenant_or_404", new_callable=AsyncMock):
        result = await delete_llm_key(
            tenant_id=tenant_id,
            key_id=key_id,
            session=mock_session,
        )

    assert result is None
    mock_session.delete.assert_called_once_with(existing_key)
    mock_session.commit.assert_called_once()


@pytest.mark.asyncio
async def test_create_duplicate_provider(tenant_id: uuid.UUID, mock_session: AsyncMock) -> None:
    """POST with same tenant_id+provider returns 409 Conflict."""
    # Mock: existing key found for this tenant+provider
    existing_key = _make_mock_key(tenant_id, "openai", "Existing Key", "sk-EXISTING-9999")
    mock_session.execute.return_value = MagicMock(
        scalar_one_or_none=MagicMock(return_value=existing_key)
    )

    body = LlmKeyCreate(provider="openai", label="Duplicate Key", api_key="sk-NEW-KEY-0000")

    with patch("shared.api.llm_keys._get_tenant_or_404", new_callable=AsyncMock):
        with pytest.raises(HTTPException) as exc_info:
            await create_llm_key(
                tenant_id=tenant_id,
                body=body,
                session=mock_session,
            )

    assert exc_info.value.status_code == 409


@pytest.mark.asyncio
async def test_delete_nonexistent_key(tenant_id: uuid.UUID, mock_session: AsyncMock) -> None:
    """DELETE with unknown key_id returns 404."""
    key_id = uuid.uuid4()

    # Mock: no key found
    mock_session.execute.return_value = MagicMock(
        scalar_one_or_none=MagicMock(return_value=None)
    )

    with patch("shared.api.llm_keys._get_tenant_or_404", new_callable=AsyncMock):
        with pytest.raises(HTTPException) as exc_info:
            await delete_llm_key(
                tenant_id=tenant_id,
                key_id=key_id,
                session=mock_session,
            )

    assert exc_info.value.status_code == 404
