"""check_availability/create_event/cancel_event call the Google Calendar
API directly -- freebusy.query for availability, events.insert for
booking (with the visitor as an attendee, which is what sends the calendar
invite), events.list + events.delete 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.google_calendar import (
    cancel_event, check_availability, create_event,
)

CONNECTION = {
    "access_token": "access-tok",
    "calendar_email": "shivraj@galaxiq.ai",
    "timezone": "Australia/Sydney",
}

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({
        "calendars": {"shivraj@galaxiq.ai": {"busy": []}}})
    with patch("httpx.post", return_value=response):
        slots = check_availability(CONNECTION, START, END)

    assert len(slots) > 0
    assert all(s["end"] - s["start"] == timedelta(minutes=30) for s in slots)


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({
        "calendars": {"shivraj@galaxiq.ai": {"busy": [
            {"start": busy_start.isoformat(), "end": busy_end.isoformat()}]}}})
    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_meet_link(monkeypatch):
    response = _json_response({
        "id": "evt123",
        "hangoutLink": "https://meet.google.com/abc-defg-hij",
    })
    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": "evt123",
                      "meeting_link": "https://meet.google.com/abc-defg-hij"}
    body = post.call_args.kwargs["json"]
    assert body["attendees"] == [{"email": "visitor@example.com"}]
    assert post.call_args.kwargs["params"]["sendUpdates"] == "all"
    assert post.call_args.kwargs["params"]["conferenceDataVersion"] == 1


def test_creating_an_event_with_no_meet_link_still_returns_the_event_id(monkeypatch):
    response = _json_response({"id": "evt123"})  # no hangoutLink
    with patch("httpx.post", return_value=response):
        result = create_event(CONNECTION, START, START + timedelta(minutes=30),
                              "visitor@example.com", "Jo Visitor")

    assert result == {"event_id": "evt123", "meeting_link": None}


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": [{"email": email}]}


def test_cancel_deletes_the_single_matching_event(monkeypatch):
    list_response = _json_response({"items": [_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({"items": []})
    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(
        {"items": [_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):
    # The `q` full-text search can surface an event that only mentions the
    # email in its text -- if the visitor isn't in `attendees`, it must not
    # be treated as a match.
    list_response = _json_response({"items": [
        {"id": "evt123", "attendees": [{"email": "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_narrows_by_near_time_when_given(monkeypatch):
    list_response = _json_response({"items": [_attendee_event("evt123")]})
    with patch("httpx.get", return_value=list_response) as get:
        cancel_event(CONNECTION, "visitor@example.com",
                     near_time=datetime(2026, 9, 1, 10, 0, tzinfo=timezone.utc))

    params = get.call_args.kwargs["params"]
    assert "timeMin" in params and "timeMax" in params


def test_check_availability_with_malformed_response_missing_busy_returns_empty_list(monkeypatch):
    """Test that check_availability returns [] when response is missing 'busy' key."""
    response = _json_response({
        "calendars": {"shivraj@galaxiq.ai": {}}})  # missing "busy" key
    with patch("httpx.post", return_value=response):
        slots = check_availability(CONNECTION, START, END)

    assert slots == []


def test_check_availability_with_malformed_busy_block_missing_timestamps_returns_empty_list(monkeypatch):
    """Test that check_availability returns [] when busy block is missing 'start' or 'end'."""
    response = _json_response({
        "calendars": {"shivraj@galaxiq.ai": {"busy": [
            {"start": "2026-09-01T10:00:00+00:00"}]}}})  # missing "end"
    with patch("httpx.post", return_value=response):
        slots = check_availability(CONNECTION, START, END)

    assert slots == []


def test_cancel_with_malformed_attendees_field_returns_false_not_a_crash(monkeypatch):
    """attendees is null on one candidate, and a non-dict entry on another --
    neither should raise; both should just fail to match."""
    list_response = _json_response({"items": [
        {"id": "evt123", "attendees": None},
        {"id": "evt456", "attendees": ["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_create_event_with_malformed_response_missing_id_returns_none(monkeypatch):
    """Test that create_event returns None when response is missing 'id' key."""
    response = _json_response({})  # missing "id"
    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
