#!/usr/bin/env python3
"""
Tests for Story 4.02 (Track B): TierClassifier — Heuristic Task Classifier

Black Box tests (BB): verify public contract from the outside
White Box tests (WB): verify internal priority ordering and structural guarantees

Story: 4.02
File under test: core/routing/tier_classifier.py
"""
import sys
sys.path.insert(0, '/mnt/e/genesis-system')

import pytest
from core.routing.tier_classifier import (
    TierClassifier,
    RoutingDecision,
    T0_TYPES,
    T1_TYPES,
    T0_PAYLOAD_KEYS,
    T1_PAYLOAD_KEYS,
    TIER_MODELS,
)


@pytest.fixture
def classifier():
    return TierClassifier()


# ===========================================================================
# Black Box tests
# ===========================================================================

def test_bb1_health_check_routes_to_t0(classifier):
    """BB1: type='health_check' → T0, model='python_function'"""
    decision = classifier.classify({"type": "health_check"})
    assert decision.tier == "T0"
    assert decision.model == "python_function"


def test_bb2_lead_qualify_routes_to_t1(classifier):
    """BB2: type='lead_qualify' → T1, model='gemini-flash'"""
    decision = classifier.classify({"type": "lead_qualify"})
    assert decision.tier == "T1"
    assert decision.model == "gemini-flash"


def test_bb3_novel_type_routes_to_t2(classifier):
    """BB3: type='novel_decision' → T2, model='claude-opus-4-6'"""
    decision = classifier.classify({"type": "novel_decision"})
    assert decision.tier == "T2"
    assert decision.model == "claude-opus-4-6"


def test_bb4_crud_operation_payload_key_routes_to_t0(classifier):
    """BB4: payload has 'crud_operation' key → T0 regardless of type"""
    decision = classifier.classify({"type": "anything", "crud_operation": "INSERT"})
    assert decision.tier == "T0"
    assert decision.model == "python_function"


def test_bb5_template_payload_key_routes_to_t1(classifier):
    """BB5: payload has 'template' key → T1 regardless of type"""
    decision = classifier.classify({"type": "anything", "template": "welcome_email"})
    assert decision.tier == "T1"
    assert decision.model == "gemini-flash"


# ===========================================================================
# White Box tests
# ===========================================================================

def test_wb1_t0_wins_over_t1_when_both_keys_present(classifier):
    """WB1: T0 check runs before T1 — payload with a T0 key AND T1 type → T0 wins"""
    # 'lead_qualify' is a T1 type, but 'crud_operation' is a T0 payload key
    # The T0 *type* check runs first, but here type is T1.
    # The T0 *payload* check runs before T1 type check, so T0 must still win.
    decision = classifier.classify({
        "type": "lead_qualify",   # T1 type
        "crud_operation": "GET",  # T0 payload key
    })
    assert decision.tier == "T0", (
        f"Expected T0 (payload key wins), got {decision.tier}. "
        "T0 payload check must precede T1 type check."
    )


def test_wb2_rationale_is_non_empty_and_identifies_rule(classifier):
    """WB2: Rationale string is non-empty and identifies which rule fired"""
    for payload, expected_fragment in [
        ({"type": "cache_get"},                      "T0_TYPES"),
        ({"type": "email_draft"},                    "T1_TYPES"),
        ({"type": "unknown_xyz"},                    "defaulting"),
        ({"type": "x", "crud_operation": "DELETE"},  "crud_operation"),
        ({"type": "x", "template": "t"},             "template"),
    ]:
        decision = classifier.classify(payload)
        assert decision.rationale, f"Rationale must not be empty for payload {payload}"
        assert expected_fragment in decision.rationale, (
            f"Rationale '{decision.rationale}' does not contain '{expected_fragment}' "
            f"for payload {payload}"
        )


def test_wb3_is_cached_defaults_to_false(classifier):
    """WB3: RoutingDecision.is_cached defaults to False for every tier"""
    for payload in [
        {"type": "redis_read"},
        {"type": "faq_answer"},
        {"type": "completely_unknown"},
    ]:
        decision = classifier.classify(payload)
        assert decision.is_cached is False, (
            f"is_cached must default to False, got {decision.is_cached} "
            f"for tier {decision.tier}"
        )


def test_wb4_unknown_type_defaults_to_t2_with_defaulting_in_rationale(classifier):
    """WB4: Unknown type with no special keys → T2; rationale mentions 'defaulting'"""
    decision = classifier.classify({"type": "some_brand_new_operation"})
    assert decision.tier == "T2"
    assert "defaulting" in decision.rationale.lower()


# ===========================================================================
# Additional coverage: all T0 types, all T1 types, model mapping constants
# ===========================================================================

def test_all_t0_types_route_to_t0(classifier):
    """Every type in T0_TYPES must classify as T0."""
    for t in T0_TYPES:
        decision = classifier.classify({"type": t})
        assert decision.tier == "T0", f"Expected T0 for type='{t}', got {decision.tier}"
        assert decision.model == TIER_MODELS["T0"]


def test_all_t1_types_route_to_t1(classifier):
    """Every type in T1_TYPES (with no T0 keys present) must classify as T1."""
    for t in T1_TYPES:
        decision = classifier.classify({"type": t})
        assert decision.tier == "T1", f"Expected T1 for type='{t}', got {decision.tier}"
        assert decision.model == TIER_MODELS["T1"]


def test_routing_decision_is_dataclass():
    """RoutingDecision can be constructed directly with expected field defaults."""
    rd = RoutingDecision(tier="T2", model="claude-opus-4-6", rationale="test")
    assert rd.is_cached is False
    assert rd.tier == "T2"
    assert rd.model == "claude-opus-4-6"
    assert rd.rationale == "test"


def test_missing_type_key_defaults_to_t2(classifier):
    """Payload without a 'type' key → treated as 'unknown' → T2."""
    decision = classifier.classify({})
    assert decision.tier == "T2"


if __name__ == "__main__":
    # Allow running directly: python tests/track_b/test_story_4_02.py
    c = TierClassifier()

    tests = [
        ("BB1 health_check → T0",      {"type": "health_check"},                      "T0"),
        ("BB2 lead_qualify → T1",       {"type": "lead_qualify"},                      "T1"),
        ("BB3 novel_decision → T2",     {"type": "novel_decision"},                    "T2"),
        ("BB4 crud_operation key → T0", {"type": "x", "crud_operation": "INSERT"},     "T0"),
        ("BB5 template key → T1",       {"type": "x", "template": "welcome"},          "T1"),
        ("WB1 T0 key beats T1 type",    {"type": "lead_qualify", "crud_operation": "G"}, "T0"),
    ]

    passed = 0
    for name, payload, expected_tier in tests:
        decision = c.classify(payload)
        ok = decision.tier == expected_tier
        status = "PASS" if ok else "FAIL"
        if ok:
            passed += 1
        print(f"  [{status}] {name} → tier={decision.tier}, model={decision.model}")

    print(f"\n{passed}/{len(tests)} tests passed")
    if passed == len(tests):
        print("ALL TESTS PASSED — Story 4.02 (Track B)")
    else:
        sys.exit(1)
