#!/usr/bin/env python3
"""
Tests for Story 6.01 (Track B): StateDelta — RFC 6902 JSON Patch Proposal

Black Box tests (BB): verify the public contract from the outside —
    correct state transformation, immutability, error types.
White Box tests (WB): verify internal logic — validate_patch rules,
    specific op behaviors, path parsing, sentinel internals.

Story: 6.01
File under test: core/coherence/state_delta.py
"""

from __future__ import annotations

import sys
sys.path.insert(0, "/mnt/e/genesis-system")

import copy
import pytest
from datetime import datetime, timezone


# ---------------------------------------------------------------------------
# Module under test
# ---------------------------------------------------------------------------

from core.coherence.state_delta import (
    StateDelta,
    PatchConflictError,
    validate_patch,
    apply_patch,
    VALID_OPS,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_NOW = datetime(2026, 2, 25, 12, 0, 0, tzinfo=timezone.utc)


def _make_delta(patch: tuple = (), version: int = 1) -> StateDelta:
    return StateDelta(
        agent_id="agent-001",
        session_id="sess-abc",
        version_at_read=version,
        patch=patch,
        submitted_at=_NOW,
    )


# ===========================================================================
# Black Box tests
# ===========================================================================


def test_bb1_replace_op_updates_state():
    """BB1: Valid patch {"op": "replace", "path": "/name", "value": "new"} applied → state updated."""
    state = {"name": "old", "count": 0}
    patch = [{"op": "replace", "path": "/name", "value": "new"}]
    result = apply_patch(state, patch)
    assert result["name"] == "new"
    assert result["count"] == 0  # other keys unaffected


def test_bb2_test_op_wrong_value_raises_patch_conflict_error():
    """BB2: op: "test" with wrong value → PatchConflictError raised with path, expected, actual."""
    state = {"status": "active"}
    patch = [{"op": "test", "path": "/status", "value": "inactive"}]
    with pytest.raises(PatchConflictError) as exc_info:
        apply_patch(state, patch)
    err = exc_info.value
    assert err.path == "/status"
    assert err.expected == "inactive"
    assert err.actual == "active"


def test_bb3_frozen_dataclass_raises_frozen_instance_error():
    """BB3: frozen=True → attempt to set agent_id raises FrozenInstanceError (dataclasses.FrozenInstanceError)."""
    delta = _make_delta()
    with pytest.raises(Exception):  # FrozenInstanceError is a subclass of AttributeError
        delta.agent_id = "modified"  # type: ignore[misc]


def test_bb4_apply_patch_returns_new_dict_original_unchanged():
    """BB4: apply_patch returns NEW dict, original is not mutated."""
    original = {"x": 1, "y": 2}
    original_copy = copy.deepcopy(original)
    patch = [{"op": "replace", "path": "/x", "value": 99}]
    result = apply_patch(original, patch)
    assert result["x"] == 99
    assert original == original_copy  # original must be unchanged


def test_bb5_add_creates_new_key():
    """BB5: "add" op on a new key creates the key with the given value."""
    state = {"existing": "yes"}
    patch = [{"op": "add", "path": "/new_field", "value": "created"}]
    result = apply_patch(state, patch)
    assert result["new_field"] == "created"
    assert result["existing"] == "yes"


def test_bb6_test_op_correct_value_passes():
    """BB6: op: "test" with correct value → no exception raised, state unchanged."""
    state = {"status": "active"}
    patch = [{"op": "test", "path": "/status", "value": "active"}]
    result = apply_patch(state, patch)
    assert result == state


def test_bb7_remove_op_deletes_key():
    """BB7: "remove" op deletes an existing key from state."""
    state = {"keep": "yes", "delete_me": "bye"}
    patch = [{"op": "remove", "path": "/delete_me"}]
    result = apply_patch(state, patch)
    assert "delete_me" not in result
    assert result["keep"] == "yes"


def test_bb8_apply_to_convenience_method_matches_apply_patch():
    """BB8: StateDelta.apply_to() gives same result as apply_patch() with the same state."""
    state = {"val": 10}
    patch_list = [{"op": "replace", "path": "/val", "value": 42}]
    delta = _make_delta(patch=tuple(patch_list))
    result_method = delta.apply_to(state)
    result_func = apply_patch(state, patch_list)
    assert result_method == result_func


def test_bb9_nested_path_replace():
    """BB9: Nested path like "/a/b/c" works for replace."""
    state = {"a": {"b": {"c": "old"}}}
    patch = [{"op": "replace", "path": "/a/b/c", "value": "new"}]
    result = apply_patch(state, patch)
    assert result["a"]["b"]["c"] == "new"


def test_bb10_multi_op_patch_applied_in_order():
    """BB10: Multiple ops applied in order — add then replace."""
    state = {"x": 1}
    patch = [
        {"op": "add", "path": "/y", "value": 10},
        {"op": "replace", "path": "/y", "value": 20},
    ]
    result = apply_patch(state, patch)
    assert result["y"] == 20  # replace ran after add


# ===========================================================================
# White Box tests
# ===========================================================================


def test_wb1_validate_patch_rejects_unknown_op():
    """WB1: validate_patch rejects unknown op type "destroy" → returns False."""
    patch = [{"op": "destroy", "path": "/x"}]
    assert validate_patch(patch) is False


def test_wb2_validate_patch_rejects_missing_op_key():
    """WB2: validate_patch rejects op dict missing "op" key → returns False."""
    patch = [{"path": "/x", "value": 1}]
    assert validate_patch(patch) is False


def test_wb3_validate_patch_accepts_all_six_valid_ops():
    """WB3: validate_patch accepts all 6 valid ops → returns True for each."""
    ops_with_required_fields = [
        {"op": "add", "path": "/x", "value": 1},
        {"op": "remove", "path": "/x"},
        {"op": "replace", "path": "/x", "value": 1},
        {"op": "move", "path": "/y", "from": "/x"},
        {"op": "copy", "path": "/z", "from": "/x"},
        {"op": "test", "path": "/x", "value": 1},
    ]
    for op_dict in ops_with_required_fields:
        assert validate_patch([op_dict]) is True, f"Expected True for op: {op_dict['op']}"


def test_wb4_validate_patch_requires_from_for_move():
    """WB4: validate_patch returns False if "move" op missing "from" key."""
    patch = [{"op": "move", "path": "/dest"}]  # no "from"
    assert validate_patch(patch) is False


def test_wb5_validate_patch_requires_from_for_copy():
    """WB5: validate_patch returns False if "copy" op missing "from" key."""
    patch = [{"op": "copy", "path": "/dest"}]  # no "from"
    assert validate_patch(patch) is False


def test_wb6_validate_patch_requires_value_for_add():
    """WB6: validate_patch returns False if "add" op missing "value" key."""
    patch = [{"op": "add", "path": "/x"}]  # no "value"
    assert validate_patch(patch) is False


def test_wb7_validate_patch_requires_value_for_test():
    """WB7: validate_patch returns False if "test" op missing "value" key."""
    patch = [{"op": "test", "path": "/x"}]  # no "value"
    assert validate_patch(patch) is False


def test_wb8_remove_on_nonexistent_path_raises():
    """WB5 (test plan): "remove" on non-existent path → KeyError."""
    state = {"a": 1}
    patch = [{"op": "remove", "path": "/nonexistent"}]
    with pytest.raises(KeyError):
        apply_patch(state, patch)


def test_wb9_validate_patch_rejects_non_list():
    """WB: validate_patch returns False if patch is not a list."""
    assert validate_patch({"op": "add", "path": "/x", "value": 1}) is False  # type: ignore[arg-type]
    assert validate_patch("not a list") is False  # type: ignore[arg-type]
    assert validate_patch(None) is False  # type: ignore[arg-type]


def test_wb10_validate_patch_rejects_non_dict_elements():
    """WB: validate_patch returns False if any element is not a dict."""
    patch = ["add /x 1"]  # string instead of dict
    assert validate_patch(patch) is False


def test_wb11_patch_conflict_error_attributes():
    """WB: PatchConflictError has path, expected, actual attributes."""
    err = PatchConflictError("/x", "expected_val", "actual_val")
    assert err.path == "/x"
    assert err.expected == "expected_val"
    assert err.actual == "actual_val"
    assert "/x" in str(err)
    assert "expected_val" in str(err)
    assert "actual_val" in str(err)


def test_wb12_valid_ops_set_contains_exactly_six():
    """WB: VALID_OPS contains exactly the 6 RFC 6902 operations."""
    assert VALID_OPS == {"add", "remove", "replace", "move", "copy", "test"}


def test_wb13_frozen_dataclass_fields_accessible():
    """WB: All StateDelta fields are accessible on a frozen instance."""
    delta = _make_delta(patch=({"op": "remove", "path": "/x"},), version=5)
    assert delta.agent_id == "agent-001"
    assert delta.session_id == "sess-abc"
    assert delta.version_at_read == 5
    assert delta.patch == ({"op": "remove", "path": "/x"},)
    assert delta.submitted_at == _NOW


def test_wb14_patch_is_tuple_not_list():
    """WB: StateDelta.patch is a tuple (frozen dataclass requires hashable fields)."""
    delta = _make_delta(patch=({"op": "add", "path": "/k", "value": 1},))
    assert isinstance(delta.patch, tuple)


def test_wb15_move_op_removes_source_adds_dest():
    """WB: 'move' removes from source path and sets value at dest path."""
    state = {"src": "value_to_move", "dest": None}
    patch = [{"op": "move", "path": "/dest", "from": "/src"}]
    result = apply_patch(state, patch)
    assert result["dest"] == "value_to_move"
    assert "src" not in result


def test_wb16_copy_op_does_not_remove_source():
    """WB: 'copy' copies from source without removing it."""
    state = {"src": "original"}
    patch = [{"op": "copy", "path": "/dest", "from": "/src"}]
    result = apply_patch(state, patch)
    assert result["src"] == "original"  # source still present
    assert result["dest"] == "original"


def test_wb17_copy_op_is_deep_copy():
    """WB: 'copy' op performs a deep copy — mutating the result's copy doesn't affect source."""
    state = {"src": {"nested": [1, 2, 3]}}
    patch = [{"op": "copy", "path": "/dest", "from": "/src"}]
    result = apply_patch(state, patch)
    result["dest"]["nested"].append(99)
    # The original state's src must be unchanged
    assert state["src"]["nested"] == [1, 2, 3]


def test_wb18_replace_on_nonexistent_path_raises():
    """WB: 'replace' on non-existent path raises KeyError (must exist)."""
    state = {"a": 1}
    patch = [{"op": "replace", "path": "/does_not_exist", "value": 99}]
    with pytest.raises(KeyError):
        apply_patch(state, patch)


def test_wb19_validate_patch_accepts_empty_list():
    """WB: validate_patch returns True for an empty patch (no-op is valid RFC 6902)."""
    assert validate_patch([]) is True


def test_wb20_tilde_escaping_in_paths():
    """WB: RFC 6901 tilde escaping — ~1 means '/', ~0 means '~'."""
    state = {"a/b": "slash_key", "c~d": "tilde_key"}
    # Access key "a/b" via ~1 escaping
    patch_slash = [{"op": "replace", "path": "/a~1b", "value": "updated"}]
    result = apply_patch(state, patch_slash)
    assert result["a/b"] == "updated"

    # Access key "c~d" via ~0 escaping
    patch_tilde = [{"op": "replace", "path": "/c~0d", "value": "updated_tilde"}]
    result2 = apply_patch(state, patch_tilde)
    assert result2["c~d"] == "updated_tilde"


# ===========================================================================
# Package import / export tests
# ===========================================================================


def test_package_exports_state_delta():
    """Package level: StateDelta importable from core.coherence."""
    from core.coherence import StateDelta as SD
    assert SD is StateDelta


def test_package_exports_patch_conflict_error():
    """Package level: PatchConflictError importable from core.coherence."""
    from core.coherence import PatchConflictError as PCE
    assert PCE is PatchConflictError


def test_package_exports_validate_patch():
    """Package level: validate_patch importable from core.coherence."""
    from core.coherence import validate_patch as vp
    assert vp is validate_patch


def test_package_exports_apply_patch():
    """Package level: apply_patch importable from core.coherence."""
    from core.coherence import apply_patch as ap
    assert ap is apply_patch


def test_package_exports_valid_ops():
    """Package level: VALID_OPS importable from core.coherence."""
    from core.coherence import VALID_OPS as vo
    assert vo is VALID_OPS


# ===========================================================================
# Standalone runner (pytest preferred, fallback to direct execution)
# ===========================================================================

if __name__ == "__main__":
    import traceback

    tests_run = 0
    tests_passed = 0

    def _run(name: str, fn):
        global tests_run, tests_passed
        tests_run += 1
        try:
            fn()
            print(f"  [PASS] {name}")
            tests_passed += 1
        except Exception as exc:
            print(f"  [FAIL] {name}: {exc}")
            traceback.print_exc()

    # Black Box
    _run("BB1: replace op updates state", test_bb1_replace_op_updates_state)
    _run("BB2: test op wrong value → PatchConflictError", test_bb2_test_op_wrong_value_raises_patch_conflict_error)
    _run("BB3: frozen dataclass raises on mutation", test_bb3_frozen_dataclass_raises_frozen_instance_error)
    _run("BB4: apply_patch returns new dict", test_bb4_apply_patch_returns_new_dict_original_unchanged)
    _run("BB5: add creates new key", test_bb5_add_creates_new_key)
    _run("BB6: test op correct value passes", test_bb6_test_op_correct_value_passes)
    _run("BB7: remove op deletes key", test_bb7_remove_op_deletes_key)
    _run("BB8: apply_to convenience method", test_bb8_apply_to_convenience_method_matches_apply_patch)
    _run("BB9: nested path replace", test_bb9_nested_path_replace)
    _run("BB10: multi-op patch in order", test_bb10_multi_op_patch_applied_in_order)

    # White Box
    _run("WB1: validate_patch rejects unknown op", test_wb1_validate_patch_rejects_unknown_op)
    _run("WB2: validate_patch rejects missing op key", test_wb2_validate_patch_rejects_missing_op_key)
    _run("WB3: validate_patch accepts all 6 ops", test_wb3_validate_patch_accepts_all_six_valid_ops)
    _run("WB4: validate_patch requires 'from' for move", test_wb4_validate_patch_requires_from_for_move)
    _run("WB5: validate_patch requires 'from' for copy", test_wb5_validate_patch_requires_from_for_copy)
    _run("WB6: validate_patch requires 'value' for add", test_wb6_validate_patch_requires_value_for_add)
    _run("WB7: validate_patch requires 'value' for test", test_wb7_validate_patch_requires_value_for_test)
    _run("WB8: remove on nonexistent path raises", test_wb8_remove_on_nonexistent_path_raises)
    _run("WB9: validate_patch rejects non-list", test_wb9_validate_patch_rejects_non_list)
    _run("WB10: validate_patch rejects non-dict elements", test_wb10_validate_patch_rejects_non_dict_elements)
    _run("WB11: PatchConflictError attributes", test_wb11_patch_conflict_error_attributes)
    _run("WB12: VALID_OPS has exactly 6 ops", test_wb12_valid_ops_set_contains_exactly_six)
    _run("WB13: frozen dataclass fields accessible", test_wb13_frozen_dataclass_fields_accessible)
    _run("WB14: patch is tuple not list", test_wb14_patch_is_tuple_not_list)
    _run("WB15: move removes source, adds dest", test_wb15_move_op_removes_source_adds_dest)
    _run("WB16: copy does not remove source", test_wb16_copy_op_does_not_remove_source)
    _run("WB17: copy is deep copy", test_wb17_copy_op_is_deep_copy)
    _run("WB18: replace on nonexistent raises", test_wb18_replace_on_nonexistent_path_raises)
    _run("WB19: empty patch is valid", test_wb19_validate_patch_accepts_empty_list)
    _run("WB20: tilde escaping in paths", test_wb20_tilde_escaping_in_paths)

    # Package exports
    _run("PKG: StateDelta export", test_package_exports_state_delta)
    _run("PKG: PatchConflictError export", test_package_exports_patch_conflict_error)
    _run("PKG: validate_patch export", test_package_exports_validate_patch)
    _run("PKG: apply_patch export", test_package_exports_apply_patch)
    _run("PKG: VALID_OPS export", test_package_exports_valid_ops)

    print(f"\n{tests_passed}/{tests_run} tests passed")
    if tests_passed == tests_run:
        print("ALL TESTS PASSED -- Story 6.01 (Track B)")
    else:
        sys.exit(1)
