"""End-to-end scenario tests for MCP tool chains.

Each test simulates a natural-language business question being decomposed
into one or more MCP tool calls.  We verify the tool outputs contain enough
signal for an agent to construct a useful answer.

Scenarios:
    1. Forecasting   — "How many Bluetooth Speakers will we sell next 2 weeks?"
    2. Optimization  — "Which products should I reorder right now?"
    3. Decision      — "Should I rush-order Webcams?"  (forecast + optimize)
    4. Classification — "Top sellers — trending up or down?"
    5. Allocation    — "$10K restock budget — where does it go?"
"""

from pathlib import Path

import pytest

from platoon.mcp.controllers import forecast, optimize, read_file

SAMPLES = Path(__file__).parent.parent / "data" / "samples"


@pytest.fixture(autouse=True)
def _check_samples():
    if not (SAMPLES / "products.csv").exists():
        pytest.skip("Sample data not generated — run: uv run python data/samples/generate.py")


# ---------------------------------------------------------------------------
# Case 1 — Forecasting: volume prediction with confidence intervals
# ---------------------------------------------------------------------------

class TestCase1_Forecasting:
    """'How many Bluetooth Speakers will we sell in the next two weeks?'

    NL → forecast(PROD-1004, horizon=14)
    Agent needs: predicted_sum, predicted_mean, lower/upper bounds, weekly pattern.
    """

    def test_forecast_returns_14_day_prediction(self):
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            horizon=14,
            holdout=0,
        )
        assert result["status"] == "ok"
        assert result["product_id"] == "PROD-1004"
        assert len(result["forecast"]) == 14

    def test_predicted_sum_is_reasonable(self):
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            horizon=14,
            holdout=0,
        )
        # Bluetooth Speaker avg ~8.95/day → 14d should be 100-500 range
        assert 50 < result["predicted_sum"] < 600

    def test_confidence_intervals_present(self):
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            horizon=14,
            holdout=0,
        )
        assert len(result["lower_bound"]) == 14
        assert len(result["upper_bound"]) == 14
        # Upper bound should exceed point forecast
        assert result["upper_bound"][0] > result["forecast"][0]

    def test_weekly_seasonality_visible(self):
        """Agent needs to detect mid-week peaks to narrate the pattern."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            horizon=14,
            holdout=0,
        )
        fc = result["forecast"]
        # There should be variation (not a flat line)
        assert max(fc) - min(fc) > 1.0, "Expected weekly seasonality in forecast"


# ---------------------------------------------------------------------------
# Case 2 — Optimization: stockout alerts across the portfolio
# ---------------------------------------------------------------------------

class TestCase2_Optimization:
    """'Which SKUs are most at risk of stocking out?'

    NL → optimize(all products, with orders + inventory)
    Agent needs: sorted alerts, ABC breakdown, reorder quantities.
    """

    def test_full_portfolio_optimization(self):
        result = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        assert result["status"] == "ok"
        assert result["product_count"] == 80

    def test_abc_classification_present(self):
        result = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        abc = result["abc_summary"]
        assert abc is not None
        assert set(abc.keys()) == {"A", "B", "C"}
        assert abc["A"] + abc["B"] + abc["C"] == 80

    def test_alerts_sorted_by_risk(self):
        result = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        alerts = result["alerts"]
        assert len(alerts) > 0
        # Verify descending risk order
        risks = [a["risk"] for a in alerts]
        assert risks == sorted(risks, reverse=True)

    def test_alerts_contain_actionable_fields(self):
        result = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        for alert in result["alerts"]:
            assert "sku" in alert
            assert "risk" in alert
            assert "current_stock" in alert
            assert "reorder_point" in alert
            assert "action" in alert
            assert alert["action"].startswith("Reorder")


# ---------------------------------------------------------------------------
# Case 3 — Decision support: rush-order go/no-go
# ---------------------------------------------------------------------------

class TestCase3_DecisionSupport:
    """'Should I rush-order Webcams or wait for the regular shipment?'

    NL → forecast(PROD-1008) + optimize(PROD-1008)
    Agent needs: days_until_stockout vs lead_time, revenue at risk.
    """

    def test_forecast_and_optimize_combine(self):
        """Both tools return ok for PROD-1008."""
        fc = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1008",
            horizon=15,
            holdout=0,
        )
        opt = optimize(
            str(SAMPLES / "products.csv"),
            product_id="PROD-1008",
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        assert fc["status"] == "ok"
        assert opt["status"] == "ok"

    def test_stockout_risk_is_critical(self):
        """Webcam should be at high stockout risk — validates the 'rush' decision."""
        opt = optimize(
            str(SAMPLES / "products.csv"),
            product_id="PROD-1008",
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        item = opt["results"][0]
        assert item["stockout_risk"] >= 0.9, "Expected high stockout risk for Webcam"
        assert item["current_stock"] < item["reorder_point"], "Stock should be below reorder point"

    def test_forecast_demand_exceeds_stock(self):
        """Predicted 15-day demand should exceed current stock → rush order justified."""
        fc = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1008",
            horizon=15,
            holdout=0,
        )
        opt = optimize(
            str(SAMPLES / "products.csv"),
            product_id="PROD-1008",
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        stock = opt["results"][0]["current_stock"]
        demand_15d = fc["predicted_sum"]
        assert demand_15d > stock, (
            f"15-day demand ({demand_15d:.0f}) should exceed stock ({stock}) "
            "to justify rush order"
        )

    def test_days_until_stockout_calculable(self):
        """Agent can compute days_until_stockout = stock / daily_demand."""
        fc = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1008",
            horizon=15,
            holdout=0,
        )
        opt = optimize(
            str(SAMPLES / "products.csv"),
            product_id="PROD-1008",
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        stock = opt["results"][0]["current_stock"]
        daily_avg = fc["predicted_mean"]
        days_until_stockout = stock / daily_avg if daily_avg > 0 else float("inf")

        assert days_until_stockout < 15, (
            f"Stockout in {days_until_stockout:.1f} days — before 15-day shipment"
        )


# ---------------------------------------------------------------------------
# Case 4 — Classification + trend: top sellers direction
# ---------------------------------------------------------------------------

class TestCase4_ClassificationTrend:
    """'What are our A-class products and are they trending up or down?'

    NL → optimize(for ABC) + forecast(per A-class product)
    Agent needs: ABC lookup + forecast direction per product.
    """

    def test_identify_a_class_products(self):
        result = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
        )
        a_class = [r for r in result["results"] if r["abc_class"] == "A"]
        assert len(a_class) > 10, "Should have meaningful A-class cohort"

    def test_forecast_a_class_product(self):
        """Can forecast individual A-class products for trend detection."""
        # PROD-1004 is known A-class
        fc = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            horizon=14,
            holdout=0,
        )
        assert fc["status"] == "ok"
        assert fc["predicted_mean"] > 0

    def test_holdout_accuracy_available(self):
        """MAE gives agent confidence in the trend reading."""
        fc = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            holdout=30,
        )
        assert fc["mae"] is not None
        assert fc["mae"] >= 0
        # MAE should be reasonable relative to mean
        assert fc["mae"] < fc["predicted_mean"] * 2


# ---------------------------------------------------------------------------
# Case 5 — Budget allocation: constrained restocking
# ---------------------------------------------------------------------------

class TestCase5_BudgetAllocation:
    """'$10K budget — which products to restock for best ROI?'

    NL → optimize(all) then agent ranks alerts by revenue-at-risk / restock-cost.
    Agent needs: alerts + product cost + daily demand to compute ROI.
    """

    def test_product_data_has_cost_and_demand(self):
        """read_file returns cost and demand needed for ROI calculation."""
        result = read_file(str(SAMPLES / "products.csv"), head=5)
        assert result["status"] == "ok"
        row = result["rows"][0]
        assert "cost" in row
        assert "price" in row
        assert "base_daily_demand" in row
        assert float(row["cost"]) > 0

    def test_alerts_matchable_to_products(self):
        """Alert product_ids can be joined to product catalog for cost lookup."""
        opt = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        products = read_file(str(SAMPLES / "products.csv"))
        product_ids = {r["product_id"] for r in products["rows"]}

        for alert in opt["alerts"]:
            assert alert["product_id"] in product_ids

    def test_budget_allocation_feasible(self):
        """At least some at-risk products fit within a $10K budget."""
        opt = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        products = read_file(str(SAMPLES / "products.csv"))
        cost_lookup = {r["product_id"]: float(r["cost"]) for r in products["rows"]}

        budget = 10_000
        allocated = 0
        products_funded = 0

        for alert in opt["alerts"]:
            pid = alert["product_id"]
            # Extract EOQ from the matching result
            eoq = next(
                r["eoq"] for r in opt["results"] if r["product_id"] == pid
            )
            restock_cost = eoq * cost_lookup.get(pid, 0)
            if allocated + restock_cost <= budget:
                allocated += restock_cost
                products_funded += 1

        assert products_funded >= 1, "Should fund at least 1 product with $10K"
        assert allocated <= budget

    def test_roi_ranking_produces_different_order_than_risk(self):
        """ROI (revenue_at_risk / cost) may differ from raw risk sort —
        proving the agent adds value beyond just reading the alerts list."""
        opt = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        products = read_file(str(SAMPLES / "products.csv"))
        catalog = {r["product_id"]: r for r in products["rows"]}

        roi_list = []
        for alert in opt["alerts"][:10]:
            pid = alert["product_id"]
            p = catalog.get(pid, {})
            cost = float(p.get("cost", 1))
            price = float(p.get("price", 0))
            demand = float(p.get("base_daily_demand", 0))
            lead_time = int(p.get("lead_time_days", 7))
            eoq = next(r["eoq"] for r in opt["results"] if r["product_id"] == pid)

            daily_revenue = demand * price
            restock_cost = eoq * cost
            roi = (daily_revenue * lead_time) / restock_cost if restock_cost > 0 else 0
            roi_list.append((pid, roi))

        # Sort by ROI
        roi_sorted = [pid for pid, _ in sorted(roi_list, key=lambda x: -x[1])]
        risk_sorted = [a["product_id"] for a in opt["alerts"][:10]]

        # ROI order and risk order should differ — otherwise the agent
        # is just parroting the alert list and budget allocation adds no value
        assert roi_sorted != risk_sorted, (
            "ROI ranking should differ from pure risk ranking — "
            "proves agent reasoning adds value"
        )
