#!/usr/bin/env python3
"""
Tests for Story 6.02: SwarmMergeInterceptor — Opus Reducer
AIVA RLM Nexus PRD v2 — Track A, Module 6

Black-box tests (BB1-BB4): verify public API behaviour from the outside.
White-box tests (WB1-WB4): verify internal invariants and structural properties.
Integration test (IT1): full reduce() with 2 conflicting results.

All tests use mocks — zero real API calls to Opus.
"""

from __future__ import annotations

import inspect
import json
import sys
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

sys.path.insert(0, "/mnt/e/genesis-system")

from core.merge.semantic_merge_interceptor_v2 import (
    MERGE_PROMPT,
    SwarmMergeInterceptor,
)
from core.merge.swarm_result import SwarmResult


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _ts() -> datetime:
    """Return a fixed UTC timestamp for test stability."""
    return datetime(2026, 2, 25, 10, 0, 0, tzinfo=timezone.utc)


def _make_result(
    session_id: str = "sess-001",
    worker_name: str = "worker-A",
    output: dict | None = None,
    confidence: float = 0.9,
) -> SwarmResult:
    """Return a minimal valid SwarmResult with optional overrides."""
    return SwarmResult(
        session_id=session_id,
        worker_name=worker_name,
        output=output if output is not None else {},
        completed_at=_ts(),
        confidence=confidence,
    )


def _make_opus_response(resolved_value, reasoning="test reasoning", winner="MERGE"):
    """Build a mock Opus response object with a .text attribute."""
    payload = {
        "resolved_value": resolved_value,
        "reasoning": reasoning,
        "winner": winner,
    }
    mock_resp = MagicMock()
    mock_resp.text = json.dumps(payload)
    return mock_resp


def _make_opus_client(resolved_value=None, reasoning="test reasoning", winner="MERGE"):
    """Build an AsyncMock Opus client that returns a canned resolution."""
    client = MagicMock()
    client.generate_content_async = AsyncMock(
        return_value=_make_opus_response(resolved_value, reasoning, winner)
    )
    return client


# ---------------------------------------------------------------------------
# BB1: No conflicts → merged dict has all keys from both results (no Opus call)
# ---------------------------------------------------------------------------


class TestBB1_NoConflictsNoOpusCall:
    """BB1: When results have no conflicting keys, reduce() merges without calling Opus."""

    @pytest.mark.asyncio
    async def test_non_overlapping_keys_merged(self):
        r_a = _make_result(worker_name="A", output={"name": "George"})
        r_b = _make_result(worker_name="B", output={"location": "Cairns"})
        opus = _make_opus_client()

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert result["name"] == "George"
        assert result["location"] == "Cairns"
        opus.generate_content_async.assert_not_called()

    @pytest.mark.asyncio
    async def test_no_merge_reasoning_key_when_no_conflicts(self):
        r_a = _make_result(worker_name="A", output={"x": 1})
        r_b = _make_result(worker_name="B", output={"y": 2})
        opus = _make_opus_client()

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert "_merge_reasoning" not in result

    @pytest.mark.asyncio
    async def test_same_key_same_value_no_conflict_no_opus(self):
        """Identical values on both sides are not conflicts."""
        r_a = _make_result(worker_name="A", output={"status": "ok"})
        r_b = _make_result(worker_name="B", output={"status": "ok"})
        opus = _make_opus_client()

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert result["status"] == "ok"
        opus.generate_content_async.assert_not_called()


# ---------------------------------------------------------------------------
# BB2: One conflict → Opus called once, conflict resolved
# ---------------------------------------------------------------------------


class TestBB2_OneConflictOpusCalledOnce:
    """BB2: Single conflicting key causes exactly one Opus call."""

    @pytest.mark.asyncio
    async def test_opus_called_once_for_one_conflict(self):
        r_a = _make_result(worker_name="A", output={"status": "confirmed"})
        r_b = _make_result(worker_name="B", output={"status": "pending"})
        opus = _make_opus_client(resolved_value="confirmed", winner="A")

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        opus.generate_content_async.assert_called_once()
        # Winner "A" → raw value from result_a used
        assert result["status"] == "confirmed"

    @pytest.mark.asyncio
    async def test_winner_b_returns_result_b_value(self):
        r_a = _make_result(worker_name="A", output={"status": "confirmed"})
        r_b = _make_result(worker_name="B", output={"status": "pending"})
        opus = _make_opus_client(resolved_value="pending", winner="B")

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert result["status"] == "pending"


# ---------------------------------------------------------------------------
# BB3: Returned dict has _merge_reasoning key when conflicts exist
# ---------------------------------------------------------------------------


class TestBB3_MergeReasoningKey:
    """BB3: _merge_reasoning is present in returned dict when conflicts occurred."""

    @pytest.mark.asyncio
    async def test_merge_reasoning_key_present(self):
        r_a = _make_result(worker_name="A", output={"status": "confirmed"})
        r_b = _make_result(worker_name="B", output={"status": "pending"})
        opus = _make_opus_client(resolved_value="confirmed", winner="MERGE")

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert "_merge_reasoning" in result

    @pytest.mark.asyncio
    async def test_merge_reasoning_is_list(self):
        r_a = _make_result(worker_name="A", output={"status": "confirmed"})
        r_b = _make_result(worker_name="B", output={"status": "pending"})
        opus = _make_opus_client(resolved_value="confirmed", winner="MERGE")

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert isinstance(result["_merge_reasoning"], list)
        assert len(result["_merge_reasoning"]) == 1

    @pytest.mark.asyncio
    async def test_reasoning_entry_has_expected_fields(self):
        r_a = _make_result(worker_name="A", output={"status": "confirmed"})
        r_b = _make_result(worker_name="B", output={"status": "pending"})
        opus = _make_opus_client(
            resolved_value="confirmed",
            reasoning="Worker A's value aligns with the booking confirmation.",
            winner="A",
        )

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        entry = result["_merge_reasoning"][0]
        assert entry["key"] == "status"
        assert entry["winner"] in {"A", "B", "MERGE"}
        assert "reasoning" in entry
        assert "worker_a" in entry
        assert "worker_b" in entry


# ---------------------------------------------------------------------------
# BB4: winner="MERGE" case → blended value from reasoning
# ---------------------------------------------------------------------------


class TestBB4_WinnerMerge:
    """BB4: winner=MERGE uses the resolved_value Opus returned (not raw A or B)."""

    @pytest.mark.asyncio
    async def test_merge_winner_uses_resolved_value(self):
        r_a = _make_result(worker_name="A", output={"score": 80})
        r_b = _make_result(worker_name="B", output={"score": 90})
        # Opus returns a blended value, e.g. 85
        opus = _make_opus_client(resolved_value=85, winner="MERGE")

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert result["score"] == 85

    @pytest.mark.asyncio
    async def test_merge_winner_stored_in_reasoning(self):
        r_a = _make_result(worker_name="A", output={"score": 80})
        r_b = _make_result(worker_name="B", output={"score": 90})
        opus = _make_opus_client(resolved_value=85, winner="MERGE")

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        entry = result["_merge_reasoning"][0]
        assert entry["winner"] == "MERGE"


# ---------------------------------------------------------------------------
# WB1: Opus client not called on non-conflict path
# ---------------------------------------------------------------------------


class TestWB1_OpusNotCalledOnNonConflict:
    """WB1: generate_content_async is never invoked when no conflicts exist."""

    @pytest.mark.asyncio
    async def test_generate_content_async_not_called(self):
        r_a = _make_result(worker_name="A", output={"a": 1})
        r_b = _make_result(worker_name="B", output={"b": 2})
        opus = MagicMock()
        opus.generate_content_async = AsyncMock()

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        await interceptor.reduce([r_a, r_b])

        opus.generate_content_async.assert_not_called()

    @pytest.mark.asyncio
    async def test_empty_results_no_opus_call(self):
        opus = MagicMock()
        opus.generate_content_async = AsyncMock()

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([])

        opus.generate_content_async.assert_not_called()
        assert result == {}


# ---------------------------------------------------------------------------
# WB2: MERGE_PROMPT formatted with all 5 variables
# ---------------------------------------------------------------------------


class TestWB2_PromptFormattingAllVariables:
    """WB2: MERGE_PROMPT template fills all 5 placeholders correctly."""

    def test_merge_prompt_has_five_placeholders(self):
        placeholders = [
            "{session_id}",
            "{worker_a}",
            "{output_a}",
            "{worker_b}",
            "{output_b}",
            "{conflict_key}",
        ]
        for ph in placeholders:
            assert ph in MERGE_PROMPT, f"MERGE_PROMPT missing placeholder: {ph}"

    @pytest.mark.asyncio
    async def test_prompt_sent_to_opus_contains_all_variables(self):
        r_a = _make_result(
            session_id="sess-007",
            worker_name="AlphaWorker",
            output={"status": "confirmed"},
        )
        r_b = _make_result(
            session_id="sess-007",
            worker_name="BetaWorker",
            output={"status": "pending"},
        )
        opus = MagicMock()
        captured_prompts: list[str] = []

        async def capture_prompt(prompt: str):
            captured_prompts.append(prompt)
            resp = MagicMock()
            resp.text = json.dumps(
                {"resolved_value": "confirmed", "reasoning": "r", "winner": "A"}
            )
            return resp

        opus.generate_content_async = capture_prompt

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        await interceptor.reduce([r_a, r_b])

        assert len(captured_prompts) == 1
        prompt = captured_prompts[0]
        assert "sess-007" in prompt
        assert "AlphaWorker" in prompt
        assert "BetaWorker" in prompt
        assert "status" in prompt
        assert "confirmed" in prompt
        assert "pending" in prompt


# ---------------------------------------------------------------------------
# WB3: SwarmConflictDetector used internally (not raw comparison)
# ---------------------------------------------------------------------------


class TestWB3_SwarmConflictDetectorUsed:
    """WB3: SwarmConflictDetector is present as _detector attribute."""

    def test_detector_attribute_is_swarm_conflict_detector(self):
        from core.merge.swarm_result import SwarmConflictDetector

        opus = MagicMock()
        interceptor = SwarmMergeInterceptor(opus_client=opus)
        assert isinstance(interceptor._detector, SwarmConflictDetector)

    def test_class_name_is_swarm_merge_interceptor(self):
        assert SwarmMergeInterceptor.__name__ == "SwarmMergeInterceptor"

    def test_module_does_not_shadow_track_b_class(self):
        """SwarmMergeInterceptor must not accidentally import SemanticMergeInterceptor."""
        import core.merge.semantic_merge_interceptor_v2 as module_v2
        assert not hasattr(module_v2, "SemanticMergeInterceptor"), (
            "Track A module must not expose SemanticMergeInterceptor (Track B name)"
        )


# ---------------------------------------------------------------------------
# WB4: JSON parse failure from Opus → fallback to higher-confidence result
# ---------------------------------------------------------------------------


class TestWB4_OpusParseFailureFallback:
    """WB4: When Opus returns malformed JSON, the higher-confidence result wins."""

    @pytest.mark.asyncio
    async def test_fallback_to_higher_confidence_on_bad_json(self):
        # result_b has higher confidence
        r_a = _make_result(
            worker_name="A", output={"status": "confirmed"}, confidence=0.6
        )
        r_b = _make_result(
            worker_name="B", output={"status": "pending"}, confidence=0.95
        )

        opus = MagicMock()
        bad_resp = MagicMock()
        bad_resp.text = "THIS IS NOT JSON {{{"
        opus.generate_content_async = AsyncMock(return_value=bad_resp)

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        # Should fall back to result_b (confidence 0.95 > 0.6)
        assert result["status"] == "pending"

    @pytest.mark.asyncio
    async def test_fallback_reasoning_entry_present(self):
        r_a = _make_result(
            worker_name="A", output={"status": "confirmed"}, confidence=0.6
        )
        r_b = _make_result(
            worker_name="B", output={"status": "pending"}, confidence=0.95
        )

        opus = MagicMock()
        bad_resp = MagicMock()
        bad_resp.text = "not-json"
        opus.generate_content_async = AsyncMock(return_value=bad_resp)

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        assert "_merge_reasoning" in result
        entry = result["_merge_reasoning"][0]
        assert "parse failure" in entry["reasoning"].lower() or "fallback" in entry["reasoning"].lower()

    @pytest.mark.asyncio
    async def test_fallback_to_result_a_on_tie(self):
        """On equal confidence, result_a wins (first result)."""
        r_a = _make_result(
            worker_name="A", output={"status": "confirmed"}, confidence=0.8
        )
        r_b = _make_result(
            worker_name="B", output={"status": "pending"}, confidence=0.8
        )

        opus = MagicMock()
        bad_resp = MagicMock()
        bad_resp.text = "{{bad json}}"
        opus.generate_content_async = AsyncMock(return_value=bad_resp)

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        # Equal confidence → result_a wins
        assert result["status"] == "confirmed"


# ---------------------------------------------------------------------------
# IT1: Full reduce with 2 conflicting results → merged dict returned
# ---------------------------------------------------------------------------


class TestIT1_FullReduceWithConflicts:
    """IT1: End-to-end reduce() with 2 conflicting results returns a merged dict."""

    @pytest.mark.asyncio
    async def test_full_reduce_returns_merged_dict(self):
        r_a = _make_result(
            session_id="sess-integration",
            worker_name="WorkerAlpha",
            output={"intent": "book", "location": "Brisbane"},
            confidence=0.85,
        )
        r_b = _make_result(
            session_id="sess-integration",
            worker_name="WorkerBeta",
            output={"intent": "cancel", "service": "plumbing"},
            confidence=0.75,
        )

        # "intent" conflicts; "location" and "service" are complementary
        opus = _make_opus_client(resolved_value="book", winner="A")

        interceptor = SwarmMergeInterceptor(opus_client=opus)
        result = await interceptor.reduce([r_a, r_b])

        # Resolved conflict key
        assert result["intent"] == "book"
        # Complementary keys preserved
        assert result["location"] == "Brisbane"
        assert result["service"] == "plumbing"
        # Reasoning present
        assert "_merge_reasoning" in result
        assert len(result["_merge_reasoning"]) == 1
        entry = result["_merge_reasoning"][0]
        assert entry["key"] == "intent"
        assert entry["worker_a"] == "WorkerAlpha"
        assert entry["worker_b"] == "WorkerBeta"

    @pytest.mark.asyncio
    async def test_no_sqlite3_import_in_module(self):
        """Rule 7: sqlite3 is FORBIDDEN."""
        import core.merge.semantic_merge_interceptor_v2 as mod

        src = inspect.getsource(mod)
        assert "import sqlite3" not in src, (
            "sqlite3 is FORBIDDEN in semantic_merge_interceptor_v2.py (Rule 7)"
        )

    @pytest.mark.asyncio
    async def test_class_names_not_colliding_with_track_b(self):
        """SwarmMergeInterceptor must not share name with SemanticMergeInterceptor."""
        from core.merge.semantic_merge_interceptor import SemanticMergeInterceptor
        assert SwarmMergeInterceptor.__name__ != SemanticMergeInterceptor.__name__
        assert SwarmMergeInterceptor is not SemanticMergeInterceptor


# ---------------------------------------------------------------------------
# Run summary
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    result = pytest.main([__file__, "-v", "--tb=short"])
    sys.exit(result)
