"""check_availability/create_event/cancel_event call Microsoft Graph
directly -- getSchedule for availability, POST /me/events with
isOnlineMeeting for booking (this one call both books the calendar slot
and creates the Teams meeting), GET+DELETE /me/events for cancellation.
Fully isolated -- httpx is mocked in every test.
"""
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch

from app.services.integrations.teams import cancel_event, check_availability, create_event

CONNECTION = {
    "access_token": "access-tok",
    "user_email": "shivraj@galaxiq.ai",
    "timezone": "Pacific Standard Time",
}

START = datetime(2026, 9, 1, 9, 0, tzinfo=timezone.utc)
END = datetime(2026, 9, 1, 17, 0, tzinfo=timezone.utc)


def _json_response(payload, status_ok=True):
    response = MagicMock()
    if status_ok:
        response.raise_for_status.return_value = None
    else:
        response.raise_for_status.side_effect = Exception("http error")
    response.json.return_value = payload
    return response


def test_a_day_with_no_busy_blocks_is_fully_open(monkeypatch):
    response = _json_response({
        "value": [{"scheduleItems": []}]})
    with patch("httpx.post", return_value=response):
        slots = check_availability(CONNECTION, START, END)
    assert len(slots) > 0


def test_a_busy_block_removes_the_overlapping_slots(monkeypatch):
    busy_start = datetime(2026, 9, 1, 10, 0, tzinfo=timezone.utc)
    busy_end = datetime(2026, 9, 1, 11, 0, tzinfo=timezone.utc)
    response = _json_response({
        "value": [{"scheduleItems": [{
            "status": "busy",
            "start": {"dateTime": busy_start.strftime("%Y-%m-%dT%H:%M:%S"), "timeZone": "UTC"},
            "end": {"dateTime": busy_end.strftime("%Y-%m-%dT%H:%M:%S"), "timeZone": "UTC"},
        }]}]})
    with patch("httpx.post", return_value=response):
        slots = check_availability(CONNECTION, START, END)
    assert not any(s["start"] < busy_end and s["end"] > busy_start for s in slots)


def test_an_api_failure_returns_no_slots_not_a_crash(monkeypatch):
    response = _json_response({}, status_ok=False)
    with patch("httpx.post", return_value=response):
        assert check_availability(CONNECTION, START, END) == []


def test_creating_an_event_returns_its_id_and_teams_link(monkeypatch):
    response = _json_response({
        "id": "AAMk...evt123",
        "onlineMeeting": {"joinUrl": "https://teams.microsoft.com/l/meetup-join/abc"},
    })
    with patch("httpx.post", return_value=response) as post:
        result = create_event(CONNECTION, START, START + timedelta(minutes=30),
                              "visitor@example.com", "Jo Visitor")

    assert result == {"event_id": "AAMk...evt123",
                      "meeting_link": "https://teams.microsoft.com/l/meetup-join/abc"}
    body = post.call_args.kwargs["json"]
    assert body["isOnlineMeeting"] is True
    assert body["attendees"][0]["emailAddress"]["address"] == "visitor@example.com"


def test_event_creation_failure_returns_none(monkeypatch):
    response = _json_response({}, status_ok=False)
    with patch("httpx.post", return_value=response):
        result = create_event(CONNECTION, START, START + timedelta(minutes=30),
                              "visitor@example.com", "Jo Visitor")
    assert result is None


def _attendee_event(event_id, email="visitor@example.com"):
    return {"id": event_id,
           "attendees": [{"emailAddress": {"address": email, "name": "Jo Visitor"}}]}


def test_cancel_deletes_the_single_matching_event(monkeypatch):
    list_response = _json_response({"value": [_attendee_event("evt123")]})
    delete_response = MagicMock()
    delete_response.raise_for_status.return_value = None
    with patch("httpx.get", return_value=list_response), \
         patch("httpx.delete", return_value=delete_response) as delete:
        assert cancel_event(CONNECTION, "visitor@example.com") is True
    assert "evt123" in delete.call_args[0][0]


def test_cancel_with_no_matching_event_returns_false(monkeypatch):
    list_response = _json_response({"value": []})
    with patch("httpx.get", return_value=list_response), \
         patch("httpx.delete") as delete:
        assert cancel_event(CONNECTION, "visitor@example.com") is False
    delete.assert_not_called()


def test_cancel_with_more_than_one_matching_event_is_refused_not_guessed(monkeypatch):
    list_response = _json_response(
        {"value": [_attendee_event("evt123"), _attendee_event("evt456")]})
    with patch("httpx.get", return_value=list_response), \
         patch("httpx.delete") as delete:
        assert cancel_event(CONNECTION, "visitor@example.com") is False
    delete.assert_not_called()


def test_cancel_ignores_an_event_that_merely_mentions_the_email(monkeypatch):
    # An event returned by /me/events that does NOT actually have the visitor
    # as an attendee must not be treated as a match.
    list_response = _json_response({"value": [
        {"id": "evt123", "attendees": [{"emailAddress": {"address": "someone-else@example.com"}}]},
    ]})
    with patch("httpx.get", return_value=list_response), \
         patch("httpx.delete") as delete:
        assert cancel_event(CONNECTION, "visitor@example.com") is False
    delete.assert_not_called()


def test_cancel_with_malformed_attendees_field_returns_false_not_a_crash(monkeypatch):
    """attendees is null on one candidate, a non-dict entry on another, and a
    dict with an unexpected emailAddress shape on a third -- none should
    raise; all should just fail to match."""
    list_response = _json_response({"value": [
        {"id": "evt123", "attendees": None},
        {"id": "evt456", "attendees": ["not-a-dict"]},
        {"id": "evt789", "attendees": [{"emailAddress": "not-a-dict"}]},
    ]})
    with patch("httpx.get", return_value=list_response), \
         patch("httpx.delete") as delete:
        assert cancel_event(CONNECTION, "visitor@example.com") is False
    delete.assert_not_called()


def test_check_availability_with_malformed_scheduleitem_missing_start(monkeypatch):
    """Malformed schedule item (missing 'start' field) should return [] not crash."""
    response = _json_response({
        "value": [{"scheduleItems": [{
            "status": "busy",
            "end": {"dateTime": "2026-09-01T11:00:00", "timeZone": "UTC"},
        }]}]})
    with patch("httpx.post", return_value=response):
        assert check_availability(CONNECTION, START, END) == []


def test_check_availability_with_malformed_datetime_string(monkeypatch):
    """Malformed datetime string should return [] not crash."""
    response = _json_response({
        "value": [{"scheduleItems": [{
            "status": "busy",
            "start": {"dateTime": "not-a-datetime", "timeZone": "UTC"},
            "end": {"dateTime": "2026-09-01T11:00:00", "timeZone": "UTC"},
        }]}]})
    with patch("httpx.post", return_value=response):
        assert check_availability(CONNECTION, START, END) == []


def test_create_event_with_response_missing_id(monkeypatch):
    """Response missing 'id' field should return None not crash."""
    response = _json_response({
        "onlineMeeting": {"joinUrl": "https://teams.microsoft.com/l/meetup-join/abc"},
    })
    with patch("httpx.post", return_value=response):
        result = create_event(CONNECTION, START, START + timedelta(minutes=30),
                              "visitor@example.com", "Jo Visitor")
    assert result is None


def test_cancel_event_with_near_time_parameter(monkeypatch):
    """cancel_event with near_time should construct time window params."""
    near_time = datetime(2026, 9, 1, 12, 0, tzinfo=timezone.utc)
    list_response = _json_response({"value": [_attendee_event("evt123")]})
    delete_response = MagicMock()
    delete_response.raise_for_status.return_value = None
    with patch("httpx.get", return_value=list_response) as get_call, \
         patch("httpx.delete", return_value=delete_response):
        assert cancel_event(CONNECTION, "visitor@example.com", near_time=near_time) is True

    # Verify the params include the time window
    call_params = get_call.call_args.kwargs.get("params", {})
    assert "startDateTime" in call_params
    assert "endDateTime" in call_params
    # near_time - 2 hours = 10:00, near_time + 2 hours = 14:00
    assert "2026-09-01T10:00:00" in call_params["startDateTime"]
    assert "2026-09-01T14:00:00" in call_params["endDateTime"]


def test_cancel_narrows_by_near_time_when_given(monkeypatch):
    """near_time narrowing must call the calendarView endpoint (the only one
    that honours a startDateTime/endDateTime time window on Graph) rather
    than /me/events, which silently ignores those params."""
    list_response = _json_response({"value": [_attendee_event("evt123")]})
    with patch("httpx.get", return_value=list_response) as get_call:
        cancel_event(CONNECTION, "visitor@example.com",
                     near_time=datetime(2026, 9, 1, 10, 0, tzinfo=timezone.utc))

    url = get_call.call_args[0][0]
    assert url.endswith("/me/calendarView")
    params = get_call.call_args.kwargs["params"]
    assert "startDateTime" in params and "endDateTime" in params
