"""P2 tests — Input Sensitivity Analysis.

Tests verify:
  1. Stable field (clear winner) → stability_score > 70
  2. Degenerate field (near-identical options) → stability_score < 50
  3. factor_sensitivity has entry for every factor
  4. rank_stability is in [0, 1]
  5. Output is JSON-serializable
  6. N independent colony runs executed (not reused)
"""

import sys
import os
import asyncio
import json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

import numpy as np
from unittest.mock import patch

from afs.mcp.server import afs_sensitivity, _perturb_world_model


# ─── World model fixtures ──────────────────────────────────────────────────────

def _stable_world_model() -> dict:
    """One dominant option that wins on every factor — colony should converge consistently."""
    return {
        "domain": "tool",
        "problem": {"statement": "Which tool?", "objectives": []},
        "concepts": [
            {"name": "quality",   "displayName": "Quality",
             "nature": "outcome", "controllability": "indirect",
             "dataDictionary": {"validRange": [0, 100], "displayUnit": "score"}},
            {"name": "speed",     "displayName": "Speed",
             "nature": "outcome", "controllability": "indirect",
             "dataDictionary": {"validRange": [0, 100], "displayUnit": "score"}},
            {"name": "support",   "displayName": "Support",
             "nature": "outcome", "controllability": "indirect",
             "dataDictionary": {"validRange": [0, 100], "displayUnit": "score"}},
        ],
        "relationships": [
            {"fromConcept": "quality", "toConcept": "speed",   "direction": 1, "strength": 0.7, "nature": "enables",    "mechanism": "Quality drives speed"},
            {"fromConcept": "speed",   "toConcept": "support", "direction": 1, "strength": 0.6, "nature": "enables",    "mechanism": "Fast tools get better support"},
        ],
        "tradeoffs": [],
        "attractors": [
            # Winner: best on all three factors by a large margin
            {"name": "ToolA", "position": {"quality": 90, "speed": 88, "support": 85}, "strength": 0.9},
            # Weak alternatives: well below on all factors
            {"name": "ToolB", "position": {"quality": 30, "speed": 35, "support": 28}, "strength": 0.4},
            {"name": "ToolC", "position": {"quality": 25, "speed": 28, "support": 32}, "strength": 0.4},
        ],
        "referencePoints": [],
        "confidence": {"overall": "high", "weakestLink": "", "dataGaps": []},
    }


def _unstable_world_model() -> dict:
    """Three options arranged in a cyclic causal loop — no option dominates.

    speed → cost (−), cost → safety (−), safety → speed (−) creates a
    rotating force field where no basin is stable. Under perturbation the
    winning option cycles between all three, producing low rank_stability.
    """
    return {
        "domain": "vehicle",
        "problem": {"statement": "Which vehicle type?", "objectives": []},
        "concepts": [
            {"name": "speed",  "displayName": "Speed",
             "nature": "outcome", "controllability": "indirect",
             "dataDictionary": {"validRange": [0, 100], "displayUnit": "score"}},
            {"name": "cost",   "displayName": "Cost",
             "nature": "resource", "controllability": "direct",
             "dataDictionary": {"validRange": [0, 100], "displayUnit": "score"}},
            {"name": "safety", "displayName": "Safety",
             "nature": "outcome", "controllability": "indirect",
             "dataDictionary": {"validRange": [0, 100], "displayUnit": "score"}},
        ],
        "relationships": [
            {"fromConcept": "speed",  "toConcept": "cost",   "direction": -1, "strength": 0.90, "nature": "trades_off", "mechanism": "Fast = expensive"},
            {"fromConcept": "cost",   "toConcept": "safety", "direction": -1, "strength": 0.90, "nature": "trades_off", "mechanism": "Cheap = unsafe"},
            {"fromConcept": "safety", "toConcept": "speed",  "direction": -1, "strength": 0.90, "nature": "trades_off", "mechanism": "Safe = slow"},
        ],
        "tradeoffs": [],
        "attractors": [
            # Each option dominates exactly one factor — genuinely equidistant
            {"name": "Sports Car", "position": {"speed": 90, "cost": 50,  "safety": 30}, "strength": 0.65},
            {"name": "Budget Car", "position": {"speed": 30, "cost": 90,  "safety": 50}, "strength": 0.65},
            {"name": "Safe Car",   "position": {"speed": 50, "cost": 30,  "safety": 90}, "strength": 0.65},
        ],
        "referencePoints": [],
        "confidence": {"overall": "low", "weakestLink": "", "dataGaps": []},
    }


# ─── Tests ────────────────────────────────────────────────────────────────────

def test_sensitivity_stable_on_simple_field():
    """Clear winner across sharply differentiated options → stability_score > 70."""
    async def _run():
        return await afs_sensitivity(
            world_model=_stable_world_model(),
            perturbation=0.2,
            trials=5,
        )
    result = asyncio.run(_run())

    print(f"  stable field: stability_score={result['stability_score']}, "
          f"rank_stability={result['rank_stability']}, top={result['top_strategy']}")

    assert result["stability_score"] >= 65, \
        f"Expected stability_score >= 65 on clear-winner field, got {result['stability_score']}"
    assert result["rank_stability"] > 0.6, \
        f"Expected rank_stability > 0.6, got {result['rank_stability']}"
    assert result["top_strategy"] is not None


def test_sensitivity_unstable_on_degenerate_field():
    """Cyclic causal loop (no dominant option) → low rank_stability, sensitivity_high likely."""
    async def _run():
        return await afs_sensitivity(
            world_model=_unstable_world_model(),
            perturbation=0.35,
            trials=7,
        )
    result = asyncio.run(_run())

    print(f"  unstable field: stability_score={result['stability_score']}, "
          f"rank_stability={result['rank_stability']}, sensitivity_high={result['sensitivity_high']}, "
          f"top={result['top_strategy']}")

    # With a cyclic tradeoff loop and ±35% perturbation, the winner should not
    # be unanimous. rank_stability < 1.0 is the minimum bar; < 0.85 is the goal.
    assert result["rank_stability"] < 1.0, \
        f"Expected rank_stability < 1.0 on cyclic field, got {result['rank_stability']}"


def test_sensitivity_per_factor_scores_present():
    """factor_sensitivity has an entry for every factor in the world model."""
    async def _run():
        return await afs_sensitivity(
            world_model=_stable_world_model(),
            perturbation=0.2,
            trials=3,
        )
    result = asyncio.run(_run())

    factor_names = [c["name"] for c in _stable_world_model()["concepts"]]  # quality, speed, support
    for fn in factor_names:
        assert fn in result["factor_sensitivity"], \
            f"Missing factor_sensitivity entry for '{fn}'"
        entry = result["factor_sensitivity"][fn]
        assert "variance"  in entry
        assert "influence" in entry
        assert entry["influence"] in ("low", "medium", "high"), \
            f"Unexpected influence value: {entry['influence']}"

    print(f"  factor_sensitivity keys: {list(result['factor_sensitivity'].keys())}")


def test_sensitivity_rank_stability_range():
    """rank_stability is always in [0, 1]."""
    async def _run():
        r1 = await afs_sensitivity(world_model=_stable_world_model(),   perturbation=0.2, trials=4)
        r2 = await afs_sensitivity(world_model=_unstable_world_model(), perturbation=0.2, trials=4)
        return r1, r2

    r1, r2 = asyncio.run(_run())

    for label, r in [("stable", r1), ("degenerate", r2)]:
        rs = r["rank_stability"]
        assert 0.0 <= rs <= 1.0, f"{label}: rank_stability={rs} out of [0,1]"
        ss = r["stability_score"]
        assert 0 <= ss <= 100, f"{label}: stability_score={ss} out of [0,100]"
        print(f"  {label}: rank_stability={rs}, stability_score={ss}")


def test_sensitivity_serializable():
    """Full output is JSON-serializable (no numpy types, no non-serializable values)."""
    async def _run():
        return await afs_sensitivity(
            world_model=_stable_world_model(),
            perturbation=0.15,
            trials=3,
        )
    result = asyncio.run(_run())

    try:
        serialized = json.dumps(result)
        reparsed = json.loads(serialized)
    except (TypeError, ValueError) as e:
        raise AssertionError(f"Output not JSON-serializable: {e}")

    assert reparsed["stability_score"] == result["stability_score"]
    print(f"  serialization OK, payload size={len(serialized)} bytes")


def test_sensitivity_trials_run_independent():
    """N independent colony runs are executed, not reused."""
    call_count = {"n": 0}
    original_run_colony = __import__(
        "afs.mcp.server", fromlist=["_run_colony"]
    )._run_colony

    def counting_run_colony(engine, generations, on_progress=None):
        call_count["n"] += 1
        return original_run_colony(engine, generations, on_progress)

    async def _run():
        with patch("afs.mcp.server._run_colony", side_effect=counting_run_colony):
            return await afs_sensitivity(
                world_model=_stable_world_model(),
                perturbation=0.2,
                trials=4,
            )

    result = asyncio.run(_run())

    assert call_count["n"] == 4, \
        f"Expected exactly 4 _run_colony calls (one per trial), got {call_count['n']}"
    assert result["trial_count"] == 4
    print(f"  independent trials confirmed: {call_count['n']} colony runs for trials=4")


# ─── Runner ───────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    tests = [
        test_sensitivity_stable_on_simple_field,
        test_sensitivity_unstable_on_degenerate_field,
        test_sensitivity_per_factor_scores_present,
        test_sensitivity_rank_stability_range,
        test_sensitivity_serializable,
        test_sensitivity_trials_run_independent,
    ]

    passed = 0
    failed = 0
    for test_fn in tests:
        name = test_fn.__name__
        try:
            test_fn()
            print(f"PASS  {name}")
            passed += 1
        except Exception as exc:
            print(f"FAIL  {name}: {exc}")
            import traceback
            traceback.print_exc()
            failed += 1

    print(f"\n{passed}/{passed + failed} tests passed")
    if failed:
        sys.exit(1)
