"""Layer 1: Spec & Validator — Static Guarantees.

Proving: "Invalid identity logic can never reach execution."
All tests are pure Python: spec string → validate(). No engine calls.

NOTE: The current Rust validator checks api_version, identity_version, entity,
threshold ordering, and weight bounds. It does NOT currently reject missing
rules/sources/blocking/decision or unknown rule types. Tests document actual
behavior and validate the boundaries that DO exist.
"""
from __future__ import annotations

import pytest

from tests.contract.conftest import MINIMAL_VALID_SPEC, make_spec, requires_native

pytestmark = requires_native

from kanoniv.validate import validate


# ---------------------------------------------------------------------------
# Valid specs
# ---------------------------------------------------------------------------


class TestValidSpecs:
    def test_valid_minimal_spec(self):
        """Minimum viable spec passes validation."""
        spec = make_spec(MINIMAL_VALID_SPEC)
        result = validate(spec)
        assert result.valid
        assert result.errors == []

    def test_valid_spec_bool_true(self):
        """bool(validate(valid_spec)) is True."""
        spec = make_spec(MINIMAL_VALID_SPEC)
        assert bool(validate(spec)) is True

    def test_raise_on_error_valid_noop(self):
        """.raise_on_error() on valid spec does nothing."""
        spec = make_spec(MINIMAL_VALID_SPEC)
        validate(spec).raise_on_error()  # should not raise


# ---------------------------------------------------------------------------
# Missing top-level sections — entity is required, others are permissive
# ---------------------------------------------------------------------------


class TestMissingSections:
    def test_missing_entity_fails(self):
        """No entity → validation error (entity IS required)."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid

    def test_missing_api_version_fails(self):
        """No api_version → validation error."""
        yaml = """\
identity_version: test_v1.0
entity:
  name: customer
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid

    def test_missing_identity_version_fails(self):
        """No identity_version → validation error."""
        yaml = """\
api_version: kanoniv/v2
entity:
  name: customer
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid

    def test_missing_rules_accepted(self):
        """No rules → currently accepted by validator (permissive design)."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
entity:
  name: customer
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        # Document: validator is permissive — missing rules passes
        assert result.valid

    def test_missing_sources_accepted(self):
        """No sources → currently accepted by validator (permissive design)."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
entity:
  name: customer
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert result.valid


# ---------------------------------------------------------------------------
# Invalid thresholds and weights — these ARE validated
# ---------------------------------------------------------------------------


class TestInvalidThresholdsWeights:
    def test_threshold_match_below_review_fails(self):
        """match: 0.3, review: 0.5 (inverted) → rejected."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
entity:
  name: customer
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.3
    review: 0.5
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid

    def test_negative_weight_fails(self):
        """weight: -0.5 → rejected."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
entity:
  name: customer
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: -0.5
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid

    def test_weight_above_one_fails(self):
        """weight: 1.5 → rejected."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
entity:
  name: customer
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.5
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid


# ---------------------------------------------------------------------------
# Duplicate rule names — validated
# ---------------------------------------------------------------------------


class TestDuplicateRules:
    def test_duplicate_rule_names_fails(self):
        """Two rules with same name → rejected."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
entity:
  name: customer
sources:
  - name: src_a
    system: csv
    table: src_a
    id: id
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
  - name: email_exact
    type: exact
    field: email
    weight: 0.8
decision:
  thresholds:
    match: 0.7
    review: 0.4
  conflict_strategy: prefer_high_confidence
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid


# ---------------------------------------------------------------------------
# Malformed YAML
# ---------------------------------------------------------------------------


class TestMalformedInput:
    def test_empty_yaml_fails(self):
        """Empty string → rejected (missing api_version, identity_version, entity)."""
        spec = make_spec("")
        result = validate(spec)
        assert not result.valid

    def test_non_yaml_fails(self):
        """{{{not yaml → Rust parser raises ValueError at parse time."""
        with pytest.raises(ValueError):
            make_spec("{{{not yaml")


# ---------------------------------------------------------------------------
# Error quality
# ---------------------------------------------------------------------------


class TestErrorQuality:
    def test_invalid_spec_bool_false(self):
        """bool(validate(bad_spec)) is False."""
        spec = make_spec("")
        assert bool(validate(spec)) is False

    def test_raise_on_error_invalid_raises(self):
        """.raise_on_error() on invalid spec raises ValueError."""
        spec = make_spec("")
        with pytest.raises(ValueError):
            validate(spec).raise_on_error()

    def test_errors_are_descriptive(self):
        """Error messages contain relevant context."""
        yaml = """\
api_version: kanoniv/v2
identity_version: test_v1.0
"""
        spec = make_spec(yaml)
        result = validate(spec)
        assert not result.valid
        combined = " ".join(result.errors).lower()
        assert len(combined) > 10
