"""Tests for the scenario simulator."""

from __future__ import annotations

import json
import tempfile

from orxaq.intelligence.simulator import (
    ADVERSE_SCENARIO,
    BASELINE_SCENARIO,
    BUILT_IN_SCENARIOS,
    SEVERE_SCENARIO,
    PropagationStep,
    Scenario,
    ScenarioSimulator,
    Shock,
    SimulationResult,
)
from orxaq.kernel.types import CausalEdge, Entity
from orxaq.kernel.world_dag import WorldDAG


def _make_dag() -> WorldDAG:
    """Create a simple test DAG: A -> B -> C."""
    dag = WorldDAG()
    dag.add_entity(Entity(entity_id="a", name="A"))
    dag.add_entity(Entity(entity_id="b", name="B"))
    dag.add_entity(Entity(entity_id="c", name="C"))
    dag.add_edge(CausalEdge(source="a", target="b", confidence=0.8))
    dag.add_edge(CausalEdge(source="b", target="c", confidence=0.6))
    return dag


def _make_credit_dag() -> WorldDAG:
    """Create a realistic credit DAG for testing."""
    dag = WorldDAG()
    for name in [
        "unemployment_rate",
        "fed_funds_rate",
        "gdp_growth",
        "income",
        "dti",
        "credit_score",
        "default_flag",
    ]:
        dag.add_entity(Entity(entity_id=name, name=name))
    dag.add_edge(CausalEdge(source="unemployment_rate", target="income", confidence=0.7))
    dag.add_edge(CausalEdge(source="fed_funds_rate", target="dti", confidence=0.5))
    dag.add_edge(CausalEdge(source="gdp_growth", target="income", confidence=0.4))
    dag.add_edge(CausalEdge(source="income", target="credit_score", confidence=0.6))
    dag.add_edge(CausalEdge(source="income", target="dti", confidence=0.5))
    dag.add_edge(CausalEdge(source="credit_score", target="default_flag", confidence=0.8))
    dag.add_edge(CausalEdge(source="dti", target="default_flag", confidence=0.7))
    return dag


# ── Shock ────────────────────────────────────────────────────


class TestShock:
    def test_creation(self):
        s = Shock("unemployment_rate", 0.10)
        assert s.variable == "unemployment_rate"
        assert s.value == 0.10
        assert s.shock_type == "absolute"

    def test_relative_shock(self):
        s = Shock("gdp", 0.95, shock_type="relative")
        assert s.shock_type == "relative"

    def test_delta_shock(self):
        s = Shock("rate", -0.02, shock_type="delta")
        assert s.shock_type == "delta"


# ── Scenario ─────────────────────────────────────────────────


class TestScenario:
    def test_creation(self):
        s = Scenario(
            name="test",
            description="Test scenario",
            shocks=(Shock("x", 1.0),),
        )
        assert s.name == "test"
        assert len(s.shocks) == 1
        assert s.severity == "baseline"

    def test_to_dict(self):
        s = BASELINE_SCENARIO
        d = s.to_dict()
        assert d["name"] == "baseline"
        assert len(d["shocks"]) > 0
        assert "variable" in d["shocks"][0]

    def test_from_dict(self):
        data = {
            "name": "custom",
            "description": "Custom scenario",
            "shocks": [{"variable": "x", "value": 1.5}],
            "severity": "custom",
            "horizon_quarters": 8,
        }
        s = Scenario.from_dict(data)
        assert s.name == "custom"
        assert s.horizon_quarters == 8
        assert len(s.shocks) == 1

    def test_roundtrip(self):
        original = ADVERSE_SCENARIO
        d = original.to_dict()
        restored = Scenario.from_dict(d)
        assert restored.name == original.name
        assert len(restored.shocks) == len(original.shocks)

    def test_from_json(self):
        data = {
            "name": "file_test",
            "shocks": [{"variable": "a", "value": 0.5}],
        }
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(data, f)
            path = f.name

        s = Scenario.from_json(path)
        assert s.name == "file_test"


# ── PropagationStep ──────────────────────────────────────────


class TestPropagationStep:
    def test_creation(self):
        step = PropagationStep(
            variable="x",
            original_value=1.0,
            new_value=1.5,
            delta=0.5,
        )
        assert step.delta == 0.5
        assert step.depth == 0


# ── ScenarioSimulator ────────────────────────────────────────


class TestSimulatorBasic:
    def test_simulate_simple(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(
            name="test",
            description="",
            shocks=(Shock("a", 10.0),),
        )
        baseline = {"a": 5.0, "b": 3.0, "c": 1.0}
        result = sim.simulate(scenario, baseline)

        assert isinstance(result, SimulationResult)
        assert result.total_affected >= 1
        assert "a" in result.affected_variables
        assert result.affected_variables["a"] == 5.0  # 10 - 5

    def test_propagation_through_chain(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(
            name="chain",
            description="",
            shocks=(Shock("a", 10.0),),
        )
        baseline = {"a": 5.0, "b": 3.0, "c": 1.0}
        result = sim.simulate(scenario, baseline)

        # B should be affected (child of A)
        assert "b" in result.affected_variables
        # C should be affected (grandchild of A)
        assert "c" in result.affected_variables

    def test_max_depth(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="d", description="", shocks=(Shock("a", 10.0),))
        result = sim.simulate(scenario, {"a": 5.0, "b": 3.0, "c": 1.0})
        assert result.max_depth >= 1

    def test_no_baseline(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="nb", description="", shocks=(Shock("a", 10.0),))
        result = sim.simulate(scenario)
        assert result.total_affected >= 1

    def test_empty_shocks(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="empty", description="", shocks=())
        result = sim.simulate(scenario, {"a": 1.0})
        assert result.total_affected == 0
        assert result.impact_score == 0.0


class TestShockTypes:
    def test_absolute_shock(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="abs", description="", shocks=(Shock("a", 10.0, "absolute"),))
        result = sim.simulate(scenario, {"a": 5.0})
        assert result.affected_variables["a"] == 5.0

    def test_relative_shock(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="rel", description="", shocks=(Shock("a", 2.0, "relative"),))
        result = sim.simulate(scenario, {"a": 5.0})
        assert result.affected_variables["a"] == 5.0  # 2.0 * 5.0 - 5.0

    def test_delta_shock(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="del", description="", shocks=(Shock("a", 3.0, "delta"),))
        result = sim.simulate(scenario, {"a": 5.0})
        assert result.affected_variables["a"] == 3.0  # 5 + 3 - 5


class TestCreditDAG:
    def test_adverse_scenario(self):
        dag = _make_credit_dag()
        sim = ScenarioSimulator(dag)
        baseline = {
            "unemployment_rate": 0.04,
            "fed_funds_rate": 0.03,
            "gdp_growth": 0.02,
            "income": 50000.0,
            "dti": 0.35,
            "credit_score": 720.0,
            "default_flag": 0.0,
        }
        result = sim.simulate(ADVERSE_SCENARIO, baseline)
        assert result.total_affected > 3
        assert result.impact_score > 0

    def test_severe_scenario_higher_impact(self):
        dag = _make_credit_dag()
        baseline = {
            "unemployment_rate": 0.04,
            "fed_funds_rate": 0.03,
            "gdp_growth": 0.02,
            "income": 50000.0,
            "dti": 0.35,
            "credit_score": 720.0,
            "default_flag": 0.0,
        }
        sim_adv = ScenarioSimulator(dag)
        adv_result = sim_adv.simulate(ADVERSE_SCENARIO, baseline)
        sim_sev = ScenarioSimulator(dag)
        sev_result = sim_sev.simulate(SEVERE_SCENARIO, baseline)

        # Severe should have higher impact than adverse
        assert sev_result.impact_score >= adv_result.impact_score


class TestSimulationResult:
    def test_to_dict(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="td", description="", shocks=(Shock("a", 10.0),))
        result = sim.simulate(scenario, {"a": 5.0, "b": 3.0, "c": 1.0})
        d = result.to_dict()
        assert "scenario" in d
        assert "steps" in d
        assert "affected_variables" in d
        assert "impact_score" in d


class TestMonteCarlo:
    def test_basic_monte_carlo(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="mc", description="", shocks=(Shock("a", 10.0),))
        baseline = {"a": 5.0, "b": 3.0, "c": 1.0}
        mc = sim.monte_carlo(scenario, baseline, n_simulations=20)

        assert mc["n_simulations"] == 20
        assert mc["scenario"] == "mc"
        assert "variable_stats" in mc
        assert "a" in mc["variable_stats"]
        assert mc["impact_score_mean"] > 0

    def test_monte_carlo_confidence_interval(self):
        dag = _make_dag()
        sim = ScenarioSimulator(dag)
        scenario = Scenario(name="ci", description="", shocks=(Shock("a", 10.0),))
        mc = sim.monte_carlo(scenario, {"a": 5.0, "b": 3.0, "c": 1.0}, n_simulations=50)

        stats = mc["variable_stats"]["a"]
        assert stats["p5"] <= stats["mean_delta"]
        assert stats["p95"] >= stats["mean_delta"]


class TestBuiltInScenarios:
    def test_baseline_exists(self):
        assert "baseline" in BUILT_IN_SCENARIOS

    def test_adverse_exists(self):
        assert "adverse" in BUILT_IN_SCENARIOS

    def test_severely_adverse_exists(self):
        assert "severely_adverse" in BUILT_IN_SCENARIOS

    def test_all_have_shocks(self):
        for name, s in BUILT_IN_SCENARIOS.items():
            assert len(s.shocks) > 0, f"{name} has no shocks"


# ── Targeted coverage (line 192) ──────────────────────────


class TestSimulatorNegligibleDelta:
    """Cover line 192: skip variable when propagated delta < 1e-10."""

    def test_tiny_delta_skipped(self):
        from unittest.mock import patch

        dag = WorldDAG()
        dag.add_entity(Entity(entity_id="a", name="A"))
        dag.add_entity(Entity(entity_id="b", name="B"))
        dag.add_edge(CausalEdge(source="a", target="b", confidence=0.8))
        sim = ScenarioSimulator(dag, seed=42)
        scenario = Scenario(
            name="tiny",
            description="test",
            shocks=[Shock(variable="a", value=0.01, shock_type="delta")],
        )
        with patch.object(sim, "_compute_propagated_delta", return_value=0.0):
            result = sim.simulate(scenario, {"a": 1.0, "b": 2.0})
        # b should NOT appear in affected_variables since delta was 0
        assert "b" not in result.affected_variables
