"""
Story 7.06 — Test Suite
========================
SystemPromptInjector.build_injection(session_id)

Assembles the full system prompt injection block from recent memory + AIVA persona.

BB Tests (4):
  BB1: Full data available → all 4 sections populated in output
  BB2: No prior conversations (engine=None) → "No prior conversations" in output
  BB3: AEST time formatted correctly (contains date and time components)
  BB4: Output always contains "AIVA MEMORY INJECTION" and "END MEMORY INJECTION" markers

WB Tests (4):
  WB1: conversation_engine.get_recent_summary(3) called (mock verified)
  WB2: AEST = UTC+10 (verify offset calculation)
  WB3: Fallback text for all missing sections (None redis, None engine)
  WB4: Redis bytes decoded to utf-8

All external services are mocked. pytest-asyncio used for async tests.
"""
from __future__ import annotations

import re
from datetime import datetime, timezone, timedelta
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio

from core.injection.system_prompt_injector import SystemPromptInjector, AEST_OFFSET


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

SESSION_ID = "test-session-abc-123"


def make_conv_engine(summary: str = "Kinan discussed revenue targets") -> MagicMock:
    """Return a mock conversation engine with get_recent_summary."""
    engine = MagicMock()
    engine.get_recent_summary.return_value = summary
    return engine


def make_redis(
    directives: bytes | None = b"Never ask Kinan to do browser tasks",
    caller: bytes | None = b"+61412345678",
    tasks: bytes | None = b"Launch TradiesVoice Q1 2026",
) -> MagicMock:
    """Return a mock Redis client with pre-configured get() responses."""
    redis = MagicMock()

    def get_side_effect(key: str):
        if key == "kinan:directives:active":
            return directives
        if key.startswith("aiva:state:"):
            return caller
        if key == "aiva:tasks:active":
            return tasks
        return None

    redis.get.side_effect = get_side_effect
    return redis


# ---------------------------------------------------------------------------
# BB1 — Full data available → all 4 sections populated in output
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb1_all_four_sections_present():
    """BB1: When all data is available, all 4 INJECTION_TEMPLATE sections are populated."""
    engine = make_conv_engine("Kinan discussed $500K ARR target")
    redis = make_redis(
        directives=b"Browser-first protocol active",
        caller=b"+61412345678",
        tasks=b"Demo TradiesVoice to George",
    )
    injector = SystemPromptInjector(conversation_engine=engine, redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    # All 4 custom values should appear
    assert "Kinan discussed $500K ARR target" in result, "recent_conversations missing"
    assert "Browser-first protocol active" in result, "kinan_directives missing"
    assert "+61412345678" in result, "caller_info missing"
    assert "Demo TradiesVoice to George" in result, "open_tasks missing"


@pytest.mark.asyncio
async def test_bb1_result_is_non_empty_string():
    """BB1: build_injection always returns a non-empty string."""
    engine = make_conv_engine()
    redis = make_redis()
    injector = SystemPromptInjector(conversation_engine=engine, redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    assert isinstance(result, str), "Result must be a string"
    assert len(result) > 0, "Result must be non-empty"


# ---------------------------------------------------------------------------
# BB2 — No prior conversations (engine=None) → "No prior conversations"
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb2_no_engine_uses_fallback():
    """BB2: When conversation_engine=None → 'No prior conversations' in output."""
    injector = SystemPromptInjector(conversation_engine=None, redis_client=None)
    result = await injector.build_injection(SESSION_ID)

    assert "No prior conversations" in result, (
        f"Expected 'No prior conversations' fallback, got:\n{result}"
    )


@pytest.mark.asyncio
async def test_bb2_no_engine_result_still_non_empty():
    """BB2: Even with no engine, build_injection returns non-empty string."""
    injector = SystemPromptInjector(conversation_engine=None, redis_client=None)
    result = await injector.build_injection(SESSION_ID)
    assert len(result) > 0, "Result must be non-empty even when engine is None"


# ---------------------------------------------------------------------------
# BB3 — AEST time formatted correctly
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb3_aest_time_in_output():
    """BB3: Output contains 'AEST' label."""
    injector = SystemPromptInjector()
    result = await injector.build_injection(SESSION_ID)
    assert "AEST" in result, f"Expected 'AEST' in output, got:\n{result}"


@pytest.mark.asyncio
async def test_bb3_time_has_date_and_time_components():
    """BB3: The AEST timestamp contains both date (YYYY-MM-DD) and time (HH:MM:SS) components."""
    injector = SystemPromptInjector()
    result = await injector.build_injection(SESSION_ID)

    # Match YYYY-MM-DD HH:MM:SS pattern
    time_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}")
    assert time_pattern.search(result), (
        f"Expected timestamp matching YYYY-MM-DD HH:MM:SS in output, got:\n{result}"
    )


# ---------------------------------------------------------------------------
# BB4 — Output always contains header + footer markers
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_bb4_header_marker_present_no_data():
    """BB4: 'AIVA MEMORY INJECTION' header marker always present (no data case)."""
    injector = SystemPromptInjector()
    result = await injector.build_injection(SESSION_ID)
    assert "AIVA MEMORY INJECTION" in result, (
        f"Header marker missing from output:\n{result}"
    )


@pytest.mark.asyncio
async def test_bb4_footer_marker_present_no_data():
    """BB4: 'END MEMORY INJECTION' footer marker always present (no data case)."""
    injector = SystemPromptInjector()
    result = await injector.build_injection(SESSION_ID)
    assert "END MEMORY INJECTION" in result, (
        f"Footer marker missing from output:\n{result}"
    )


@pytest.mark.asyncio
async def test_bb4_both_markers_present_full_data():
    """BB4: Both markers present even when all data is available."""
    engine = make_conv_engine()
    redis = make_redis()
    injector = SystemPromptInjector(conversation_engine=engine, redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    assert "AIVA MEMORY INJECTION" in result, "Header marker missing"
    assert "END MEMORY INJECTION" in result, "Footer marker missing"


# ---------------------------------------------------------------------------
# WB1 — conversation_engine.get_recent_summary(3) called
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_wb1_get_recent_summary_called_with_3():
    """WB1: _get_recent_conversations calls get_recent_summary(3) exactly once."""
    engine = make_conv_engine("Three conversations summarised")
    injector = SystemPromptInjector(conversation_engine=engine, redis_client=None)
    await injector.build_injection(SESSION_ID)

    engine.get_recent_summary.assert_called_once_with(3)


@pytest.mark.asyncio
async def test_wb1_summary_content_in_output():
    """WB1: The return value of get_recent_summary(3) appears in the output."""
    summary = "Kinan approved Q1 launch plan"
    engine = make_conv_engine(summary)
    injector = SystemPromptInjector(conversation_engine=engine, redis_client=None)
    result = await injector.build_injection(SESSION_ID)

    assert summary in result, (
        f"Expected summary '{summary}' in output, got:\n{result}"
    )


# ---------------------------------------------------------------------------
# WB2 — AEST = UTC+10 (verify offset calculation)
# ---------------------------------------------------------------------------


def test_wb2_aest_offset_is_utc_plus_10():
    """WB2: AEST_OFFSET constant equals timedelta(hours=10)."""
    assert AEST_OFFSET == timedelta(hours=10), (
        f"Expected AEST_OFFSET=timedelta(hours=10), got {AEST_OFFSET}"
    )


def test_wb2_get_aest_time_offset_matches_utc_plus_10():
    """WB2: _get_aest_time() returns a time that is UTC+10."""
    # Freeze a known UTC time and verify the AEST output is exactly +10 hours
    frozen_utc = datetime(2026, 2, 25, 14, 30, 0, tzinfo=timezone.utc)
    expected_aest = frozen_utc + timedelta(hours=10)
    expected_str = expected_aest.strftime("%Y-%m-%d %H:%M:%S")

    with patch("core.injection.system_prompt_injector.datetime") as mock_dt:
        mock_dt.now.return_value = frozen_utc
        result = SystemPromptInjector._get_aest_time()

    assert result == expected_str, (
        f"Expected AEST time '{expected_str}', got '{result}'"
    )


# ---------------------------------------------------------------------------
# WB3 — Fallback text for all missing sections (None redis, None engine)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_wb3_all_fallbacks_when_no_dependencies():
    """WB3: All four fields use fallback text when redis and engine are both None."""
    injector = SystemPromptInjector(conversation_engine=None, redis_client=None)
    result = await injector.build_injection(SESSION_ID)

    assert "No prior conversations" in result, "recent_conversations fallback missing"
    assert "No active directives" in result, "kinan_directives fallback missing"
    assert "Unknown" in result, "caller_info fallback missing"
    assert "None" in result, "open_tasks fallback missing"


@pytest.mark.asyncio
async def test_wb3_redis_exception_uses_fallbacks():
    """WB3: Redis.get() raising an exception triggers graceful fallbacks."""
    redis = MagicMock()
    redis.get.side_effect = ConnectionError("Redis connection refused")

    injector = SystemPromptInjector(conversation_engine=None, redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    # All redis-derived fields should fall back
    assert "No active directives" in result, "directives fallback missing on Redis error"
    assert "Unknown" in result, "caller fallback missing on Redis error"
    assert "None" in result, "tasks fallback missing on Redis error"


@pytest.mark.asyncio
async def test_wb3_engine_exception_uses_fallback():
    """WB3: conversation_engine.get_recent_summary() raising an exception uses fallback."""
    engine = MagicMock()
    engine.get_recent_summary.side_effect = RuntimeError("DB unavailable")

    injector = SystemPromptInjector(conversation_engine=engine, redis_client=None)
    result = await injector.build_injection(SESSION_ID)

    assert "No prior conversations" in result, "Fallback missing when engine raises"


# ---------------------------------------------------------------------------
# WB4 — Redis bytes decoded to utf-8
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_wb4_bytes_directives_decoded():
    """WB4: Redis bytes value for directives is decoded to utf-8 string."""
    redis = MagicMock()
    redis.get.side_effect = lambda key: (
        "Browser-first: active".encode("utf-8") if key == "kinan:directives:active" else None
    )
    injector = SystemPromptInjector(redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    assert "Browser-first: active" in result, (
        f"Decoded directive value not found in output:\n{result}"
    )


@pytest.mark.asyncio
async def test_wb4_bytes_caller_info_decoded():
    """WB4: Redis bytes value for caller_info is decoded to utf-8 string."""
    redis = MagicMock()
    redis.get.side_effect = lambda key: (
        "+61400111222".encode("utf-8") if key.startswith("aiva:state:") else None
    )
    injector = SystemPromptInjector(redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    assert "+61400111222" in result, (
        f"Decoded caller_info not found in output:\n{result}"
    )


@pytest.mark.asyncio
async def test_wb4_bytes_tasks_decoded():
    """WB4: Redis bytes value for open_tasks is decoded to utf-8 string."""
    redis = MagicMock()
    redis.get.side_effect = lambda key: (
        "Ship TradiesVoice beta".encode("utf-8") if key == "aiva:tasks:active" else None
    )
    injector = SystemPromptInjector(redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    assert "Ship TradiesVoice beta" in result, (
        f"Decoded open_tasks not found in output:\n{result}"
    )


@pytest.mark.asyncio
async def test_wb4_string_values_passed_through():
    """WB4: Redis returning plain str (not bytes) is also handled gracefully."""
    redis = MagicMock()
    redis.get.side_effect = lambda key: (
        "str-directive" if key == "kinan:directives:active" else None
    )
    injector = SystemPromptInjector(redis_client=redis)
    result = await injector.build_injection(SESSION_ID)

    assert "str-directive" in result, "Plain string Redis value not passed through"
