"""Tests for ``arborist.substrate.weights`` — WeightSet defaults +
``from_dict`` adapter (#000012 Phase 1a).

Pure-stdlib dataclass + classmethod-style adapter; small surface.
This pins:

  - documented defaults (lifted from the module docstring)
  - greek-letter and Python-safe key aliases ("lambda" ↔ "lambda_")
  - missing-keys fall-through to defaults
  - extra-keys silently ignored
  - string values coerce to float
  - frozen-dataclass invariant (mutation raises)
"""

from __future__ import annotations

import dataclasses

import pytest

from arborist.substrate.weights import (
    DEFAULT_WEIGHTS,
    WeightSet,
    from_dict,
)


# --- defaults pinned to the docstring ------------------------------


def test_default_weights_match_documented_values():
    """Each weight in the dataclass docstring is pinned. If a future
    PR changes a default, this test fires and forces an explicit
    update of both the docstring AND the test."""
    assert DEFAULT_WEIGHTS.alpha == 1.0
    assert DEFAULT_WEIGHTS.beta == 1.0
    assert DEFAULT_WEIGHTS.gamma == 1.0
    assert DEFAULT_WEIGHTS.delta == 0.5
    assert DEFAULT_WEIGHTS.epsilon == 0.3
    assert DEFAULT_WEIGHTS.zeta == 0.0   # validator-diversity off in single-validator
    assert DEFAULT_WEIGHTS.eta == 2.0    # regression > improvement weight
    assert DEFAULT_WEIGHTS.theta == 0.5
    assert DEFAULT_WEIGHTS.iota == 0.0   # reserved
    assert DEFAULT_WEIGHTS.kappa == 0.0  # reserved
    assert DEFAULT_WEIGHTS.lambda_ == 0.5


def test_eta_is_heavier_than_alpha():
    """Documented invariant from the docstring:
    'η (RegressionPenalty) heavier than improvement weights so the
    scorer is biased toward refusing regressions.'"""
    assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.alpha
    assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.beta
    assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.gamma


def test_three_batteries_equal_by_default():
    """Documented invariant: 'α = β = γ (3 batteries weighted equally
    by default).'"""
    assert DEFAULT_WEIGHTS.alpha == DEFAULT_WEIGHTS.beta == DEFAULT_WEIGHTS.gamma


def test_reserved_weights_are_zero_phase_1a():
    """Phase 1a discipline: validator-diversity / security / complexity
    are zero by default. v8 multi-validator deployments must opt in."""
    assert DEFAULT_WEIGHTS.zeta == 0.0
    assert DEFAULT_WEIGHTS.iota == 0.0
    assert DEFAULT_WEIGHTS.kappa == 0.0


# --- as_dict --------------------------------------------------------


def test_as_dict_returns_all_fields():
    d = DEFAULT_WEIGHTS.as_dict()
    expected_keys = {
        "alpha", "beta", "gamma", "delta", "epsilon", "zeta",
        "eta", "theta", "iota", "kappa", "lambda_",
        "delta_aggregator",  # #000047 — categorical, not a numeric weight
    }
    assert set(d.keys()) == expected_keys
    assert d["delta_aggregator"] == "mean"


def test_as_dict_uses_lambda_underscore_key():
    """``lambda`` is a Python keyword → dataclass uses ``lambda_``;
    as_dict() carries it forward unchanged. Callers serializing to
    JSON / YAML rename to bare ``lambda`` if needed."""
    d = DEFAULT_WEIGHTS.as_dict()
    assert "lambda_" in d
    assert "lambda" not in d


# --- from_dict adapter ---------------------------------------------


def test_from_dict_uses_greek_letter_keys():
    ws = from_dict({"alpha": 0.7, "beta": 0.8, "gamma": 0.9})
    assert ws.alpha == 0.7
    assert ws.beta == 0.8
    assert ws.gamma == 0.9
    # Unspecified keys fall through to defaults.
    assert ws.delta == DEFAULT_WEIGHTS.delta


def test_from_dict_translates_lambda_to_lambda_underscore():
    """JSON/YAML configs use bare ``lambda`` (the Greek letter, not
    the keyword). from_dict translates to the Python-safe field
    name ``lambda_``."""
    ws = from_dict({"lambda": 0.99})
    assert ws.lambda_ == 0.99
    # Bare 'lambda' did not survive into a hidden field.
    assert "lambda" not in ws.as_dict()


def test_from_dict_lambda_underscore_key_also_accepted():
    """For Python callers writing config dicts directly, ``lambda_``
    is the natural key name; from_dict accepts it too."""
    ws = from_dict({"lambda_": 0.42})
    assert ws.lambda_ == 0.42


def test_from_dict_missing_keys_fall_through_to_defaults():
    """Empty dict → identical to DEFAULT_WEIGHTS."""
    ws = from_dict({})
    assert ws == DEFAULT_WEIGHTS


def test_from_dict_partial_override_preserves_other_defaults():
    ws = from_dict({"alpha": 0.5})
    assert ws.alpha == 0.5
    # Every other field is unchanged.
    assert ws.beta == DEFAULT_WEIGHTS.beta
    assert ws.eta == DEFAULT_WEIGHTS.eta
    assert ws.lambda_ == DEFAULT_WEIGHTS.lambda_


def test_from_dict_ignores_unknown_keys():
    """Defensive: extra keys (e.g. "mu" — never defined) are
    silently dropped rather than raising. Forward-compat with
    config files that include vendor-specific tags."""
    ws = from_dict({"alpha": 0.6, "mu": 1.234, "extra_metadata": "ignore"})
    assert ws.alpha == 0.6
    # mu / extra_metadata silently dropped.
    assert not hasattr(ws, "mu")


def test_from_dict_coerces_string_to_float():
    """JSON/YAML may pass numeric weights as strings. from_dict
    coerces via float()."""
    ws = from_dict({"alpha": "0.7", "lambda": "0.3"})
    assert ws.alpha == 0.7
    assert ws.lambda_ == 0.3


def test_from_dict_int_input_becomes_float():
    ws = from_dict({"alpha": 1})
    assert ws.alpha == 1.0
    assert isinstance(ws.alpha, float)


# --- frozen invariant ----------------------------------------------


def test_weight_set_is_frozen():
    """WeightSet is dataclass(frozen=True) — mutation raises
    FrozenInstanceError. Pinned so a future @dataclass change
    forces this test to be updated explicitly."""
    with pytest.raises(dataclasses.FrozenInstanceError):
        DEFAULT_WEIGHTS.alpha = 99.0


def test_weight_sets_equal_by_value():
    """Two WeightSet instances with identical fields compare equal —
    standard dataclass equality. Used by tests + audit-trail
    serialization."""
    a = WeightSet(alpha=0.5, beta=0.6)
    b = WeightSet(alpha=0.5, beta=0.6)
    assert a == b
    # Different values → not equal.
    c = WeightSet(alpha=0.5, beta=0.7)
    assert a != c
