"""Tests for SyncManager — orchestrates external calendar sync (U6, G8).

Uses a mock ``AbstractSyncProvider`` to test SyncManager in isolation,
verifying provider iteration, last_sync updates, and conflict WS push.
"""

from __future__ import annotations

import asyncio
import json
import sys
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock

import pytest

# caldav is not installed — inject mock so SyncManager's import of
# CalDAVSyncProvider (which imports caldav) does not fail.
if "caldav" not in sys.modules:  # pragma: no cover
    sys.modules["caldav"] = MagicMock()

from agentkit.calendar.db import (
    init_calendar_db,
    insert_external_config,
    list_external_configs,
)
from agentkit.calendar.models import CalendarEvent, ExternalCalendarConfig
from agentkit.calendar.sync.base import AbstractSyncProvider
from agentkit.calendar.sync.manager import SyncManager

USER_ID = "user-1"


# ---------------------------------------------------------------------------
# Mock provider + helpers
# ---------------------------------------------------------------------------


class MockSyncProvider(AbstractSyncProvider):
    """In-memory mock provider that records calls for assertion."""

    def __init__(self) -> None:
        self.pull_calls: int = 0
        self.push_calls: int = 0
        self.pull_configs: list[ExternalCalendarConfig] = []
        self.push_configs: list[ExternalCalendarConfig] = []

    async def pull_changes(
        self, config: ExternalCalendarConfig, since: str | None = None
    ) -> list[CalendarEvent]:
        self.pull_calls += 1
        self.pull_configs.append(config)
        return []

    async def push_changes(
        self, config: ExternalCalendarConfig, events: list[CalendarEvent]
    ) -> list[CalendarEvent]:
        self.push_calls += 1
        self.push_configs.append(config)
        return events

    async def test_connection(self, config: ExternalCalendarConfig) -> tuple[bool, str]:
        return True, ""


def make_config(
    config_id: str = "config-1",
    user_id: str = USER_ID,
    provider: str = "caldav",
    last_sync: str | None = None,
) -> ExternalCalendarConfig:
    return ExternalCalendarConfig(
        id=config_id,
        user_id=user_id,
        provider=provider,
        credentials=json.dumps(
            {"url": "https://caldav.example.com", "username": "user", "password": "pass"}
        ),
        last_sync=last_sync,
    )


def make_event(
    event_id: str = "evt-1",
    title: str = "Test Event",
    last_modified: str = "2026-06-01T00:00:00+00:00",
    external_id: str | None = None,
) -> CalendarEvent:
    return CalendarEvent(
        id=event_id,
        user_id=USER_ID,
        title=title,
        start_time="2026-07-01T10:00:00+00:00",
        end_time="2026-07-01T11:00:00+00:00",
        external_id=external_id,
        external_provider="caldav" if external_id else None,
        last_modified=last_modified,
        created_at=last_modified,
    )


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def calendar_db_path(tmp_path: Path) -> Path:
    path = tmp_path / "test_calendar.db"
    asyncio.run(init_calendar_db(path))
    return path


# ---------------------------------------------------------------------------
# sync_all tests
# ---------------------------------------------------------------------------


async def test_sync_manager_sync_all_iterates_providers(
    calendar_db_path: Path,
) -> None:
    """2 configs → both providers called (G8)."""
    config1 = make_config(config_id="config-1")
    config2 = make_config(config_id="config-2")
    await insert_external_config(config1, calendar_db_path)
    await insert_external_config(config2, calendar_db_path)

    mock_provider = MockSyncProvider()
    manager = SyncManager(db_path=calendar_db_path, providers={"caldav": mock_provider})

    result = await manager.sync_all(USER_ID)

    assert result["synced"] == 2
    assert result["errors"] == []
    assert mock_provider.pull_calls == 2
    assert mock_provider.push_calls == 2


# ---------------------------------------------------------------------------
# sync_provider tests
# ---------------------------------------------------------------------------


async def test_sync_manager_sync_provider_updates_last_sync(
    calendar_db_path: Path,
) -> None:
    """Sync completes → config.last_sync updated (G8)."""
    config = make_config(config_id="config-1", last_sync=None)
    await insert_external_config(config, calendar_db_path)

    mock_provider = MockSyncProvider()
    manager = SyncManager(db_path=calendar_db_path, providers={"caldav": mock_provider})

    await manager.sync_provider("config-1")

    configs = await list_external_configs(USER_ID, calendar_db_path)
    assert len(configs) == 1
    assert configs[0].last_sync is not None


# ---------------------------------------------------------------------------
# resolve_conflict tests
# ---------------------------------------------------------------------------


async def test_sync_manager_resolve_conflict_notifies_user(
    calendar_db_path: Path,
) -> None:
    """Conflict → WS callback called with calendar_sync_conflict type (G8/G4)."""
    notifications: list[tuple[str, dict[str, Any]]] = []

    async def notify(event_type: str, payload: dict[str, Any]) -> None:
        notifications.append((event_type, payload))

    manager = SyncManager(db_path=calendar_db_path, notify_callback=notify)

    local = make_event(
        event_id="evt-1",
        title="Local Version",
        last_modified="2026-06-15T00:00:00+00:00",
        external_id="uid-1",
    )
    remote = make_event(
        event_id="evt-remote",
        title="Remote Version",
        last_modified="2026-06-01T00:00:00+00:00",
        external_id="uid-1",
    )

    winner = await manager.resolve_conflict(local, remote)

    assert len(notifications) == 1
    event_type, payload = notifications[0]
    assert event_type == "calendar_sync_conflict"
    assert payload["winner"] == "local"
    assert winner is local  # Local is newer → local wins
