"""Unit tests for the Journey Matcher (Delta -> Journey).

The matcher ranks known journeys by the share of the Capability Delta they EXPLAIN, using ONLY the
delta and injected capability-cluster signatures — deterministic, auditable, no ML. These tests pin
the score semantics (recall over the delta), the ranking order, the audit reasons, and that context
corroborates without ever changing the score.
"""

from __future__ import annotations

from compliance.journey_matcher import (
    JourneySignature,
    MatchContext,
    match_journeys,
)


def _sig(jid, pattern, **kw):
    return JourneySignature(journey_id=jid, label=jid, capability_pattern=pattern, **kw)


def test_score_is_share_of_delta_explained():
    delta = ["a", "b", "c", "d", "e"]
    j = _sig("J", ["a", "b", "c", "d"])           # explains 4 of 5
    res = match_journeys(delta, [j])
    assert res.matches[0].score == 0.8
    assert res.matches[0].explains == "4 von 5 fehlenden Capabilities"


def test_ranking_orders_by_explanatory_power():
    delta = ["a", "b", "c", "d"]
    journeys = [
        _sig("low", ["a"]),                        # 1/4
        _sig("high", ["a", "b", "c"]),             # 3/4
        _sig("mid", ["a", "b"]),                   # 2/4
    ]
    res = match_journeys(delta, journeys)
    assert [m.journey_id for m in res.matches] == ["high", "mid", "low"]
    assert res.best.journey_id == "high"


def test_audit_reason_partitions_the_delta():
    delta = ["a", "b", "c"]
    j = _sig("J", ["b", "c", "x", "y"])            # explains b,c; misses a; reaches beyond into x,y
    r = match_journeys(delta, [j]).matches[0].reason
    assert r.matched_capabilities == ["b", "c"]
    assert r.unexplained_delta == ["a"]
    assert r.journey_only == ["x", "y"]


def test_context_corroborates_but_never_changes_score():
    delta = ["a", "b"]
    same = _sig("same", ["a", "b"], target_type="regulation")
    other = _sig("other", ["a", "b"], target_type="contract")
    ctx = MatchContext(target_type="regulation")
    res = match_journeys(delta, [other, same], ctx)
    # identical score (1.0) -> tie broken by context-signal count: 'same' first
    assert res.matches[0].score == res.matches[1].score == 1.0
    assert res.matches[0].journey_id == "same"
    assert "gleiche Zielart" in res.matches[0].reason.context_signals
    assert res.matches[1].reason.context_signals == []


def test_deterministic_tiebreak_by_journey_id():
    delta = ["a", "b"]
    res = match_journeys(delta, [_sig("zeta", ["a"]), _sig("alpha", ["a"])])
    assert [m.journey_id for m in res.matches] == ["alpha", "zeta"]


def test_no_journey_explains_the_delta():
    res = match_journeys(["a", "b"], [_sig("J", ["x", "y"])])
    assert res.best is None
    assert res.matches[0].score == 0.0
    assert "neue Journey-Kandidatin" in res.headline


def test_empty_delta_yields_no_best():
    res = match_journeys([], [_sig("J", ["a"])])
    assert res.delta_size == 0
    assert res.best is None
