"""
Story 7.07 — Test Suite
========================
SystemPromptInjector.push_to_telnyx(session_id, assistant_id)

Calls Telnyx API to PATCH /v2/ai_assistants/{assistant_id} with the
memory injection block as system_prompt. Returns True on HTTP 200,
False on any error.

BB Tests (5):
  BB1: Valid credentials + assistant_id → True returned (mock HTTP 200)
  BB2: Invalid API key (HTTP 401) → False returned
  BB3: PATCH request body contains the system_prompt from build_injection
  BB4: HTTP 5xx server error → False returned (no crash)
  BB5: Missing TELNYX_API_KEY env var → False returned immediately

WB Tests (5):
  WB1: API key read from os.environ (not hardcoded — env patched in test)
  WB2: HTTP errors caught and logged (no exception propagation)
  WB3: Request URL matches /v2/ai_assistants/{assistant_id}
  WB4: Authorization header is "Bearer <api_key>"
  WB5: TELNYX_API_BASE constant is correct URL prefix

All external services are mocked via injected http_client.
No real network calls are made.
"""
from __future__ import annotations

import os
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio

from core.injection.system_prompt_injector import (
    SystemPromptInjector,
    TELNYX_API_BASE,
)


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

SESSION_ID = "test-session-707-xyz"
ASSISTANT_ID = "assistant-9c42d3ce-e05a-4e34-8083-c91081917637"
FAKE_API_KEY = "KEY_test_abc123_not_real"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def make_mock_http(status: int = 200) -> MagicMock:
    """
    Return an async-compatible mock HTTP client.

    The mock exposes ``patch(url, json, headers)`` as an async method
    that returns a response object with a ``.status`` attribute.
    This mirrors the aiohttp ClientSession interface.
    """
    response = MagicMock()
    response.status = status

    http = MagicMock()
    http.patch = AsyncMock(return_value=response)
    return http


def make_injector(
    http_status: int = 200,
    api_key: str = FAKE_API_KEY,
    conversation_engine=None,
    redis_client=None,
) -> tuple[SystemPromptInjector, MagicMock]:
    """Build an injector with a mocked HTTP client and patched env var."""
    http = make_mock_http(http_status)
    injector = SystemPromptInjector(
        conversation_engine=conversation_engine,
        redis_client=redis_client,
        http_client=http,
    )
    return injector, http


# ---------------------------------------------------------------------------
# BB1 — Valid credentials + assistant_id → True returned (mock HTTP 200)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb1_http_200_returns_true():
    """BB1: HTTP 200 from Telnyx → push_to_telnyx returns True."""
    injector, _ = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is True, f"Expected True on HTTP 200, got {result}"


@pytest.mark.asyncio
async def test_bb1_http_204_returns_true():
    """BB1: HTTP 204 (No Content) from Telnyx is also a success → True."""
    injector, _ = make_injector(http_status=204)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is True, f"Expected True on HTTP 204, got {result}"


# ---------------------------------------------------------------------------
# BB2 — Invalid API key (HTTP 401) → False returned
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb2_http_401_returns_false():
    """BB2: HTTP 401 Unauthorized → push_to_telnyx returns False."""
    injector, _ = make_injector(http_status=401)
    with patch.dict(os.environ, {"TELNYX_API_KEY": "invalid-key"}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, f"Expected False on HTTP 401, got {result}"


@pytest.mark.asyncio
async def test_bb2_http_403_returns_false():
    """BB2: HTTP 403 Forbidden → push_to_telnyx returns False."""
    injector, _ = make_injector(http_status=403)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, f"Expected False on HTTP 403, got {result}"


# ---------------------------------------------------------------------------
# BB3 — PATCH body contains the system_prompt from build_injection
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb3_patch_body_contains_system_prompt():
    """BB3: The PATCH request JSON body has 'system_prompt' key with injection content."""
    injector, http = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    http.patch.assert_called_once()
    call_kwargs = http.patch.call_args

    # Extract the json= keyword argument
    sent_json = call_kwargs.kwargs.get("json") or (
        call_kwargs.args[1] if len(call_kwargs.args) > 1 else None
    )
    assert sent_json is not None, "No JSON body was sent in the PATCH request"
    assert "system_prompt" in sent_json, (
        f"'system_prompt' key missing from PATCH body: {sent_json}"
    )


@pytest.mark.asyncio
async def test_bb3_system_prompt_contains_injection_markers():
    """BB3: The system_prompt value in the PATCH body contains AIVA injection markers."""
    injector, http = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    call_kwargs = http.patch.call_args
    sent_json = call_kwargs.kwargs.get("json") or (
        call_kwargs.args[1] if len(call_kwargs.args) > 1 else None
    )
    system_prompt_value = sent_json["system_prompt"]

    assert "AIVA MEMORY INJECTION" in system_prompt_value, (
        "AIVA MEMORY INJECTION header marker missing from system_prompt payload"
    )
    assert "END MEMORY INJECTION" in system_prompt_value, (
        "END MEMORY INJECTION footer marker missing from system_prompt payload"
    )


# ---------------------------------------------------------------------------
# BB4 — HTTP 5xx server error → False returned (no crash)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb4_http_500_returns_false():
    """BB4: HTTP 500 Internal Server Error → push_to_telnyx returns False without raising."""
    injector, _ = make_injector(http_status=500)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, f"Expected False on HTTP 500, got {result}"


@pytest.mark.asyncio
async def test_bb4_http_503_returns_false():
    """BB4: HTTP 503 Service Unavailable → push_to_telnyx returns False."""
    injector, _ = make_injector(http_status=503)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, f"Expected False on HTTP 503, got {result}"


@pytest.mark.asyncio
async def test_bb4_network_exception_returns_false():
    """BB4: Network exception (e.g. connection refused) → returns False, does not raise."""
    http = MagicMock()
    http.patch = AsyncMock(side_effect=ConnectionError("Connection refused"))

    injector = SystemPromptInjector(http_client=http)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, "Expected False when network raises ConnectionError"


# ---------------------------------------------------------------------------
# BB5 — Missing TELNYX_API_KEY → False returned immediately
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb5_missing_api_key_returns_false():
    """BB5: When TELNYX_API_KEY is not set, push_to_telnyx returns False immediately."""
    injector, http = make_injector(http_status=200)

    # Ensure the key is absent from environment
    env_without_key = {k: v for k, v in os.environ.items() if k != "TELNYX_API_KEY"}
    with patch.dict(os.environ, env_without_key, clear=True):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, "Expected False when TELNYX_API_KEY is missing"


@pytest.mark.asyncio
async def test_bb5_empty_api_key_returns_false():
    """BB5: When TELNYX_API_KEY is an empty string, push_to_telnyx returns False."""
    injector, http = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": ""}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, "Expected False when TELNYX_API_KEY is empty string"


@pytest.mark.asyncio
async def test_bb5_missing_key_no_http_call():
    """BB5: When TELNYX_API_KEY is missing, no HTTP request is made at all."""
    injector, http = make_injector(http_status=200)

    env_without_key = {k: v for k, v in os.environ.items() if k != "TELNYX_API_KEY"}
    with patch.dict(os.environ, env_without_key, clear=True):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    http.patch.assert_not_called()


# ---------------------------------------------------------------------------
# WB1 — API key read from os.environ (not hardcoded)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_wb1_api_key_from_env_used_in_auth_header():
    """WB1: The TELNYX_API_KEY env var value is used in the Authorization header."""
    custom_key = "CUSTOM_KEY_FOR_TEST_9999"
    injector, http = make_injector(http_status=200)

    with patch.dict(os.environ, {"TELNYX_API_KEY": custom_key}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    call_kwargs = http.patch.call_args
    sent_headers = call_kwargs.kwargs.get("headers") or (
        call_kwargs.args[2] if len(call_kwargs.args) > 2 else None
    )
    assert sent_headers is not None, "No headers were passed to http.patch"
    assert sent_headers.get("Authorization") == f"Bearer {custom_key}", (
        f"Expected 'Bearer {custom_key}', got {sent_headers.get('Authorization')}"
    )


@pytest.mark.asyncio
async def test_wb1_different_api_key_reflected_in_header():
    """WB1: Changing the env var changes the Authorization header (not hardcoded)."""
    key_a = "KEY_AAAA_0001"
    key_b = "KEY_BBBB_0002"
    injector, http = make_injector(http_status=200)

    # First call with key_a
    with patch.dict(os.environ, {"TELNYX_API_KEY": key_a}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    first_call = http.patch.call_args_list[0]
    first_headers = first_call.kwargs.get("headers") or {}
    assert f"Bearer {key_a}" == first_headers.get("Authorization"), (
        "First call did not use key_a"
    )

    # Second call with key_b
    with patch.dict(os.environ, {"TELNYX_API_KEY": key_b}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    second_call = http.patch.call_args_list[1]
    second_headers = second_call.kwargs.get("headers") or {}
    assert f"Bearer {key_b}" == second_headers.get("Authorization"), (
        "Second call did not use key_b"
    )


# ---------------------------------------------------------------------------
# WB2 — HTTP errors caught and logged (no exception propagation)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_wb2_timeout_exception_does_not_propagate():
    """WB2: TimeoutError from http.patch is caught and returns False (no raise)."""
    http = MagicMock()
    http.patch = AsyncMock(side_effect=TimeoutError("Request timed out"))

    injector = SystemPromptInjector(http_client=http)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, "Expected False when TimeoutError is raised"


@pytest.mark.asyncio
async def test_wb2_runtime_exception_does_not_propagate():
    """WB2: Unexpected RuntimeError is caught and returns False (no raise)."""
    http = MagicMock()
    http.patch = AsyncMock(side_effect=RuntimeError("Unexpected error"))

    injector = SystemPromptInjector(http_client=http)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        result = await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    assert result is False, "Expected False when RuntimeError is raised"


# ---------------------------------------------------------------------------
# WB3 — Request URL matches /v2/ai_assistants/{assistant_id}
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_wb3_request_url_contains_assistant_id():
    """WB3: The PATCH URL includes the correct assistant_id path segment."""
    injector, http = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    call_kwargs = http.patch.call_args
    # First positional arg or 'url' kwarg
    called_url = call_kwargs.args[0] if call_kwargs.args else call_kwargs.kwargs.get("url")
    assert called_url is not None, "No URL was passed to http.patch"
    assert ASSISTANT_ID in called_url, (
        f"assistant_id '{ASSISTANT_ID}' not found in URL '{called_url}'"
    )


@pytest.mark.asyncio
async def test_wb3_request_url_uses_telnyx_api_base():
    """WB3: The PATCH URL starts with TELNYX_API_BASE constant."""
    injector, http = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    call_kwargs = http.patch.call_args
    called_url = call_kwargs.args[0] if call_kwargs.args else call_kwargs.kwargs.get("url")
    assert called_url.startswith(TELNYX_API_BASE), (
        f"URL '{called_url}' does not start with TELNYX_API_BASE '{TELNYX_API_BASE}'"
    )


@pytest.mark.asyncio
async def test_wb3_different_assistant_ids_produce_different_urls():
    """WB3: Changing assistant_id changes the PATCH URL (URL is built dynamically)."""
    id_a = "assistant-AAAA-0001"
    id_b = "assistant-BBBB-0002"

    injector, http = make_injector(http_status=200)

    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        await injector.push_to_telnyx(SESSION_ID, id_a)
        await injector.push_to_telnyx(SESSION_ID, id_b)

    url_a = http.patch.call_args_list[0].args[0]
    url_b = http.patch.call_args_list[1].args[0]

    assert id_a in url_a, f"id_a not found in url_a: {url_a}"
    assert id_b in url_b, f"id_b not found in url_b: {url_b}"
    assert url_a != url_b, "Different assistant_ids should produce different URLs"


# ---------------------------------------------------------------------------
# WB4 — Authorization header is "Bearer <api_key>"
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_wb4_authorization_header_format():
    """WB4: Authorization header is exactly 'Bearer <api_key>'."""
    injector, http = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    call_kwargs = http.patch.call_args
    headers = call_kwargs.kwargs.get("headers") or {}

    expected = f"Bearer {FAKE_API_KEY}"
    assert headers.get("Authorization") == expected, (
        f"Expected Authorization='{expected}', got '{headers.get('Authorization')}'"
    )


@pytest.mark.asyncio
async def test_wb4_content_type_header_is_json():
    """WB4: Content-Type header is 'application/json'."""
    injector, http = make_injector(http_status=200)
    with patch.dict(os.environ, {"TELNYX_API_KEY": FAKE_API_KEY}):
        await injector.push_to_telnyx(SESSION_ID, ASSISTANT_ID)

    call_kwargs = http.patch.call_args
    headers = call_kwargs.kwargs.get("headers") or {}
    assert headers.get("Content-Type") == "application/json", (
        f"Expected Content-Type='application/json', got '{headers.get('Content-Type')}'"
    )


# ---------------------------------------------------------------------------
# WB5 — TELNYX_API_BASE constant is correct URL prefix
# ---------------------------------------------------------------------------


def test_wb5_telnyx_api_base_constant():
    """WB5: TELNYX_API_BASE equals 'https://api.telnyx.com/v2'."""
    assert TELNYX_API_BASE == "https://api.telnyx.com/v2", (
        f"Expected 'https://api.telnyx.com/v2', got '{TELNYX_API_BASE}'"
    )


def test_wb5_telnyx_api_base_exported_from_init():
    """WB5: TELNYX_API_BASE is importable from core.injection package."""
    from core.injection import TELNYX_API_BASE as exported_base
    assert exported_base == "https://api.telnyx.com/v2", (
        f"Exported TELNYX_API_BASE is incorrect: '{exported_base}'"
    )


def test_wb5_telnyx_api_base_is_https():
    """WB5: TELNYX_API_BASE uses HTTPS (not HTTP) for security."""
    assert TELNYX_API_BASE.startswith("https://"), (
        f"TELNYX_API_BASE must use HTTPS, got: '{TELNYX_API_BASE}'"
    )


# ---------------------------------------------------------------------------
# Integration — http_client=None constructor path (no injection)
# ---------------------------------------------------------------------------


def test_constructor_http_client_defaults_to_none():
    """Constructor: http_client defaults to None when not provided."""
    injector = SystemPromptInjector()
    assert injector.http is None, "http_client should default to None"


def test_constructor_http_client_stored():
    """Constructor: injected http_client is stored as self.http."""
    mock_http = MagicMock()
    injector = SystemPromptInjector(http_client=mock_http)
    assert injector.http is mock_http, "http_client not stored in self.http"


def test_constructor_all_three_params_stored():
    """Constructor: all three parameters are stored as instance attributes."""
    engine = MagicMock()
    redis = MagicMock()
    http = MagicMock()
    injector = SystemPromptInjector(
        conversation_engine=engine, redis_client=redis, http_client=http
    )
    assert injector.conversation_engine is engine
    assert injector.redis is redis
    assert injector.http is http
