"""
tests/track_b/test_story_7_06.py

Story 7.06: CompensatingTransaction — Partial-Fail Recovery

Black Box Tests (BB1–BB5):
    BB1  Failed saga with "add" op → compensating "remove" op generated
    BB2  Failed saga with "remove" op → compensating "add" restores original value
    BB3  Compensation event written to ColdLedger (verify mock call)
    BB4  "replace" op → inverse "replace" with original value from state
    BB5  Multiple failed deltas → all inverse ops collected

White Box Tests (WB1–WB4):
    WB1  invert_patch correctly inverts add / remove / replace / move
    WB2  "copy" op raises IrreversibleOperationError
    WB3  Patch order is reversed (last op inverted first)
    WB4  _extract_patch handles both StateDelta objects and plain dicts

Package Test:
    PKG  __init__.py exports CompensatingTransaction, CompensationResult,
         IrreversibleOperationError
"""

from __future__ import annotations

import sys
from datetime import datetime
from unittest.mock import MagicMock

import pytest

# ---------------------------------------------------------------------------
# Path setup
# ---------------------------------------------------------------------------

GENESIS_ROOT = "/mnt/e/genesis-system"
if GENESIS_ROOT not in sys.path:
    sys.path.insert(0, GENESIS_ROOT)

# ---------------------------------------------------------------------------
# Imports under test
# ---------------------------------------------------------------------------

from core.merge.compensating_transaction import (  # noqa: E402
    CompensatingTransaction,
    CompensationResult,
    IrreversibleOperationError,
)
from core.coherence.state_delta import StateDelta  # noqa: E402


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def make_delta(agent_id: str, patch: list) -> StateDelta:
    """Create a StateDelta with the given patch stored as a tuple (frozen dataclass)."""
    return StateDelta(
        agent_id=agent_id,
        session_id="sess-test",
        version_at_read=1,
        patch=tuple(patch),
        submitted_at=datetime(2026, 2, 25, 12, 0, 0),
    )


def make_dict_delta(patch: list) -> dict:
    """Create a plain dict delta — tests that _extract_patch handles both types."""
    return {"agent_id": "agent-dict", "patch": patch}


# ===========================================================================
# BB Tests — Black Box
# ===========================================================================


def test_bb1_add_op_generates_remove():
    """BB1: Failed saga with 'add' op → compensating op is 'remove' at same path."""
    state = {"existing": "value"}
    delta = make_delta("agent-A", [{"op": "add", "path": "/new_key", "value": "hello"}])

    ct = CompensatingTransaction()
    result = ct.compensate([delta], state)

    assert isinstance(result, CompensationResult)
    assert len(result.compensating_ops) == 1
    inv = result.compensating_ops[0]
    assert inv["op"] == "remove"
    assert inv["path"] == "/new_key"
    assert result.success is True
    assert result.final_status == "fully_compensated"


def test_bb2_remove_op_restores_original_value():
    """BB2: Failed saga with 'remove' op → compensating 'add' includes original value."""
    state = {"name": "Alice", "status": "active"}
    delta = make_delta("agent-B", [{"op": "remove", "path": "/name"}])

    ct = CompensatingTransaction()
    result = ct.compensate([delta], state)

    assert result.success is True
    assert len(result.compensating_ops) == 1
    inv = result.compensating_ops[0]
    assert inv["op"] == "add"
    assert inv["path"] == "/name"
    assert inv["value"] == "Alice"  # original value from state


def test_bb3_compensation_event_written_to_cold_ledger():
    """BB3: When ops exist, compensation event is written to ColdLedger mock."""
    mock_ledger = MagicMock()
    state = {"x": 42}
    delta = make_delta("agent-C", [{"op": "add", "path": "/y", "value": 99}])

    ct = CompensatingTransaction(cold_ledger=mock_ledger)
    result = ct.compensate([delta], state)

    # The ledger's write_event should have been called once
    mock_ledger.write_event.assert_called_once()
    call_args = mock_ledger.write_event.call_args

    # Positional args: session_id, event_type, payload
    args = call_args[0]
    assert args[1] == "compensation_applied"
    payload = args[2]
    assert "ops_count" in payload
    assert payload["ops_count"] == 1
    assert result.success is True


def test_bb4_replace_op_restores_original_value():
    """BB4: 'replace' op → inverse 'replace' restores the original value from state."""
    state = {"config": {"mode": "slow", "debug": False}}
    delta = make_delta(
        "agent-D",
        [{"op": "replace", "path": "/config/mode", "value": "fast"}],
    )

    ct = CompensatingTransaction()
    result = ct.compensate([delta], state)

    assert result.success is True
    assert len(result.compensating_ops) == 1
    inv = result.compensating_ops[0]
    assert inv["op"] == "replace"
    assert inv["path"] == "/config/mode"
    assert inv["value"] == "slow"  # restored from original state


def test_bb5_multiple_deltas_all_ops_collected():
    """BB5: Multiple failed deltas → inverse ops from all are collected in order."""
    state = {"a": 1, "b": 2, "c": 3}
    delta1 = make_delta("agent-E", [{"op": "add", "path": "/d", "value": 4}])
    delta2 = make_delta("agent-F", [{"op": "replace", "path": "/a", "value": 99}])
    delta3 = make_delta("agent-G", [{"op": "remove", "path": "/b"}])

    ct = CompensatingTransaction()
    result = ct.compensate([delta1, delta2, delta3], state)

    assert result.success is True
    assert len(result.compensating_ops) == 3

    ops_by_path = {op["path"]: op for op in result.compensating_ops}

    # delta1 "add /d" → inverse "remove /d"
    assert ops_by_path["/d"]["op"] == "remove"

    # delta2 "replace /a" → inverse "replace /a" with original value 1
    assert ops_by_path["/a"]["op"] == "replace"
    assert ops_by_path["/a"]["value"] == 1

    # delta3 "remove /b" → inverse "add /b" with original value 2
    assert ops_by_path["/b"]["op"] == "add"
    assert ops_by_path["/b"]["value"] == 2


# ===========================================================================
# WB Tests — White Box
# ===========================================================================


def test_wb1_invert_patch_all_supported_ops():
    """WB1: invert_patch correctly inverts add, remove, replace, and move ops."""
    state = {"key": "original", "src": "src_val"}

    ct = CompensatingTransaction()
    patch = [
        {"op": "add", "path": "/new_key", "value": "v"},
        {"op": "remove", "path": "/key"},
        {"op": "replace", "path": "/key", "value": "changed"},
        {"op": "move", "path": "/dst", "from": "/src"},
    ]
    inverse = ct.invert_patch(patch, state)

    # Should be 4 inverse ops (reversed order of patch)
    assert len(inverse) == 4

    # Original patch last op = move → first inverse op
    assert inverse[0]["op"] == "move"
    assert inverse[0]["path"] == "/src"   # original "from" becomes "path"
    assert inverse[0]["from"] == "/dst"   # original "path" becomes "from"

    # replace → replace with original value
    assert inverse[1]["op"] == "replace"
    assert inverse[1]["path"] == "/key"
    assert inverse[1]["value"] == "original"

    # remove → add with original value
    assert inverse[2]["op"] == "add"
    assert inverse[2]["path"] == "/key"
    assert inverse[2]["value"] == "original"

    # add → remove
    assert inverse[3]["op"] == "remove"
    assert inverse[3]["path"] == "/new_key"


def test_wb2_copy_op_raises_irreversible():
    """WB2: 'copy' op raises IrreversibleOperationError (cannot invert reliably)."""
    state = {"src": "value"}
    ct = CompensatingTransaction()

    with pytest.raises(IrreversibleOperationError) as exc_info:
        ct.invert_patch(
            [{"op": "copy", "path": "/dst", "from": "/src"}],
            state,
        )

    assert "copy" in str(exc_info.value).lower()


def test_wb3_patch_order_reversed():
    """WB3: invert_patch processes ops in reverse order so rollback is correct."""
    state = {"x": "original_x", "y": "original_y"}
    ct = CompensatingTransaction()

    # Two ops: first add /x, then remove /y
    patch = [
        {"op": "add", "path": "/x_new", "value": "new"},
        {"op": "remove", "path": "/y"},
    ]
    inverse = ct.invert_patch(patch, state)

    # First inverse op should be from the LAST patch op (remove /y → add /y)
    assert inverse[0]["op"] == "add"
    assert inverse[0]["path"] == "/y"

    # Second inverse op should be from the FIRST patch op (add /x_new → remove /x_new)
    assert inverse[1]["op"] == "remove"
    assert inverse[1]["path"] == "/x_new"


def test_wb4_extract_patch_handles_state_delta_and_dict():
    """WB4: _extract_patch normalises StateDelta tuple patches and plain dict patches."""
    # StateDelta stores patch as tuple
    delta_obj = make_delta("agent-H", [{"op": "add", "path": "/p", "value": 1}])
    assert isinstance(delta_obj.patch, tuple)  # confirm frozen

    extracted_from_obj = CompensatingTransaction._extract_patch(delta_obj)
    assert isinstance(extracted_from_obj, list)
    assert extracted_from_obj == [{"op": "add", "path": "/p", "value": 1}]

    # Plain dict delta
    dict_delta = make_dict_delta([{"op": "remove", "path": "/q"}])
    extracted_from_dict = CompensatingTransaction._extract_patch(dict_delta)
    assert isinstance(extracted_from_dict, list)
    assert extracted_from_dict == [{"op": "remove", "path": "/q"}]

    # Object with no patch → empty list
    extracted_empty = CompensatingTransaction._extract_patch(object())
    assert extracted_empty == []


# ===========================================================================
# Additional edge cases
# ===========================================================================


def test_empty_failed_deltas_returns_no_ops():
    """Empty failed_deltas list → no ops, success=True, final_status='no_ops'."""
    ct = CompensatingTransaction()
    result = ct.compensate([], current_state={})

    assert result.success is True
    assert result.compensating_ops == []
    assert result.final_status == "no_ops"


def test_copy_op_in_delta_causes_partial_compensation():
    """A delta containing 'copy' is irreversible → partial_compensation, success=False."""
    state = {"src": "val"}
    delta = make_delta(
        "agent-I",
        [{"op": "copy", "path": "/dst", "from": "/src"}],
    )

    ct = CompensatingTransaction()
    result = ct.compensate([delta], state)

    assert result.success is False
    assert result.final_status == "partial_compensation"
    # No ops were produced (the only op was irreversible)
    assert result.compensating_ops == []


def test_mixed_reversible_and_irreversible_ops():
    """A patch mixing reversible and irreversible ops → partial_compensation."""
    state = {"a": 10, "src": "srcval"}
    # One delta with two ops: one reversible (add) and one irreversible (copy)
    delta = make_delta(
        "agent-J",
        [
            {"op": "add", "path": "/z", "value": 99},
            {"op": "copy", "path": "/dst", "from": "/src"},
        ],
    )

    ct = CompensatingTransaction()
    result = ct.compensate([delta], state)

    # The copy triggers IrreversibleOperationError for the whole invert_patch call
    assert result.success is False
    assert result.final_status == "partial_compensation"


def test_ledger_not_called_when_no_ops_produced():
    """ColdLedger should NOT be called when no inverse ops are generated."""
    mock_ledger = MagicMock()
    ct = CompensatingTransaction(cold_ledger=mock_ledger)

    # Empty delta list → no ops
    result = ct.compensate([], current_state={})

    mock_ledger.write_event.assert_not_called()
    assert result.final_status == "no_ops"


def test_get_value_at_path_nested():
    """_get_value_at_path correctly navigates nested dicts."""
    state = {"level1": {"level2": {"level3": "deep_value"}}}
    val = CompensatingTransaction._get_value_at_path(state, "/level1/level2/level3")
    assert val == "deep_value"


def test_get_value_at_path_missing_key_returns_none():
    """_get_value_at_path returns None for missing paths (fail-safe)."""
    state = {"a": 1}
    val = CompensatingTransaction._get_value_at_path(state, "/nonexistent/path")
    assert val is None


def test_get_value_at_path_root_level():
    """_get_value_at_path handles single-level paths correctly."""
    state = {"name": "Genesis"}
    assert CompensatingTransaction._get_value_at_path(state, "/name") == "Genesis"


# ===========================================================================
# Package import test
# ===========================================================================


def test_pkg_init_exports():
    """PKG: core.merge __init__.py exports CompensatingTransaction, CompensationResult,
    and IrreversibleOperationError."""
    from core.merge import (  # noqa: F401
        CompensatingTransaction as CT,
        CompensationResult as CR,
        IrreversibleOperationError as IRE,
    )

    assert CT is CompensatingTransaction
    assert CR is CompensationResult
    assert IRE is IrreversibleOperationError
