"""
tests/track_b/test_story_7_04.py

Story B-7.04: PatchReconciler — Merged Patch Validator + Applier

Black Box Tests (BB1–BB5):
    BB1  Valid "add" op → valid=True, new_state contains the added key
    BB2  Patch op dict missing "op" key → valid=False, errors describe the issue
    BB3  State result contains "API_KEY" → axiom check fails, valid=False
    BB4  Valid "replace" op → valid=True, value replaced in new_state
    BB5  "remove" op on an existing key → valid=True, key absent from new_state

White Box Tests (WB1–WB4):
    WB1  Dry-run uses state.copy() — original state NOT mutated on failure
    WB2  All 3 validation steps run: schema + apply + axiom errors accumulated
    WB3  FORBIDDEN_PATTERNS contains "sqlite3", "API_KEY", "sk-"
    WB4  Schema check validates each patch op has both "op" and "path"

Package Tests:
    PKG1  PatchReconciler importable from core.merge
    PKG2  ReconcileResult importable from core.merge
"""

from __future__ import annotations

import sys
import copy
from typing import Any

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.patch_reconciler import (  # noqa: E402
    PatchReconciler,
    ReconcileResult,
    FORBIDDEN_PATTERNS,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

RECONCILER = PatchReconciler()

BASE_STATE: dict = {
    "status": "active",
    "version": 1,
    "config": {
        "mode": "fast",
        "retries": 3,
    },
}


def fresh_state() -> dict:
    """Return a deep copy of BASE_STATE so tests never share mutable state."""
    return copy.deepcopy(BASE_STATE)


# ===========================================================================
# BB Tests — Black Box
# ===========================================================================


def test_bb1_valid_add_op_returns_new_state():
    """BB1: Valid 'add' op → valid=True and new key present in new_state."""
    state = fresh_state()
    patch = [{"op": "add", "path": "/new_key", "value": "hello"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is True
    assert result.errors == []
    assert result.new_state is not None
    assert result.new_state["new_key"] == "hello"


def test_bb2_missing_op_key_returns_invalid():
    """BB2: Patch entry missing 'op' → valid=False, error references the missing key."""
    state = fresh_state()
    # "op" is omitted — only "path" present
    patch = [{"path": "/status", "value": "inactive"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is False
    assert result.new_state is None
    assert len(result.errors) > 0
    # At least one error should mention the missing "op" key
    combined = " ".join(result.errors)
    assert "op" in combined


def test_bb3_forbidden_api_key_pattern_in_result_is_invalid():
    """BB3: Adding a value containing 'API_KEY' → axiom violation, valid=False."""
    state = fresh_state()
    patch = [{"op": "add", "path": "/secret", "value": "MY_API_KEY_HERE"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is False
    assert result.new_state is None
    assert any("API_KEY" in err for err in result.errors)


def test_bb4_valid_replace_op_updates_value():
    """BB4: Valid 'replace' op → valid=True, value updated in new_state."""
    state = fresh_state()
    patch = [{"op": "replace", "path": "/status", "value": "inactive"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is True
    assert result.new_state is not None
    assert result.new_state["status"] == "inactive"
    # Original state must be untouched
    assert state["status"] == "active"


def test_bb5_remove_op_deletes_key():
    """BB5: Valid 'remove' op on an existing key → valid=True, key gone."""
    state = fresh_state()
    patch = [{"op": "remove", "path": "/version"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is True
    assert result.new_state is not None
    assert "version" not in result.new_state
    # Original state must be untouched
    assert "version" in state


# ===========================================================================
# WB Tests — White Box
# ===========================================================================


def test_wb1_original_state_not_mutated_on_failure():
    """WB1: Dry-run operates on state.copy() — original state is never modified."""
    state = {"x": 1, "y": 2}
    state_snapshot = copy.deepcopy(state)

    # Patch that will fail axiom check (API_KEY in value)
    patch = [{"op": "add", "path": "/creds", "value": "API_KEY=abc123"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is False
    # Original state dict must remain unchanged
    assert state == state_snapshot, (
        f"State was mutated! Before: {state_snapshot}, After: {state}"
    )


def test_wb2_all_three_steps_run_errors_accumulated():
    """
    WB2: All 3 validation steps run — if schema passes but axiom fails,
    both the dry-run result AND axiom errors are accumulated.

    We trigger this by supplying a structurally valid patch that produces a
    state containing the forbidden 'sqlite3' pattern.
    """
    state = {"db": "postgres"}
    # Valid schema: 'op' and 'path' both present
    patch = [{"op": "replace", "path": "/db", "value": "sqlite3_backend"}]

    result = RECONCILER.validate_and_apply(state, patch)

    # Schema step passes (no schema errors)
    # Apply step passes (valid replace)
    # Axiom step fails (sqlite3 in new_state)
    assert result.valid is False
    assert result.new_state is None
    axiom_errors = [e for e in result.errors if "sqlite3" in e]
    assert len(axiom_errors) >= 1, f"Expected axiom error for sqlite3, got: {result.errors}"


def test_wb3_forbidden_patterns_contains_required_entries():
    """WB3: FORBIDDEN_PATTERNS module constant includes 'sqlite3', 'API_KEY', 'sk-'."""
    assert "sqlite3" in FORBIDDEN_PATTERNS, "FORBIDDEN_PATTERNS missing 'sqlite3'"
    assert "API_KEY" in FORBIDDEN_PATTERNS, "FORBIDDEN_PATTERNS missing 'API_KEY'"
    assert "sk-" in FORBIDDEN_PATTERNS, "FORBIDDEN_PATTERNS missing 'sk-'"


def test_wb4_schema_check_validates_both_op_and_path():
    """WB4: Schema check rejects ops missing 'op', missing 'path', or missing both."""
    reconciler = PatchReconciler()

    # Case A: missing 'op'
    errors_a = reconciler._check_schema([{"path": "/x", "value": 1}])
    assert len(errors_a) > 0
    assert any("op" in e for e in errors_a)

    # Case B: missing 'path'
    errors_b = reconciler._check_schema([{"op": "add", "value": 1}])
    assert len(errors_b) > 0
    assert any("path" in e for e in errors_b)

    # Case C: missing both
    errors_c = reconciler._check_schema([{"value": 1}])
    assert len(errors_c) > 0

    # Case D: valid entry → no errors
    errors_d = reconciler._check_schema([{"op": "add", "path": "/k", "value": 42}])
    assert errors_d == []


# ===========================================================================
# Additional edge-case and integration tests
# ===========================================================================


def test_nested_path_add():
    """Adding a key inside a nested dict via '/parent/child' path."""
    state = {"config": {"mode": "slow"}}
    patch = [{"op": "add", "path": "/config/debug", "value": True}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is True
    assert result.new_state["config"]["debug"] is True
    # Original nested dict must not be mutated
    assert "debug" not in state["config"]


def test_nested_path_replace():
    """Replacing a nested key value."""
    state = {"config": {"mode": "slow", "retries": 3}}
    patch = [{"op": "replace", "path": "/config/mode", "value": "fast"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is True
    assert result.new_state["config"]["mode"] == "fast"


def test_remove_nonexistent_key_returns_error():
    """Attempting to remove a key that doesn't exist produces an apply error."""
    state = {"x": 1}
    patch = [{"op": "remove", "path": "/nonexistent"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is False
    assert any("nonexistent" in e or "remove" in e for e in result.errors)


def test_replace_nonexistent_key_returns_error():
    """Replacing a key that doesn't exist is an RFC 6902 error."""
    state = {"x": 1}
    patch = [{"op": "replace", "path": "/missing_key", "value": 99}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is False


def test_empty_patch_is_valid():
    """An empty patch list produces a valid result with state unchanged."""
    state = {"a": 1}
    result = RECONCILER.validate_and_apply(state, [])

    assert result.valid is True
    assert result.new_state == {"a": 1}
    assert result.errors == []


def test_patch_not_a_list_returns_schema_error():
    """If patch is not a list at all, schema check fails gracefully."""
    state = {"a": 1}
    result = RECONCILER.validate_and_apply(state, "not-a-list")  # type: ignore[arg-type]

    assert result.valid is False
    assert any("list" in e for e in result.errors)


def test_sk_dash_forbidden_pattern():
    """Value containing 'sk-' (e.g. OpenAI key prefix) is axiom-blocked."""
    state = {"x": "ok"}
    patch = [{"op": "add", "path": "/token", "value": "sk-abc123xyz"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is False
    assert any("sk-" in e for e in result.errors)


def test_sqlite3_forbidden_pattern():
    """Value containing 'sqlite3' is axiom-blocked."""
    state = {"db": "postgres"}
    patch = [{"op": "replace", "path": "/db", "value": "sqlite3"}]

    result = RECONCILER.validate_and_apply(state, patch)

    assert result.valid is False
    assert any("sqlite3" in e for e in result.errors)


# ===========================================================================
# Package import tests
# ===========================================================================


def test_pkg1_patch_reconciler_importable_from_core_merge():
    """PKG1: PatchReconciler importable from core.merge package."""
    from core.merge import PatchReconciler as PR  # noqa: F401
    assert PR is PatchReconciler


def test_pkg2_reconcile_result_importable_from_core_merge():
    """PKG2: ReconcileResult importable from core.merge package."""
    from core.merge import ReconcileResult as RR  # noqa: F401
    assert RR is ReconcileResult
