"""
tests/track_b/test_story_7_03.py

Story B-7.03: MergePromptBuilder — Structured Conflict Prompt

Black Box Tests (BB1–BB5):
    BB1  Prompt contains "You are the Genesis Semantic Reducer"
    BB2  All conflicting agent IDs appear in prompt text
    BB3  Current state > 2000 chars → truncated with "... [truncated]" indicator
    BB4  Current state < 2000 chars → not truncated, full JSON in prompt
    BB5  "Respond ONLY with valid JSON" instruction present in prompt

White Box Tests (WB1–WB4):
    WB1  Output is deterministic — calling build() twice returns identical string
    WB2  _truncate_state with 3000-char JSON → returns string ≤ MAX_STATE_CHARS length
    WB3  _format_worker_proposals labels each delta on its own section
    WB4  _extract_agent_id works with both StateDelta-like objects and plain dicts

Package Test:
    PKG1 `from core.merge import MergePromptBuilder` works after __init__.py update
"""

from __future__ import annotations

import sys
import json
from datetime import datetime

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.merge_prompt_builder import MergePromptBuilder, MAX_STATE_CHARS  # noqa: E402
from core.merge.conflict_detector import ConflictDetector, ConflictReport  # noqa: E402
from core.coherence.state_delta import StateDelta  # noqa: E402


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_delta(
    agent_id: str,
    patch: list[dict],
    session_id: str = "sess-001",
    version: int = 1,
) -> StateDelta:
    """Create a StateDelta with the given patch (stored as tuple)."""
    return StateDelta(
        agent_id=agent_id,
        session_id=session_id,
        version_at_read=version,
        patch=tuple(patch),
        submitted_at=datetime(2026, 2, 25, 12, 0, 0),
    )


def make_conflict_report(
    has_conflicts: bool = True,
    conflicting_paths: list[str] | None = None,
    conflict_types: list[str] | None = None,
    non_conflicting_deltas: list | None = None,
) -> ConflictReport:
    return ConflictReport(
        has_conflicts=has_conflicts,
        conflicting_paths=conflicting_paths or ["/status"],
        conflict_types=conflict_types or ["path_collision"],
        non_conflicting_deltas=non_conflicting_deltas or [],
    )


BUILDER = MergePromptBuilder()
DETECTOR = ConflictDetector()

SMALL_STATE = {"status": "active", "version": 3}
LARGE_STATE = {"data": "x" * 3000}  # > 2000 chars when JSON-serialised


# ===========================================================================
# BB Tests — Black Box
# ===========================================================================


def test_bb1_prompt_contains_genesis_semantic_reducer():
    """BB1: Prompt starts with the required system identity header."""
    delta_a = make_delta("agent-A", [{"op": "replace", "path": "/status", "value": "active"}])
    delta_b = make_delta("agent-B", [{"op": "replace", "path": "/status", "value": "inactive"}])
    report = make_conflict_report()

    prompt = BUILDER.build([delta_a, delta_b], report, SMALL_STATE, version=1)

    assert "You are the Genesis Semantic Reducer" in prompt


def test_bb2_all_conflicting_agent_ids_appear_in_prompt():
    """BB2: Every agent_id involved in the conflict is visible in the prompt."""
    delta_a = make_delta("agent-ALPHA", [{"op": "replace", "path": "/x", "value": 1}])
    delta_b = make_delta("agent-BETA",  [{"op": "replace", "path": "/x", "value": 2}])
    report = make_conflict_report(conflicting_paths=["/x"])

    prompt = BUILDER.build([delta_a, delta_b], report, SMALL_STATE, version=0)

    assert "agent-ALPHA" in prompt
    assert "agent-BETA" in prompt


def test_bb3_large_state_is_truncated_with_marker():
    """BB3: State JSON > MAX_STATE_CHARS → truncated and '... [truncated]' appended."""
    delta = make_delta("agent-Z", [{"op": "add", "path": "/k", "value": "v"}])
    report = make_conflict_report()

    prompt = BUILDER.build([delta], report, LARGE_STATE, version=0)

    assert "... [truncated]" in prompt


def test_bb4_small_state_not_truncated():
    """BB4: State JSON < MAX_STATE_CHARS → full JSON present, no truncation marker."""
    delta = make_delta("agent-Z", [{"op": "add", "path": "/k", "value": "v"}])
    report = make_conflict_report()

    prompt = BUILDER.build([delta], report, SMALL_STATE, version=0)

    assert "... [truncated]" not in prompt
    # The small state keys should be readable in the prompt
    assert "status" in prompt


def test_bb5_respond_only_with_json_instruction_present():
    """BB5: The LLM response format instruction must be present verbatim."""
    delta = make_delta("agent-A", [{"op": "replace", "path": "/x", "value": 1}])
    report = make_conflict_report()

    prompt = BUILDER.build([delta], report, SMALL_STATE, version=2)

    assert "Respond ONLY with valid JSON" in prompt


# ===========================================================================
# WB Tests — White Box
# ===========================================================================


def test_wb1_output_is_deterministic():
    """WB1: Identical inputs always produce identical output strings."""
    delta_a = make_delta("agent-A", [{"op": "replace", "path": "/v", "value": 10}])
    delta_b = make_delta("agent-B", [{"op": "replace", "path": "/v", "value": 20}])
    report = make_conflict_report(conflicting_paths=["/v"])

    prompt_1 = BUILDER.build([delta_a, delta_b], report, SMALL_STATE, version=5)
    prompt_2 = BUILDER.build([delta_a, delta_b], report, SMALL_STATE, version=5)

    assert prompt_1 == prompt_2


def test_wb2_truncate_state_max_chars():
    """WB2: _truncate_state with a 3000-char JSON returns a string <= MAX_STATE_CHARS."""
    big_state = {"data": "y" * 3000}
    result = BUILDER._truncate_state(big_state)

    # Must not exceed MAX_STATE_CHARS (the truncation marker is part of that budget)
    assert len(result) <= MAX_STATE_CHARS
    assert result.endswith("... [truncated]")


def test_wb3_format_worker_proposals_labels_each_delta():
    """WB3: _format_worker_proposals produces labelled sections for each delta."""
    delta_a = make_delta("worker-1", [{"op": "add", "path": "/a", "value": 1}])
    delta_b = make_delta("worker-2", [{"op": "add", "path": "/b", "value": 2}])

    formatted = BUILDER._format_worker_proposals([delta_a, delta_b])

    # Each worker label must appear
    assert "Worker worker-1:" in formatted
    assert "Worker worker-2:" in formatted
    # Patches should appear as JSON (keys visible)
    assert '"path"' in formatted
    assert '"op"' in formatted


def test_wb4_extract_agent_id_statedelta_and_dict():
    """WB4: _extract_agent_id works with StateDelta objects AND plain dicts."""
    delta_obj = make_delta("state-delta-agent", [])
    result_obj = BUILDER._extract_agent_id(delta_obj)
    assert result_obj == "state-delta-agent"

    delta_dict = {"agent_id": "dict-agent", "patch": []}
    result_dict = BUILDER._extract_agent_id(delta_dict)
    assert result_dict == "dict-agent"

    # Missing agent_id → fallback to "unknown"
    result_missing = BUILDER._extract_agent_id({"patch": []})
    assert result_missing == "unknown"


# ===========================================================================
# Package import test
# ===========================================================================


def test_pkg1_from_core_merge_import_merge_prompt_builder():
    """PKG1: MergePromptBuilder importable from core.merge package."""
    from core.merge import MergePromptBuilder as MPB  # noqa: F401
    assert MPB is MergePromptBuilder
