"""Tests for the Platoon MCP server and controllers.

Tests the documented requirements:
    1. read_file controller reads CSV and text files
    2. forecast controller runs forecasting on sample data
    3. optimize controller runs inventory optimization on sample data
    4. MCP server registers all three tools
    5. MCP tools call through to controllers correctly
"""

import asyncio
from pathlib import Path

import pytest

from platoon.mcp.controllers import (
    forecast, optimize, read_file,
    detect_anomalies, basket_analysis, cashflow, schedule,
)

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")


# ---------------------------------------------------------------------------
# Controller: read_file
# ---------------------------------------------------------------------------


class TestReadFile:
    """Tests for the read_file controller."""

    def test_read_csv(self):
        """Reads CSV and returns rows as list of dicts."""
        result = read_file(str(SAMPLES / "products.csv"))
        assert result["status"] == "ok"
        assert result["format"] == "csv"
        assert result["row_count"] == 80
        assert "product_id" in result["columns"]
        assert "price" in result["columns"]

    def test_read_csv_head(self):
        """head parameter limits rows returned."""
        result = read_file(str(SAMPLES / "products.csv"), head=5)
        assert result["status"] == "ok"
        assert result["row_count"] == 5

    def test_read_text_file(self):
        """Reads non-CSV files as plain text."""
        result = read_file(str(SAMPLES / "Readme.md"))
        assert result["status"] == "ok"
        assert result["format"] == "text"
        assert result["line_count"] > 0

    def test_read_missing_file(self):
        """Returns error for missing files."""
        result = read_file("/nonexistent/file.csv")
        assert result["status"] == "error"
        assert "not found" in result["error"].lower()


# ---------------------------------------------------------------------------
# Controller: forecast
# ---------------------------------------------------------------------------


class TestForecast:
    """Tests for the forecast controller."""

    def test_forecast_auto_product(self):
        """Forecasts the highest-volume product when no product_id given."""
        result = forecast(str(SAMPLES / "daily_demand.csv"))
        assert result["status"] == "ok"
        assert result["product_id"] is not None
        assert len(result["forecast"]) > 0
        assert result["predicted_sum"] > 0
        assert result["elapsed_ms"] > 0

    def test_forecast_specific_product(self):
        """Forecasts a specific product."""
        result = forecast(str(SAMPLES / "daily_demand.csv"), product_id="PROD-1001")
        assert result["status"] == "ok"
        assert result["product_id"] == "PROD-1001"
        assert len(result["forecast"]) == 30  # holdout default

    def test_forecast_with_method(self):
        """Runs with a specific method."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1001",
            method="moving_average",
            holdout=0,
            horizon=7,
        )
        assert result["status"] == "ok"
        assert "builtin:moving_average" in result["method"]
        assert len(result["forecast"]) == 7

    def test_forecast_with_mae(self):
        """Holdout produces MAE accuracy metric."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1001",
            holdout=14,
        )
        assert result["status"] == "ok"
        assert result["mae"] is not None
        assert result["mae"] >= 0

    def test_forecast_missing_file(self):
        """Returns error for missing file."""
        result = forecast("/nonexistent.csv")
        assert result["status"] == "error"

    def test_forecast_bad_product(self):
        """Returns error for unknown product_id."""
        result = forecast(str(SAMPLES / "daily_demand.csv"), product_id="NOPE")
        assert result["status"] == "error"
        assert "available" in result

    def test_forecast_intermittent_has_confidence_intervals(self):
        """Intermittent products get synthetic CIs even when model doesn't provide them."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1002",  # 70.7% zeros
            holdout=0,
            horizon=7,
        )
        assert result["status"] == "ok"
        assert result["zero_pct"] > 40
        assert len(result["lower_bound"]) == 7, "Expected synthetic CIs for intermittent demand"
        assert len(result["upper_bound"]) == 7

    def test_forecast_lower_bounds_non_negative(self):
        """Lower confidence bounds are clamped to 0 — demand can't be negative."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1002",
            method="moving_average",
            holdout=0,
            horizon=7,
        )
        assert result["status"] == "ok"
        assert all(v >= 0 for v in result["lower_bound"]), (
            f"Negative lower bounds: {result['lower_bound']}"
        )

    def test_forecast_auto_reports_actual_method(self):
        """Auto-selected method is reported (not just 'auto')."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1002",
            holdout=0,
            horizon=7,
        )
        assert result["status"] == "ok"
        # Should report the actual model, not "auto"
        assert "croston" in result["method"], (
            f"Expected croston for 70% zeros, got: {result['method']}"
        )

    def test_forecast_horizon_zero_returns_error(self):
        """horizon=0 with holdout=0 returns error instead of crashing."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1001",
            horizon=0,
            holdout=0,
        )
        assert result["status"] == "error"
        assert "horizon" in result["error"].lower()

    def test_forecast_returns_trend(self):
        """Forecast includes trend direction."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            holdout=0,
            horizon=14,
        )
        assert result["status"] == "ok"
        assert result["trend"] in ("up", "down", "stable")

    def test_forecast_detects_weekly_seasonality(self):
        """Weekly seasonal products have seasonality_detected=True."""
        result = forecast(
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1004",
            holdout=0,
            horizon=14,
        )
        assert result["status"] == "ok"
        assert result["seasonality_detected"] is True

    def test_forecast_short_series_warning(self):
        """Series with <7 points gets a warning."""
        import tempfile, os
        tf = tempfile.NamedTemporaryFile(suffix=".csv", delete=False, mode="w")
        tf.write("date,product_id,units_sold\n")
        for i in range(3):
            tf.write(f"2024-01-{i+1:02d},P1,{5+i}\n")
        tf.close()
        result = forecast(tf.name, product_id="P1", holdout=0, horizon=7)
        os.unlink(tf.name)
        assert result["status"] == "ok"
        assert "warning" in result
        assert "short" in result["warning"].lower()


# ---------------------------------------------------------------------------
# Controller: optimize
# ---------------------------------------------------------------------------


class TestOptimize:
    """Tests for the optimize controller."""

    def test_optimize_all_products(self):
        """Runs optimization across all products."""
        result = optimize(str(SAMPLES / "products.csv"))
        assert result["status"] == "ok"
        assert result["product_count"] == 80
        assert len(result["results"]) == 80
        for r in result["results"]:
            assert r["eoq"] > 0
            assert r["safety_stock"] >= 0

    def test_optimize_single_product(self):
        """Runs optimization for a single product."""
        result = optimize(str(SAMPLES / "products.csv"), product_id="PROD-1001")
        assert result["status"] == "ok"
        assert result["product_count"] == 1
        assert result["results"][0]["product_id"] == "PROD-1001"

    def test_optimize_with_orders(self):
        """Adds ABC classification when orders provided."""
        result = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
        )
        assert result["status"] == "ok"
        assert result["abc_summary"] is not None
        assert "A" in result["abc_summary"]

    def test_optimize_with_inventory(self):
        """Uses current stock from inventory file."""
        result = optimize(
            str(SAMPLES / "products.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        assert result["status"] == "ok"
        # Should have some alerts for at-risk products
        assert isinstance(result["alerts"], list)

    def test_optimize_full(self):
        """Full optimization with orders + inventory + alerts."""
        result = optimize(
            str(SAMPLES / "products.csv"),
            orders_path=str(SAMPLES / "orders.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        assert result["status"] == "ok"
        assert result["abc_summary"] is not None
        assert result["product_count"] == 80

        # Print summary
        print(f"\n  Products: {result['product_count']}")
        print(f"  ABC: {result['abc_summary']}")
        print(f"  Alerts: {len(result['alerts'])} products at risk")
        for a in result["alerts"][:3]:
            print(f"    {a['sku']}: {a['risk']:.0%} risk — {a['action']}")

    def test_optimize_invalid_service_level(self):
        """Invalid service_level returns error instead of crashing."""
        for sl in [0.0, 1.0, 1.5, -0.5]:
            result = optimize(str(SAMPLES / "products.csv"), product_id="PROD-1001", service_level=sl)
            assert result["status"] == "error", f"Expected error for service_level={sl}"
            assert "service_level" in result["error"]

    def test_optimize_results_include_price_and_cost(self):
        """Results include price, cost, daily_demand for agent revenue calculations."""
        result = optimize(str(SAMPLES / "products.csv"), product_id="PROD-1001")
        r = result["results"][0]
        assert "price" in r and r["price"] > 0
        assert "cost" in r and r["cost"] > 0
        assert "daily_demand" in r and r["daily_demand"] > 0
        assert "daily_revenue" in r and r["daily_revenue"] > 0
        assert "restock_cost" in r and r["restock_cost"] > 0

    def test_optimize_results_include_days_of_stock(self):
        """Results include days_of_stock for stockout timeline."""
        result = optimize(
            str(SAMPLES / "products.csv"),
            product_id="PROD-1001",
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        r = result["results"][0]
        assert "days_of_stock" in r
        # days_of_stock = current_stock / daily_demand
        if r["current_stock"] > 0 and r["daily_demand"] > 0:
            expected = round(r["current_stock"] / r["daily_demand"], 1)
            assert r["days_of_stock"] == expected

    def test_optimize_alerts_include_revenue_fields(self):
        """Alerts include daily_revenue and restock_cost for budget decisions."""
        result = optimize(
            str(SAMPLES / "products.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        if result["alerts"]:
            a = result["alerts"][0]
            assert "daily_revenue" in a
            assert "restock_cost" in a
            assert "days_of_stock" in a


# ---------------------------------------------------------------------------
# Controller: detect_anomalies
# ---------------------------------------------------------------------------


class TestDetectAnomalies:
    """Tests for the detect_anomalies controller."""

    def test_detect_auto_product(self):
        """Detects anomalies on highest-volume product when no product_id given."""
        result = detect_anomalies(str(SAMPLES / "daily_demand.csv"))
        assert result["status"] == "ok"
        assert result["product_id"] is not None
        assert result["total_points"] > 0
        assert result["anomaly_rate"] >= 0

    def test_detect_specific_product(self):
        """Detects anomalies for a specific product."""
        result = detect_anomalies(str(SAMPLES / "daily_demand.csv"), product_id="PROD-1001")
        assert result["status"] == "ok"
        assert result["product_id"] == "PROD-1001"

    def test_detect_iqr_method(self):
        """Works with IQR method."""
        result = detect_anomalies(str(SAMPLES / "daily_demand.csv"), product_id="PROD-1001", method="iqr")
        assert result["status"] == "ok"
        assert result["method"] == "iqr"

    def test_detect_missing_file(self):
        """Returns error for missing file."""
        result = detect_anomalies("/nonexistent.csv")
        assert result["status"] == "error"

    def test_detect_bad_product(self):
        """Returns error for unknown product_id."""
        result = detect_anomalies(str(SAMPLES / "daily_demand.csv"), product_id="NOPE")
        assert result["status"] == "error"

    def test_anomaly_fields(self):
        """Each anomaly has required fields."""
        result = detect_anomalies(str(SAMPLES / "daily_demand.csv"), product_id="PROD-1001")
        assert result["status"] == "ok"
        for a in result["anomalies"]:
            assert "date" in a
            assert "value" in a
            assert "expected" in a
            assert "z_score" in a
            assert "direction" in a
            assert "severity" in a


# ---------------------------------------------------------------------------
# Controller: basket_analysis
# ---------------------------------------------------------------------------


class TestBasketAnalysis:
    """Tests for the basket_analysis controller."""

    def test_basket_basic(self):
        """Runs basket analysis on orders."""
        result = basket_analysis(str(SAMPLES / "orders.csv"))
        assert result["status"] == "ok"
        assert result["total_orders"] > 0
        assert result["multi_item_orders"] > 0

    def test_basket_has_rules(self):
        """Finds at least some association rules."""
        result = basket_analysis(str(SAMPLES / "orders.csv"), min_support=0.001, min_confidence=0.01)
        assert result["status"] == "ok"
        assert result["rule_count"] > 0

    def test_basket_rule_fields(self):
        """Each rule has required fields."""
        result = basket_analysis(str(SAMPLES / "orders.csv"), min_support=0.001, min_confidence=0.01)
        for r in result["rules"]:
            assert "antecedent" in r
            assert "consequent" in r
            assert "support" in r
            assert "confidence" in r
            assert "lift" in r

    def test_basket_max_rules(self):
        """max_rules limits output."""
        result = basket_analysis(str(SAMPLES / "orders.csv"), min_support=0.001, min_confidence=0.01, max_rules=5)
        assert result["rule_count"] <= 5

    def test_basket_missing_file(self):
        """Returns error for missing file."""
        result = basket_analysis("/nonexistent.csv")
        assert result["status"] == "error"


# ---------------------------------------------------------------------------
# Controller: cashflow
# ---------------------------------------------------------------------------


class TestCashflow:
    """Tests for the cashflow controller."""

    def test_cashflow_basic(self):
        """Runs cash flow projection."""
        result = cashflow(str(SAMPLES / "products.csv"), str(SAMPLES / "daily_demand.csv"))
        assert result["status"] == "ok"
        assert result["horizon_days"] == 30
        assert len(result["periods"]) == 30
        assert result["summary"]["total_revenue"] > 0

    def test_cashflow_with_inventory(self):
        """Cash flow with inventory tracking and reorder simulation."""
        result = cashflow(
            str(SAMPLES / "products.csv"),
            str(SAMPLES / "daily_demand.csv"),
            inventory_path=str(SAMPLES / "inventory.csv"),
        )
        assert result["status"] == "ok"
        assert isinstance(result["reorder_events"], list)

    def test_cashflow_single_product(self):
        """Cash flow for a single product."""
        result = cashflow(
            str(SAMPLES / "products.csv"),
            str(SAMPLES / "daily_demand.csv"),
            product_id="PROD-1001",
        )
        assert result["status"] == "ok"
        assert result["summary"]["total_revenue"] > 0

    def test_cashflow_summary_fields(self):
        """Summary has all expected keys."""
        result = cashflow(str(SAMPLES / "products.csv"), str(SAMPLES / "daily_demand.csv"))
        s = result["summary"]
        assert "total_revenue" in s
        assert "total_cogs" in s
        assert "net_cash" in s
        assert "avg_daily_revenue" in s

    def test_cashflow_missing_file(self):
        """Returns error for missing file."""
        result = cashflow("/nonexistent.csv", str(SAMPLES / "daily_demand.csv"))
        assert result["status"] == "error"

    def test_cashflow_period_fields(self):
        """Each period has required fields."""
        result = cashflow(str(SAMPLES / "products.csv"), str(SAMPLES / "daily_demand.csv"), horizon=7)
        for p in result["periods"]:
            assert "date" in p
            assert "revenue" in p
            assert "cogs" in p
            assert "gross_profit" in p
            assert "net_cash" in p


# ---------------------------------------------------------------------------
# Controller: schedule
# ---------------------------------------------------------------------------


class TestSchedule:
    """Tests for the schedule controller."""

    def test_schedule_basic(self):
        """Runs scheduling from demand + staff data."""
        result = schedule(str(SAMPLES / "daily_demand.csv"), str(SAMPLES / "staff.csv"))
        assert result["status"] == "ok"
        assert result["total_cost"] > 0
        assert result["total_hours"] > 0
        assert result["assignment_count"] > 0

    def test_schedule_has_grid(self):
        """Schedule grid maps days to staff names."""
        result = schedule(str(SAMPLES / "daily_demand.csv"), str(SAMPLES / "staff.csv"))
        assert result["status"] == "ok"
        assert len(result["schedule_grid"]) > 0

    def test_schedule_missing_demand(self):
        """Returns error for missing demand file."""
        result = schedule("/nonexistent.csv", str(SAMPLES / "staff.csv"))
        assert result["status"] == "error"

    def test_schedule_missing_staff(self):
        """Returns error for missing staff file."""
        result = schedule(str(SAMPLES / "daily_demand.csv"), "/nonexistent.csv")
        assert result["status"] == "error"

    def test_schedule_assignment_fields(self):
        """Each assignment has required fields."""
        result = schedule(str(SAMPLES / "daily_demand.csv"), str(SAMPLES / "staff.csv"))
        for a in result["assignments"]:
            assert "staff_id" in a
            assert "staff_name" in a
            assert "day" in a
            assert "hours" in a
            assert "cost" in a

    def test_schedule_with_slots_path(self):
        """Skill-based scheduling via slots_path."""
        slots_file = SAMPLES / "tutor_slots.csv"
        if not slots_file.exists():
            pytest.skip("tutor_slots.csv not generated — run: uv run python data/samples/generate.py")
        result = schedule(
            str(SAMPLES / "daily_demand.csv"),
            str(SAMPLES / "staff.csv"),
            slots_path=str(slots_file),
        )
        assert result["status"] == "ok"
        assert result["mode"] == "skill_based"
        assert result["total_hours"] > 0
        assert result["assignment_count"] > 0

    def test_schedule_slots_missing_file(self):
        """Returns error for missing slots file."""
        result = schedule(
            str(SAMPLES / "daily_demand.csv"),
            str(SAMPLES / "staff.csv"),
            slots_path="/nonexistent.csv",
        )
        assert result["status"] == "error"


# ---------------------------------------------------------------------------
# MCP server tool registration
# ---------------------------------------------------------------------------


class TestMCPServer:
    """Tests that the MCP server registers tools correctly."""

    def test_server_creation(self):
        """Server creates without errors."""
        from platoon.mcp.server import create_mcp_server
        mcp = create_mcp_server()
        assert mcp.name == "platoon"

    def test_tools_registered(self):
        """All seven tools are registered."""
        from platoon.mcp.server import create_mcp_server
        mcp = create_mcp_server()
        tools = asyncio.get_event_loop().run_until_complete(mcp.list_tools())
        tool_names = {t.name for t in tools}
        assert "read_file" in tool_names
        assert "forecast" in tool_names
        assert "optimize" in tool_names
        assert "detect_anomalies" in tool_names
        assert "basket_analysis" in tool_names
        assert "cashflow" in tool_names
        assert "schedule" in tool_names

    def test_tool_call_read_file(self):
        """read_file tool calls through to controller."""
        from platoon.mcp.server import create_mcp_server
        mcp = create_mcp_server()

        result = asyncio.get_event_loop().run_until_complete(
            mcp.call_tool("read_file", {"path": str(SAMPLES / "products.csv"), "head": 3})
        )
        assert result.structured_content["status"] == "ok"
        assert result.structured_content["row_count"] == 3

    def test_tool_call_forecast(self):
        """forecast tool calls through to controller."""
        from platoon.mcp.server import create_mcp_server
        mcp = create_mcp_server()

        result = asyncio.get_event_loop().run_until_complete(
            mcp.call_tool("forecast", {
                "data_path": str(SAMPLES / "daily_demand.csv"),
                "product_id": "PROD-1001",
                "holdout": 0,
                "horizon": 7,
            })
        )
        assert result.structured_content["status"] == "ok"
        assert len(result.structured_content["forecast"]) == 7

    def test_tool_call_optimize(self):
        """optimize tool calls through to controller."""
        from platoon.mcp.server import create_mcp_server
        mcp = create_mcp_server()

        result = asyncio.get_event_loop().run_until_complete(
            mcp.call_tool("optimize", {
                "data_path": str(SAMPLES / "products.csv"),
                "product_id": "PROD-1001",
            })
        )
        assert result.structured_content["status"] == "ok"
        assert result.structured_content["product_count"] == 1


# ---------------------------------------------------------------------------
# Agent schema
# ---------------------------------------------------------------------------


class TestAgentSchema:
    """Tests that the commerce-analyst agent schema is well-formed."""

    def test_schema_loads(self):
        """Agent dict has all required Percolate schema fields."""
        from platoon.mcp.agent import COMMERCE_ANALYST

        assert COMMERCE_ANALYST["name"] == "commerce-analyst"
        assert COMMERCE_ANALYST["kind"] == "agent"
        assert COMMERCE_ANALYST["version"] is not None
        assert len(COMMERCE_ANALYST["content"]) > 500  # non-trivial prompt

    def test_tools_declared(self):
        """Agent declares all Platoon MCP tools."""
        from platoon.mcp.agent import COMMERCE_ANALYST

        tool_names = {t["name"] for t in COMMERCE_ANALYST["json_schema"]["tools"]}
        expected = {
            "get_file", "platoon_read_file", "platoon_forecast", "platoon_optimize",
            "platoon_detect_anomalies", "platoon_basket_analysis",
            "platoon_cashflow", "platoon_schedule",
        }
        assert tool_names == expected

    def test_tools_reference_mcp_server(self):
        """All tool references point to an MCP server."""
        from platoon.mcp.agent import COMMERCE_ANALYST

        for tool in COMMERCE_ANALYST["json_schema"]["tools"]:
            assert tool["server"] in ("platoon", "rem")
            assert tool["protocol"] == "mcp"

    def test_prompt_has_key_sections(self):
        """System prompt contains Tools, Theory, Examples, and Guidelines."""
        from platoon.mcp.agent import COMMERCE_ANALYST

        prompt = COMMERCE_ANALYST["content"]
        assert "## Tools" in prompt
        assert "## Theory" in prompt
        assert "## Examples" in prompt
        assert "## Response Guidelines" in prompt

    def test_prompt_has_tool_signatures(self):
        """System prompt documents how to call each tool with parameters."""
        from platoon.mcp.agent import COMMERCE_ANALYST

        prompt = COMMERCE_ANALYST["content"]
        assert "forecast(" in prompt
        assert "optimize(" in prompt
        assert "read_file(" in prompt

    def test_prompt_has_worked_examples(self):
        """System prompt includes at least 3 concrete worked examples."""
        from platoon.mcp.agent import COMMERCE_ANALYST

        prompt = COMMERCE_ANALYST["content"]
        assert prompt.count("### Example") >= 3
