"""
tests/track_b/test_story_9_05.py

Story 9.05: AxiomDistiller — Gemini Pro Weekly Distillation

Black Box Tests (BB):
    BB1  Mock Gemini returns 7 axioms → 7 axioms in result
    BB2  Axioms with confidence < 0.6 filtered out (10 axioms, 3 below → 7 returned)
    BB3  Axiom ID format matches epoch_YYYY_MM_DD_NNN pattern
    BB4  No client → empty DistillationResult (no exception)

White Box Tests (WB):
    WB1  model_name defaults to "gemini-pro" (not Flash)
    WB2  JSON parse failure → empty DistillationResult (not exception)
    WB3  week_summary extracted from LLM response
    WB4  CONFIDENCE_THRESHOLD == 0.6

ALL tests use mock gemini_client. No real LLM calls.
"""

from __future__ import annotations

import json
import re
import sys

import pytest

# ---------------------------------------------------------------------------
# Path setup
# ---------------------------------------------------------------------------

GENESIS_ROOT = "/mnt/e/genesis-system"
if GENESIS_ROOT not in sys.path:
    sys.path.insert(0, GENESIS_ROOT)

# ---------------------------------------------------------------------------
# Imports under test
# ---------------------------------------------------------------------------

from core.epoch.axiom_distiller import (  # noqa: E402
    AxiomDistiller,
    Axiom,
    DistillationResult,
    CONFIDENCE_THRESHOLD,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_axiom_payload(
    idx: int,
    confidence: float,
    *,
    date_hint: str = "2026_02_25",
) -> dict:
    """Build a single axiom dict as the LLM would return it."""
    return {
        "id": f"epoch_{date_hint}_{idx:03d}",
        "content": f"Axiom content number {idx}",
        "category": "operations",
        "confidence": confidence,
        "source_saga_ids": [f"saga_{idx:03d}"],
    }


def _mock_client(axiom_dicts: list[dict], week_summary: str = "A good week.") -> object:
    """Return a callable that acts like a Gemini client returning fixed JSON."""
    payload = {"axioms": axiom_dicts, "week_summary": week_summary}
    raw_json = json.dumps(payload)

    def _client(prompt: str) -> str:  # noqa: ARG001
        return raw_json

    return _client


class _FakeConversations:
    """Minimal WeeklyConversationSummary-alike for injection."""

    def __init__(
        self,
        total_sessions: int = 5,
        failed_tasks: int = 1,
        snippets: list[str] | None = None,
    ) -> None:
        self.total_sessions = total_sessions
        self.failed_tasks = failed_tasks
        self.conversation_snippets = snippets or []


class _FakeScars:
    """Minimal ScarReport-alike for injection."""

    def __init__(self, clusters: list | None = None) -> None:
        self.clusters = clusters or []


# ===========================================================================
# BB Tests — Black Box
# ===========================================================================


def test_bb1_seven_axioms_returned():
    """BB1: Mock Gemini returns 7 axioms (all confidence >= 0.6) → 7 axioms in result."""
    axiom_dicts = [_make_axiom_payload(i, 0.75) for i in range(1, 8)]
    distiller = AxiomDistiller(gemini_client=_mock_client(axiom_dicts))

    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert isinstance(result, DistillationResult)
    assert len(result.axioms) == 7, f"Expected 7 axioms, got {len(result.axioms)}"
    for axiom in result.axioms:
        assert isinstance(axiom, Axiom)


def test_bb2_low_confidence_axioms_filtered():
    """BB2: 10 axioms, 3 below confidence threshold → 7 returned."""
    # 7 axioms above threshold, 3 below
    high = [_make_axiom_payload(i, 0.80) for i in range(1, 8)]
    low = [_make_axiom_payload(i, 0.50) for i in range(8, 11)]  # all < 0.6
    axiom_dicts = high + low

    distiller = AxiomDistiller(gemini_client=_mock_client(axiom_dicts))
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert len(result.axioms) == 7, (
        f"Expected 7 axioms after filtering 3 low-confidence ones, got {len(result.axioms)}"
    )
    for axiom in result.axioms:
        assert axiom.confidence >= CONFIDENCE_THRESHOLD


def test_bb3_axiom_id_format():
    """BB3: Axiom ID format matches epoch_YYYY_MM_DD_NNN pattern."""
    axiom_dicts = [
        {
            "id": "epoch_2026_02_25_001",
            "content": "Verify infra before build",
            "category": "infrastructure",
            "confidence": 0.9,
            "source_saga_ids": ["saga_001"],
        }
    ]
    distiller = AxiomDistiller(gemini_client=_mock_client(axiom_dicts))
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert len(result.axioms) == 1
    axiom_id = result.axioms[0].id
    # Pattern: epoch_YYYY_MM_DD_NNN (date digits + 3-digit sequence)
    pattern = re.compile(r"^epoch_\d{4}_\d{2}_\d{2}_\d{3}$")
    assert pattern.match(axiom_id), (
        f"Axiom ID '{axiom_id}' does not match pattern epoch_YYYY_MM_DD_NNN"
    )


def test_bb4_no_client_returns_empty_result():
    """BB4: No client → empty DistillationResult with no exception raised."""
    distiller = AxiomDistiller(gemini_client=None)
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert isinstance(result, DistillationResult)
    assert result.axioms == []
    assert result.week_summary == ""


# ===========================================================================
# WB Tests — White Box
# ===========================================================================


def test_wb1_default_model_is_gemini_pro():
    """WB1: model_name defaults to 'gemini-pro' (not Flash) when not supplied."""
    distiller = AxiomDistiller()
    assert distiller.model_name == "gemini-pro", (
        f"Expected model_name='gemini-pro', got '{distiller.model_name}'"
    )


def test_wb2_json_parse_failure_returns_empty_not_exception():
    """WB2: LLM returning invalid JSON → empty DistillationResult, no exception raised."""
    def _bad_client(prompt: str) -> str:  # noqa: ARG001
        return "THIS IS NOT JSON {{{{{{{"

    distiller = AxiomDistiller(gemini_client=_bad_client)
    # Must not raise
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert isinstance(result, DistillationResult)
    assert result.axioms == []
    assert result.week_summary == ""


def test_wb3_week_summary_extracted():
    """WB3: week_summary field is extracted correctly from LLM response."""
    expected_summary = "Genesis executed 12 sessions. Three failures resolved via axiom evolution."
    axiom_dicts = [_make_axiom_payload(1, 0.85)]

    distiller = AxiomDistiller(
        gemini_client=_mock_client(axiom_dicts, week_summary=expected_summary)
    )
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert result.week_summary == expected_summary, (
        f"Expected week_summary='{expected_summary}', got '{result.week_summary}'"
    )


def test_wb4_confidence_threshold_is_0_6():
    """WB4: CONFIDENCE_THRESHOLD constant equals exactly 0.6."""
    assert CONFIDENCE_THRESHOLD == 0.6, (
        f"Expected CONFIDENCE_THRESHOLD=0.6, got {CONFIDENCE_THRESHOLD}"
    )


# ===========================================================================
# Additional edge-case / contract tests
# ===========================================================================


def test_boundary_confidence_exactly_0_6_is_included():
    """Axiom with confidence exactly 0.6 (boundary) is included."""
    axiom_dicts = [_make_axiom_payload(1, 0.6)]
    distiller = AxiomDistiller(gemini_client=_mock_client(axiom_dicts))
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert len(result.axioms) == 1
    assert result.axioms[0].confidence == 0.6


def test_boundary_confidence_just_below_0_6_is_excluded():
    """Axiom with confidence 0.59 (just below threshold) is excluded."""
    axiom_dicts = [_make_axiom_payload(1, 0.59)]
    distiller = AxiomDistiller(gemini_client=_mock_client(axiom_dicts))
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert len(result.axioms) == 0


def test_empty_axioms_list_in_response():
    """LLM returns empty axioms list → DistillationResult with empty axioms list."""
    distiller = AxiomDistiller(gemini_client=_mock_client([]))
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert isinstance(result, DistillationResult)
    assert result.axioms == []


def test_axiom_fields_all_populated():
    """All Axiom fields (id, content, category, confidence, source_saga_ids) are set."""
    axiom_dicts = [
        {
            "id": "epoch_2026_02_25_001",
            "content": "Read before build",
            "category": "research",
            "confidence": 0.95,
            "source_saga_ids": ["saga_001", "saga_002"],
        }
    ]
    distiller = AxiomDistiller(gemini_client=_mock_client(axiom_dicts))
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert len(result.axioms) == 1
    a = result.axioms[0]
    assert a.id == "epoch_2026_02_25_001"
    assert a.content == "Read before build"
    assert a.category == "research"
    assert a.confidence == 0.95
    assert a.source_saga_ids == ["saga_001", "saga_002"]


def test_distillation_result_is_dataclass():
    """DistillationResult and Axiom are proper dataclasses with correct fields."""
    import dataclasses

    assert dataclasses.is_dataclass(DistillationResult)
    assert dataclasses.is_dataclass(Axiom)

    dr_fields = {f.name for f in dataclasses.fields(DistillationResult)}
    assert "axioms" in dr_fields
    assert "week_summary" in dr_fields

    ax_fields = {f.name for f in dataclasses.fields(Axiom)}
    assert "id" in ax_fields
    assert "content" in ax_fields
    assert "category" in ax_fields
    assert "confidence" in ax_fields
    assert "source_saga_ids" in ax_fields


def test_non_dict_json_response_returns_empty():
    """LLM returns a JSON list (not dict) → empty DistillationResult."""
    def _list_client(prompt: str) -> str:  # noqa: ARG001
        return json.dumps([{"id": "epoch_001", "content": "x", "confidence": 0.9}])

    distiller = AxiomDistiller(gemini_client=_list_client)
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert isinstance(result, DistillationResult)
    assert result.axioms == []


def test_package_init_exports_axiom_distiller():
    """core.epoch __init__ exports AxiomDistiller, Axiom, DistillationResult, CONFIDENCE_THRESHOLD."""
    from core.epoch import (  # noqa: F401
        AxiomDistiller as AD,
        Axiom as A,
        DistillationResult as DR,
        CONFIDENCE_THRESHOLD as CT,
    )

    assert AD is AxiomDistiller
    assert A is Axiom
    assert DR is DistillationResult
    assert CT == 0.6


def test_custom_model_name_stored():
    """model_name passed to constructor is stored as attribute."""
    distiller = AxiomDistiller(model_name="gemini-1.5-pro")
    assert distiller.model_name == "gemini-1.5-pro"


def test_runtime_exception_in_client_returns_empty():
    """If gemini_client raises an unexpected exception → empty DistillationResult."""
    def _crashing_client(prompt: str) -> str:
        raise RuntimeError("network timeout")

    distiller = AxiomDistiller(gemini_client=_crashing_client)
    result = distiller.distill(_FakeConversations(), _FakeScars())

    assert isinstance(result, DistillationResult)
    assert result.axioms == []


# ===========================================================================
# Standalone runner
# ===========================================================================

if __name__ == "__main__":
    import traceback

    tests = [
        ("BB1: 7 axioms returned", test_bb1_seven_axioms_returned),
        ("BB2: low-confidence filtered (10→7)", test_bb2_low_confidence_axioms_filtered),
        ("BB3: axiom ID format epoch_YYYY_MM_DD_NNN", test_bb3_axiom_id_format),
        ("BB4: no client → empty result", test_bb4_no_client_returns_empty_result),
        ("WB1: default model is gemini-pro", test_wb1_default_model_is_gemini_pro),
        ("WB2: JSON parse failure → empty result", test_wb2_json_parse_failure_returns_empty_not_exception),
        ("WB3: week_summary extracted", test_wb3_week_summary_extracted),
        ("WB4: CONFIDENCE_THRESHOLD == 0.6", test_wb4_confidence_threshold_is_0_6),
        ("EDGE: boundary 0.6 included", test_boundary_confidence_exactly_0_6_is_included),
        ("EDGE: boundary 0.59 excluded", test_boundary_confidence_just_below_0_6_is_excluded),
        ("EDGE: empty axioms list", test_empty_axioms_list_in_response),
        ("EDGE: all axiom fields populated", test_axiom_fields_all_populated),
        ("EDGE: DistillationResult is dataclass", test_distillation_result_is_dataclass),
        ("EDGE: non-dict JSON → empty", test_non_dict_json_response_returns_empty),
        ("PKG: __init__ exports AxiomDistiller", test_package_init_exports_axiom_distiller),
        ("EDGE: custom model_name stored", test_custom_model_name_stored),
        ("EDGE: client runtime exception → empty", test_runtime_exception_in_client_returns_empty),
    ]

    passed = 0
    total = len(tests)
    for name, fn in tests:
        try:
            fn()
            print(f"  [PASS] {name}")
            passed += 1
        except Exception as exc:  # noqa: BLE001
            print(f"  [FAIL] {name}: {exc}")
            traceback.print_exc()

    print(f"\n{passed}/{total} tests passed")
    if passed == total:
        print("ALL TESTS PASSED -- Story 9.05 (Track B)")
    else:
        sys.exit(1)
