"""Layer 2: Plan — Deterministic Compilation.

Proving: "Same spec → same plan hash. Changes → detectable diff. No data required."
All tests are pure spec → plan. No engine/reconciliation calls.
"""
from __future__ import annotations

import json

import pytest

from tests.contract.conftest import (
    CUSTOMER_SPEC,
    MINIMAL_VALID_SPEC,
    make_spec,
    requires_native,
)

pytestmark = requires_native

from kanoniv.plan import plan


# ---------------------------------------------------------------------------
# Idempotency and structure
# ---------------------------------------------------------------------------


class TestPlanStructure:
    def test_same_spec_same_hash(self):
        """plan(spec).plan_hash == plan(spec).plan_hash (idempotent)."""
        spec = make_spec(CUSTOMER_SPEC)
        h1 = plan(spec).plan_hash
        h2 = plan(spec).plan_hash
        assert h1 == h2

    def test_different_specs_different_hash(self):
        """Changing a weight → hash changes."""
        spec_a = make_spec(CUSTOMER_SPEC)
        modified = CUSTOMER_SPEC.replace("weight: 1.0", "weight: 0.5", 1)
        spec_b = make_spec(modified)
        assert plan(spec_a).plan_hash != plan(spec_b).plan_hash

    def test_plan_hash_is_string(self):
        """Hash is a non-empty string."""
        spec = make_spec(CUSTOMER_SPEC)
        h = plan(spec).plan_hash
        assert isinstance(h, str)
        assert len(h) > 0

    def test_plan_has_execution_stages(self):
        """Stages list is non-empty."""
        spec = make_spec(CUSTOMER_SPEC)
        assert len(plan(spec).execution_stages) > 0

    def test_plan_has_match_strategies(self):
        """Match strategies reflects rules."""
        spec = make_spec(CUSTOMER_SPEC)
        strategies = plan(spec).match_strategies
        assert len(strategies) > 0

    def test_plan_has_blocking_analysis(self):
        """Blocking section populated."""
        spec = make_spec(CUSTOMER_SPEC)
        blocking = plan(spec).blocking
        assert isinstance(blocking, dict)

    def test_plan_summary_non_empty(self):
        """.summary() returns readable string."""
        spec = make_spec(CUSTOMER_SPEC)
        s = plan(spec).summary()
        assert isinstance(s, str)
        assert len(s) > 0

    def test_plan_to_dict_serializable(self):
        """.to_dict() is JSON-serializable."""
        spec = make_spec(CUSTOMER_SPEC)
        d = plan(spec).to_dict()
        json.dumps(d)  # should not raise

    def test_plan_entity_matches_spec(self):
        """.entity reflects spec entity name."""
        spec = make_spec(CUSTOMER_SPEC)
        assert plan(spec).entity == "customer"


# ---------------------------------------------------------------------------
# Hash sensitivity to meaningful changes
# ---------------------------------------------------------------------------


class TestPlanHashSensitivity:
    def test_threshold_change_changes_hash(self):
        """Changing match: 0.5 → 0.85 → different hash."""
        spec_a = make_spec(CUSTOMER_SPEC)
        modified = CUSTOMER_SPEC.replace("match: 0.5", "match: 0.85")
        spec_b = make_spec(modified)
        assert plan(spec_a).plan_hash != plan(spec_b).plan_hash

    def test_weight_change_changes_hash(self):
        """Changing rule weight → different hash."""
        spec_a = make_spec(CUSTOMER_SPEC)
        modified = CUSTOMER_SPEC.replace("weight: 0.8", "weight: 0.6")
        spec_b = make_spec(modified)
        assert plan(spec_a).plan_hash != plan(spec_b).plan_hash

    def test_rule_added_changes_hash(self):
        """Adding a rule → different hash."""
        spec_a = make_spec(CUSTOMER_SPEC)
        extra_rule = """  - name: name_exact
    type: exact
    field: first_name
    weight: 0.5
"""
        modified = CUSTOMER_SPEC.replace("decision:", extra_rule + "decision:")
        spec_b = make_spec(modified)
        assert plan(spec_a).plan_hash != plan(spec_b).plan_hash

    def test_rule_removed_changes_hash(self):
        """Removing a rule → different hash."""
        spec_a = make_spec(CUSTOMER_SPEC)
        modified = CUSTOMER_SPEC.replace(
            """  - name: phone_exact
    type: exact
    field: phone
    weight: 0.8
""",
            "",
        )
        spec_b = make_spec(modified)
        assert plan(spec_a).plan_hash != plan(spec_b).plan_hash

    def test_blocking_change_changes_hash(self):
        """Changing blocking keys → different hash."""
        spec_a = make_spec(CUSTOMER_SPEC)
        modified = CUSTOMER_SPEC.replace(
            "    - [phone]", "    - [first_name, last_name]"
        )
        spec_b = make_spec(modified)
        assert plan(spec_a).plan_hash != plan(spec_b).plan_hash

    def test_survivorship_change_changes_hash(self):
        """Adding survivorship → different hash."""
        spec_a = make_spec(CUSTOMER_SPEC)
        survivorship = """survivorship:
  - field: email
    strategy: most_recent
"""
        modified = CUSTOMER_SPEC + survivorship
        spec_b = make_spec(modified)
        assert plan(spec_a).plan_hash != plan(spec_b).plan_hash
