"""
Unit tests for CornerstoneInjector — pure injection layer (issue #711).

Tests are isolated: no storage, no async. The injector is purely synchronous
and stateless — it only formats pre-loaded CornerstoneMemory lists.
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass, field
from typing import List, Optional

import pytest

from cortex.identity.cornerstone import CornerstoneMemory
from cortex.identity.cornerstone_injector import (
    CornerstoneInjector,
    DEFAULT_BLOCK_ORDER,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def sha256(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).digest().hex()


def make_cs(
    label: str,
    content: str,
    injection_order: int = 0,
    category: Optional[str] = "memory",
    token_count: Optional[int] = None,
    status: str = "active",
) -> CornerstoneMemory:
    h = sha256(content)
    tc = token_count if token_count is not None else len(content.split())
    return CornerstoneMemory(
        id=f"cs-{label[:4].lower().replace(' ', '_')}",
        owner_id="default",
        label=label,
        content=content,
        golden_copy=content,
        golden_hash=h,
        current_hash=h,
        status=status,
        injection_order=injection_order,
        category=category,
        token_count=tc,
    )


@dataclass
class MockConfig:
    cornerstone_block_ordering: Optional[List[str]] = None


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def config():
    return MockConfig()


@pytest.fixture
def injector(config):
    return CornerstoneInjector(config=config)


# ---------------------------------------------------------------------------
# Tests: format_block — empty
# ---------------------------------------------------------------------------

def test_format_block_empty(injector):
    result = injector.format_block([])
    assert result == ""


# ---------------------------------------------------------------------------
# Tests: format_block — flat mode (no categories)
# ---------------------------------------------------------------------------

def test_format_block_flat_no_category(injector):
    """When all cornerstones have category=None, uses flat list format."""
    cs1 = make_cs("I am Eva", "Eva is the AI.", category=None, injection_order=0)
    cs2 = make_cs("Creator", "Andrew is the creator.", category=None, injection_order=1)
    result = injector.format_block([cs1, cs2])
    assert result.startswith("[CORNERSTONES — WHO I AM]")
    assert "I am Eva: Eva is the AI." in result
    assert "Creator: Andrew is the creator." in result
    assert "[END CORNERSTONES —" in result
    # No category headers in flat mode
    assert "##" not in result


def test_format_block_flat_includes_token_count(injector):
    cs = make_cs("Eva", "Eva is the AI.", category=None, token_count=5)
    result = injector.format_block([cs])
    assert "[END CORNERSTONES — 5 tokens]" in result


# ---------------------------------------------------------------------------
# Tests: format_block — category-block mode
# ---------------------------------------------------------------------------

def test_format_block_category_mode_headers(injector):
    """When categories are present, uses block headers."""
    cs1 = make_cs("Memory1", "I was born here.", category="memory", injection_order=0)
    cs2 = make_cs("Guardrail1", "I refuse.", category="guardrail", injection_order=1)
    result = injector.format_block([cs1, cs2])
    assert "[CORNERSTONES — WHO I AM]" in result
    assert "## Memory" in result
    assert "## Guardrail" in result
    assert "Memory1: I was born here." in result
    assert "Guardrail1: I refuse." in result


def test_format_block_category_ordering_respects_default_order(injector):
    """Default ordering: memory → anchor → meta_story → guardrail → other."""
    cs_guardrail = make_cs("G", "Guardrail.", category="guardrail", injection_order=3)
    cs_memory = make_cs("M", "Memory.", category="memory", injection_order=0)
    cs_anchor = make_cs("A", "Anchor.", category="anchor", injection_order=1)
    cornerstones = [cs_guardrail, cs_memory, cs_anchor]
    result = injector.format_block(cornerstones)
    # memory should appear before guardrail
    mem_pos = result.index("## Memory")
    anchor_pos = result.index("## Anchor")
    guard_pos = result.index("## Guardrail")
    assert mem_pos < anchor_pos < guard_pos


def test_format_block_custom_block_order(config):
    """Config can override block ordering."""
    config.cornerstone_block_ordering = ["guardrail", "memory"]
    injector = CornerstoneInjector(config=config)
    cs_mem = make_cs("M", "Memory.", category="memory")
    cs_guard = make_cs("G", "Guardrail.", category="guardrail")
    result = injector.format_block([cs_mem, cs_guard])
    guard_pos = result.index("## Guardrail")
    mem_pos = result.index("## Memory")
    assert guard_pos < mem_pos


def test_format_block_unknown_category_goes_to_other(injector):
    """Cornerstones with unknown/unlisted categories end up in 'other' block."""
    cs = make_cs("X", "Custom category.", category="custom_unknown")
    result = injector.format_block([cs])
    assert "## Custom_unknown" in result  # capitalized


def test_format_block_no_category_treated_as_other(injector):
    """Mixed: some have categories, some don't — None → 'other'."""
    cs_mem = make_cs("Mem", "Memory.", category="memory")
    cs_none = make_cs("Uncategorized", "No cat.", category=None)
    result = injector.format_block([cs_mem, cs_none])
    assert "## Memory" in result
    assert "## Other" in result


def test_format_block_tokens_summed(injector):
    cs1 = make_cs("A", "Alpha.", category="memory", token_count=10)
    cs2 = make_cs("B", "Beta.", category="guardrail", token_count=5)
    result = injector.format_block([cs1, cs2])
    assert "[END CORNERSTONES — 15 tokens]" in result


# ---------------------------------------------------------------------------
# Tests: total_tokens
# ---------------------------------------------------------------------------

def test_total_tokens_empty(injector):
    assert injector.total_tokens([]) == 0


def test_total_tokens_sum(injector):
    cs1 = make_cs("A", "A", token_count=10)
    cs2 = make_cs("B", "B", token_count=7)
    assert injector.total_tokens([cs1, cs2]) == 17


# ---------------------------------------------------------------------------
# Tests: is_within_budget
# ---------------------------------------------------------------------------

def test_is_within_budget_true(injector):
    cs = make_cs("A", "Content.", token_count=50)
    assert injector.is_within_budget([cs], token_budget=100)


def test_is_within_budget_exactly_at_limit(injector):
    cs = make_cs("A", "Content.", token_count=100)
    assert injector.is_within_budget([cs], token_budget=100)


def test_is_within_budget_false(injector):
    cs = make_cs("A", "Content.", token_count=101)
    assert not injector.is_within_budget([cs], token_budget=100)


# ---------------------------------------------------------------------------
# Tests: filter_to_budget
# ---------------------------------------------------------------------------

def test_filter_to_budget_includes_all_that_fit(injector):
    cs1 = make_cs("A", "A", injection_order=0, token_count=30)
    cs2 = make_cs("B", "B", injection_order=1, token_count=30)
    cs3 = make_cs("C", "C", injection_order=2, token_count=30)
    result = injector.filter_to_budget([cs1, cs2, cs3], token_budget=80)
    # 30+30=60 ≤ 80, 60+30=90 > 80
    assert len(result) == 2
    assert result[0].id == cs1.id
    assert result[1].id == cs2.id


def test_filter_to_budget_all_fit(injector):
    cs1 = make_cs("A", "A", token_count=10)
    cs2 = make_cs("B", "B", token_count=10)
    result = injector.filter_to_budget([cs1, cs2], token_budget=100)
    assert len(result) == 2


def test_filter_to_budget_none_fit(injector):
    cs = make_cs("A", "A", token_count=200)
    result = injector.filter_to_budget([cs], token_budget=100)
    assert result == []


def test_filter_to_budget_empty_input(injector):
    result = injector.filter_to_budget([], token_budget=100)
    assert result == []


# ---------------------------------------------------------------------------
# Tests: DEFAULT_BLOCK_ORDER constant
# ---------------------------------------------------------------------------

def test_default_block_order_has_required_categories():
    required = {"memory", "anchor", "meta_story", "guardrail", "other"}
    assert required.issubset(set(DEFAULT_BLOCK_ORDER))


def test_default_block_order_memory_first():
    assert DEFAULT_BLOCK_ORDER[0] == "memory"


def test_default_block_order_guardrail_before_other():
    assert DEFAULT_BLOCK_ORDER.index("guardrail") < DEFAULT_BLOCK_ORDER.index("other")
