"""Tests for the google_calendar skill."""

import json
import os
import time
from contextlib import contextmanager
from unittest.mock import patch

import httpx
import pytest

from fliiq.runtime.package_data import bundled_skills_dir
from fliiq.runtime.skills.base import SkillBase

SKILLS_DIR = str(bundled_skills_dir())

# Save real class before any patching
_RealAsyncClient = httpx.AsyncClient


def _load_skill() -> SkillBase:
    return SkillBase(os.path.join(SKILLS_DIR, "google_calendar"))


def _write_tokens(tmp_path, accounts=None):
    """Helper: write token file and return its path."""
    if accounts is None:
        accounts = {
            "test@gmail.com": {
                "access_token": "valid_token",
                "refresh_token": "refresh_tok",
                "expires_at": time.time() + 3600,
            }
        }
    token_file = tmp_path / "google_tokens.json"
    token_file.write_text(json.dumps(accounts))
    return token_file


def _mock_transport(responses: list[httpx.Response]):
    """Create a mock transport that returns responses in order."""
    idx = 0

    def handler(request):
        nonlocal idx
        if idx < len(responses):
            resp = responses[idx]
            idx += 1
            return resp
        return httpx.Response(500, json={"error": "No more mocked responses"})

    return httpx.MockTransport(handler)


@contextmanager
def _mock_gcal(tmp_path, transport, accounts=None):
    """Combined helper: patch TOKENS_PATH + httpx client on both gcal and google_auth modules."""
    import fliiq.runtime.google_auth as google_auth
    from fliiq.data.skills.core.google_calendar import main as gcal

    token_file = _write_tokens(tmp_path, accounts)

    def _make_client(**kw):
        kw["transport"] = transport
        return _RealAsyncClient(**kw)

    class MockHttpx:
        AsyncClient = staticmethod(_make_client)
        HTTPStatusError = httpx.HTTPStatusError

    with patch.object(google_auth, "TOKENS_PATH", token_file):
        # Patch httpx on both modules: google_auth (for token refresh) and gcal (for API calls)
        with patch.object(google_auth, "httpx", MockHttpx):
            with patch.object(gcal, "httpx", MockHttpx):
                yield token_file


# --- Schema ---


def test_schema():
    skill = _load_skill()
    schema = skill.schema()
    assert schema["name"] == "google_calendar"
    assert "action" in schema["parameters"]["properties"]
    assert schema["parameters"]["required"] == ["action"]
    actions = schema["parameters"]["properties"]["action"]["enum"]
    assert "list_calendars" in actions
    assert "list_events" in actions
    assert "create_event" in actions
    assert "update_event" in actions
    assert "delete_event" in actions


# --- Missing credentials ---


async def test_missing_token_file(tmp_path):
    """No token file -> clear error."""
    import fliiq.runtime.google_auth as google_auth
    from fliiq.data.skills.core.google_calendar import main as gcal

    with patch.object(google_auth, "TOKENS_PATH", tmp_path / "nonexistent.json"):
        with pytest.raises(ValueError, match="fliiq google auth"):
            await gcal.handler({"action": "list_events"})


async def test_account_not_found(tmp_path):
    """Requested account not in tokens -> clear error."""
    import fliiq.runtime.google_auth as google_auth
    from fliiq.data.skills.core.google_calendar import main as gcal

    token_file = _write_tokens(tmp_path)
    with patch.object(google_auth, "TOKENS_PATH", token_file):
        with pytest.raises(ValueError, match="No tokens for"):
            await gcal.handler({"action": "list_events", "calendar_account": "other@gmail.com"})


async def test_missing_client_creds_on_refresh(monkeypatch, tmp_path):
    """Expired token + no client creds -> clear error."""
    import fliiq.runtime.google_auth as google_auth
    from fliiq.data.skills.core.google_calendar import main as gcal

    monkeypatch.delenv("GOOGLE_CLIENT_ID", raising=False)
    monkeypatch.delenv("GOOGLE_CLIENT_SECRET", raising=False)

    expired = {
        "test@gmail.com": {
            "access_token": "expired",
            "refresh_token": "rt",
            "expires_at": 0,
        }
    }

    transport = _mock_transport([])

    with _mock_gcal(tmp_path, transport, expired) as tf:
        with pytest.raises(ValueError, match="GOOGLE_CLIENT_ID"):
            await gcal.handler({"action": "list_events"})


# --- Token refresh ---


async def test_token_auto_refresh(monkeypatch, tmp_path):
    """Expired token triggers refresh, updates file."""
    import fliiq.runtime.google_auth as google_auth
    from fliiq.data.skills.core.google_calendar import main as gcal

    monkeypatch.setenv("GOOGLE_CLIENT_ID", "cid")
    monkeypatch.setenv("GOOGLE_CLIENT_SECRET", "csec")

    expired = {
        "test@gmail.com": {
            "access_token": "old",
            "refresh_token": "rt",
            "expires_at": 0,
        }
    }

    transport = _mock_transport([
        httpx.Response(200, json={"access_token": "new_tok", "expires_in": 3600}),
        httpx.Response(200, json={"items": []}),
    ])

    with _mock_gcal(tmp_path, transport, expired) as token_file:
        result = await gcal.handler({"action": "list_events"})

    assert result["success"] is True
    updated = json.loads(token_file.read_text())
    assert updated["test@gmail.com"]["access_token"] == "new_tok"


# --- list_calendars ---


async def test_list_calendars(tmp_path):
    """list_calendars parses response correctly."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    api_response = {
        "items": [
            {
                "id": "primary",
                "summary": "Andy's Calendar",
                "accessRole": "owner",
                "primary": True,
            },
            {
                "id": "shared@group.calendar.google.com",
                "summary": "Team Calendar",
                "accessRole": "reader",
            },
        ]
    }
    transport = _mock_transport([httpx.Response(200, json=api_response)])

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({"action": "list_calendars"})

    assert result["success"] is True
    cals = result["data"]["calendars"]
    assert len(cals) == 2
    assert cals[0]["primary"] is True
    assert cals[1]["access_role"] == "reader"


# --- list_events ---


async def test_list_events(tmp_path):
    """list_events parses events with dateTime correctly."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    api_response = {
        "items": [
            {
                "id": "evt1",
                "summary": "Team Standup",
                "start": {"dateTime": "2026-02-14T09:00:00-05:00"},
                "end": {"dateTime": "2026-02-14T09:30:00-05:00"},
                "attendees": [{"email": "a@b.com"}, {"email": "c@d.com"}],
                "status": "confirmed",
                "htmlLink": "https://calendar.google.com/event?eid=evt1",
            }
        ]
    }
    transport = _mock_transport([httpx.Response(200, json=api_response)])

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({
            "action": "list_events",
            "time_min": "2026-02-14T00:00:00-05:00",
            "time_max": "2026-02-15T00:00:00-05:00",
        })

    assert result["success"] is True
    events = result["data"]["events"]
    assert len(events) == 1
    assert events[0]["summary"] == "Team Standup"
    assert events[0]["attendees"] == ["a@b.com", "c@d.com"]


async def test_list_events_all_day(tmp_path):
    """list_events handles all-day events (date instead of dateTime)."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    api_response = {
        "items": [
            {
                "id": "evt2",
                "summary": "Holiday",
                "start": {"date": "2026-02-14"},
                "end": {"date": "2026-02-15"},
            }
        ]
    }
    transport = _mock_transport([httpx.Response(200, json=api_response)])

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({"action": "list_events"})

    events = result["data"]["events"]
    assert events[0]["start"] == "2026-02-14"
    assert events[0]["end"] == "2026-02-15"


async def test_list_events_custom_calendar_id(tmp_path):
    """list_events uses custom calendar_id in URL."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    requested_url = None

    def capture(request):
        nonlocal requested_url
        requested_url = str(request.url)
        return httpx.Response(200, json={"items": []})

    transport = httpx.MockTransport(capture)

    with _mock_gcal(tmp_path, transport):
        await gcal.handler({
            "action": "list_events",
            "calendar_id": "shared@group.calendar.google.com",
        })

    assert "shared" in requested_url


# --- create_event ---


async def test_create_event(tmp_path):
    """create_event sends correct body with attendees."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    captured_body = None

    def capture(request):
        nonlocal captured_body
        if request.method == "POST":
            captured_body = json.loads(request.content)
        return httpx.Response(200, json={
            "id": "new_evt",
            "summary": "Project Sync",
            "htmlLink": "https://calendar.google.com/event?eid=new_evt",
            "start": {"dateTime": "2026-02-15T14:00:00-05:00"},
            "end": {"dateTime": "2026-02-15T14:30:00-05:00"},
        })

    transport = httpx.MockTransport(capture)

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({
            "action": "create_event",
            "summary": "Project Sync",
            "start": "2026-02-15T14:00:00-05:00",
            "end": "2026-02-15T14:30:00-05:00",
            "attendees": ["bob@example.com", "alice@example.com"],
            "description": "Discuss roadmap",
        })

    assert result["success"] is True
    assert result["data"]["id"] == "new_evt"
    assert captured_body["summary"] == "Project Sync"
    assert captured_body["attendees"] == [
        {"email": "bob@example.com"},
        {"email": "alice@example.com"},
    ]
    assert captured_body["description"] == "Discuss roadmap"


# --- update_event ---


async def test_update_event_partial(tmp_path):
    """update_event sends PATCH with only changed fields."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    captured_body = None
    captured_method = None

    def capture(request):
        nonlocal captured_body, captured_method
        captured_method = request.method
        if request.content:
            captured_body = json.loads(request.content)
        return httpx.Response(200, json={"id": "evt1", "summary": "Updated Title"})

    transport = httpx.MockTransport(capture)

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({
            "action": "update_event",
            "event_id": "evt1",
            "summary": "Updated Title",
        })

    assert result["success"] is True
    assert captured_method == "PATCH"
    assert captured_body == {"summary": "Updated Title"}


async def test_update_event_no_fields(tmp_path):
    """update_event with no updatable fields returns error."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    transport = _mock_transport([])

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({
            "action": "update_event",
            "event_id": "evt1",
        })

    assert result["success"] is False
    assert "No fields to update" in result["message"]


# --- delete_event ---


async def test_delete_event(tmp_path):
    """delete_event sends DELETE with sendUpdates."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    captured_method = None
    captured_url = None

    def capture(request):
        nonlocal captured_method, captured_url
        captured_method = request.method
        captured_url = str(request.url)
        return httpx.Response(204)

    transport = httpx.MockTransport(capture)

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({
            "action": "delete_event",
            "event_id": "evt_to_delete",
        })

    assert result["success"] is True
    assert captured_method == "DELETE"
    assert "evt_to_delete" in captured_url
    assert "sendUpdates=all" in captured_url


# --- Unknown action ---


async def test_unknown_action(tmp_path):
    """Unknown action returns error."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    transport = _mock_transport([])

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({"action": "invalid_action"})

    assert result["success"] is False
    assert "Unknown action" in result["message"]


# --- API error handling ---


async def test_api_error(tmp_path):
    """HTTP error returns success=False with detail."""
    from fliiq.data.skills.core.google_calendar import main as gcal

    transport = _mock_transport([
        httpx.Response(403, json={"error": {"message": "Calendar not found"}}),
    ])

    with _mock_gcal(tmp_path, transport):
        result = await gcal.handler({"action": "list_events"})

    assert result["success"] is False
    assert "403" in result["message"]
    assert "Calendar not found" in result["message"]


# --- Multi-account ---


async def test_multi_account_selection(tmp_path):
    """Selects correct account tokens when calendar_account specified."""
    import fliiq.runtime.google_auth as google_auth
    from fliiq.data.skills.core.google_calendar import main as gcal

    multi_accounts = {
        "personal@gmail.com": {
            "access_token": "personal_tok",
            "refresh_token": "rt1",
            "expires_at": time.time() + 3600,
        },
        "work@gmail.com": {
            "access_token": "work_tok",
            "refresh_token": "rt2",
            "expires_at": time.time() + 3600,
        },
    }

    captured_auth = None

    def capture(request):
        nonlocal captured_auth
        captured_auth = request.headers.get("authorization")
        return httpx.Response(200, json={"items": []})

    transport = httpx.MockTransport(capture)

    with _mock_gcal(tmp_path, transport, multi_accounts):
        await gcal.handler({
            "action": "list_events",
            "calendar_account": "work@gmail.com",
        })

    assert captured_auth == "Bearer work_tok"
