"""End-to-end integration tests for the MCP-to-v9 bridge + schedule modulation.

Two test surfaces:
  1. Engine-direct: drives CognitiveEngine.process_exchange with synthesized
     UTC ISO timestamps to validate schedule routing without any wall-clock
     dependency. Portable, fast, doesn't touch the production stage.
  2. Bridge: monkeypatches harlo.clock.now_iso to validate the full MCP tool
     wrapper path (lazy init, _enrich, _v9_block, response merge).
"""

from __future__ import annotations

import datetime
import json
import sys
from pathlib import Path
from zoneinfo import ZoneInfo

import pytest

# python/ must be on sys.path so 'harlo' is importable for bridge tests
PROJECT_ROOT = Path(__file__).resolve().parents[2]
PYTHON_ROOT = PROJECT_ROOT / "python"
if str(PYTHON_ROOT) not in sys.path:
    sys.path.insert(0, str(PYTHON_ROOT))

from src.cognitive_engine import CognitiveEngine
from src.schedule import (
    DayWindow,
    Schedule,
    author_schedule_to_stage,
)
from src.schemas import ScheduleKind

NY = ZoneInfo("America/New_York")


def _ny(year, month, day, hour, minute) -> str:
    """Build a UTC ISO 8601 µs string from a NY-local datetime."""
    local = datetime.datetime(year, month, day, hour, minute, tzinfo=NY)
    return (
        local.astimezone(datetime.UTC)
        .isoformat(timespec="microseconds")
        .replace("+00:00", "Z")
    )


@pytest.fixture
def joe_schedule() -> Schedule:
    """The same shape as the bootstrapped production schedule."""
    return Schedule(
        timezone="America/New_York",
        work_hours={
            "monday":    [DayWindow(start=datetime.time(9, 0), end=datetime.time(18, 0))],
            "tuesday":   [DayWindow(start=datetime.time(9, 0), end=datetime.time(18, 0))],
            "wednesday": [DayWindow(start=datetime.time(9, 0), end=datetime.time(18, 0))],
            "thursday":  [DayWindow(start=datetime.time(9, 0), end=datetime.time(18, 0))],
            "friday":    [DayWindow(start=datetime.time(9, 0), end=datetime.time(15, 0))],
        },
        family_hours={
            "monday":    [DayWindow(start=datetime.time(18, 0), end=datetime.time(21, 0))],
            "tuesday":   [DayWindow(start=datetime.time(18, 0), end=datetime.time(21, 0))],
            "wednesday": [DayWindow(start=datetime.time(18, 0), end=datetime.time(21, 0))],
            "thursday":  [DayWindow(start=datetime.time(18, 0), end=datetime.time(21, 0))],
            "friday":    [DayWindow(start=datetime.time(18, 0), end=datetime.time(21, 0))],
            "saturday":  [DayWindow(all_day=True)],
            "sunday":    [DayWindow(all_day=True)],
        },
        overrides=(),
    )


@pytest.fixture
def engine(joe_schedule):
    """Fresh in-memory engine with the test schedule authored."""
    eng = CognitiveEngine(in_memory=True, prediction_enabled=False)
    author_schedule_to_stage(eng.stage.usd_stage, joe_schedule)
    yield eng
    eng.close()


# ════════════════════════════════════════════════════════════════════════
# Engine-direct E2E
# ════════════════════════════════════════════════════════════════════════

class TestFridayWorkThenSaturdayFamily:
    """The canonical demo arc: a Friday work session pivoting to Saturday family."""

    def test_friday_work_window_routes_normally(self, engine):
        # Fri 10:00 NY — solidly inside work hours
        result = engine.process_exchange(
            "twin_coach", {}, current_time_iso=_ny(2026, 5, 15, 10, 0),
        )
        last = engine._observations[-1]
        assert last.schedule.kind == ScheduleKind.WORK
        assert result["expert"] != "restorer"

    def test_friday_post_cutoff_drops_to_off_hours(self, engine):
        # Fri 16:00 NY — past the 15:00 work cutoff, before family at 18:00
        result = engine.process_exchange(
            "twin_coach", {}, current_time_iso=_ny(2026, 5, 15, 16, 0),
        )
        last = engine._observations[-1]
        assert last.schedule.kind == ScheduleKind.OFF_HOURS
        # Expert preserved; only context budget changes (verified in routing tests)

    def test_friday_evening_family_window(self, engine):
        # Fri 19:00 NY — inside the family window
        result = engine.process_exchange(
            "twin_coach", {}, current_time_iso=_ny(2026, 5, 15, 19, 0),
        )
        last = engine._observations[-1]
        assert last.schedule.kind == ScheduleKind.FAMILY
        assert result["expert"] == "restorer"

    def test_friday_late_night_off_hours(self, engine):
        # Fri 22:00 NY — past family end
        result = engine.process_exchange(
            "twin_coach", {}, current_time_iso=_ny(2026, 5, 15, 22, 0),
        )
        last = engine._observations[-1]
        assert last.schedule.kind == ScheduleKind.OFF_HOURS

    def test_saturday_all_day_family_forces_restorer(self, engine):
        # Sat 11:00 NY — Saturday is all_day family
        result = engine.process_exchange(
            "twin_coach", {}, current_time_iso=_ny(2026, 5, 16, 11, 0),
        )
        last = engine._observations[-1]
        assert last.schedule.kind == ScheduleKind.FAMILY
        assert result["expert"] == "restorer"

    def test_full_walkthrough_pivots_state_correctly(self, engine):
        """Walk through the demo arc and confirm each pivot lands as expected.

        This is the test that mirrors what the live demo will show: a few
        Friday work exchanges, the schedule pivot at 15:00 and 18:00, then
        a Saturday morning that's already family-locked.
        """
        timeline = [
            ("Fri 09:30 — start of work day",     _ny(2026, 5, 15,  9, 30), ScheduleKind.WORK),
            ("Fri 11:00 — mid-morning",           _ny(2026, 5, 15, 11,  0), ScheduleKind.WORK),
            ("Fri 14:00 — mid-afternoon",         _ny(2026, 5, 15, 14,  0), ScheduleKind.WORK),
            ("Fri 14:59 — last minute of work",   _ny(2026, 5, 15, 14, 59), ScheduleKind.WORK),
            ("Fri 15:00 — work cutoff",           _ny(2026, 5, 15, 15,  0), ScheduleKind.OFF_HOURS),
            ("Fri 17:30 — pre-family",            _ny(2026, 5, 15, 17, 30), ScheduleKind.OFF_HOURS),
            ("Fri 18:00 — family begins",         _ny(2026, 5, 15, 18,  0), ScheduleKind.FAMILY),
            ("Fri 20:30 — family",                _ny(2026, 5, 15, 20, 30), ScheduleKind.FAMILY),
            ("Fri 21:00 — family end",            _ny(2026, 5, 15, 21,  0), ScheduleKind.OFF_HOURS),
            ("Sat 08:00 — Saturday all-day",      _ny(2026, 5, 16,  8,  0), ScheduleKind.FAMILY),
            ("Sat 14:00 — Saturday afternoon",    _ny(2026, 5, 16, 14,  0), ScheduleKind.FAMILY),
            ("Sun 23:30 — late Sunday",           _ny(2026, 5, 17, 23, 30), ScheduleKind.FAMILY),
        ]
        for label, iso, expected_kind in timeline:
            engine.process_exchange("twin_coach", {}, current_time_iso=iso)
            actual = engine._observations[-1].schedule.kind
            assert actual == expected_kind, (
                f"{label}: expected {expected_kind.name}, got {actual.name}"
            )


# ════════════════════════════════════════════════════════════════════════
# Bridge wiring (mcp_server → v9)
# ════════════════════════════════════════════════════════════════════════

class TestBridgeWiring:
    """Validates the MCP server's lazy-singleton bridge into the v9 engine."""

    def test_lazy_singleton_initializes(self, monkeypatch):
        # Ensure src/ is on path for the in-bridge import
        monkeypatch.syspath_prepend(str(PROJECT_ROOT))
        import harlo.mcp_server as mcp

        # Reset singleton state for a clean test
        monkeypatch.setattr(mcp, "_engine", None, raising=False)

        eng = mcp._get_engine()
        assert eng is not None
        assert hasattr(eng, "process_exchange")

        # Idempotent
        assert mcp._get_engine() is eng

    @pytest.mark.skipif(
        not (PROJECT_ROOT / "models" / "cognitive_predictor_v1.joblib").exists(),
        reason="cognitive_predictor_v1.joblib not present — regen via "
               "`python src/train_predictor.py` then rerun. The FAMILY-hours "
               "routing under test depends on the predictor for delegate "
               "selection; without it, routing degrades to 'exploring'.",
    )
    def test_enrich_runs_full_exchange_with_clock_substitution(self, monkeypatch):
        monkeypatch.syspath_prepend(str(PROJECT_ROOT))
        import harlo.mcp_server as mcp
        monkeypatch.setattr(mcp, "_engine", None, raising=False)

        # Substitute the wall-clock to land in FAMILY hours (Sat 11:00 NY)
        sat_iso = _ny(2026, 5, 16, 11, 0)
        monkeypatch.setattr("harlo.clock.now_iso", lambda: sat_iso)

        eng = mcp._get_engine()
        idx_pre = eng.exchange_index
        result = mcp._enrich("twin_coach", {})
        assert result is not None
        assert result["exchange_index"] == idx_pre + 1
        assert result["expert"] == "restorer"  # FAMILY → restorer

    def test_v9_status_block_carries_schedule_kind(self, monkeypatch):
        monkeypatch.syspath_prepend(str(PROJECT_ROOT))
        import harlo.mcp_server as mcp
        monkeypatch.setattr(mcp, "_engine", None, raising=False)

        # Land in WORK — Monday 12:00 NY. Stable across schedule edits because
        # the production /schedule/ keeps Mon-Fri work_hours covering noon.
        monkeypatch.setattr("harlo.clock.now_iso", lambda: _ny(2026, 5, 11, 12, 0))
        eng = mcp._get_engine()
        enrichment = mcp._enrich("twin_coach", {})
        block = mcp._v9_status_block(enrichment)
        assert block["v9"]["schedule"]["kind"] == "WORK"

    def test_engine_failure_falls_back_to_v8_only(self, monkeypatch):
        monkeypatch.syspath_prepend(str(PROJECT_ROOT))
        import harlo.mcp_server as mcp

        # Force the failure sentinel
        monkeypatch.setattr(mcp, "_engine", False, raising=False)
        assert mcp._enrich("any_tool", {}) is None
        assert mcp._v9_block(None) == {}
        assert mcp._v9_status_block(None) == {}
