from __future__ import annotations

from collections.abc import AsyncIterator, Callable
from datetime import timedelta
from types import SimpleNamespace

import pytest
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.core.utils.time import utcnow
from app.db.models import Base, BridgeRingMember
from app.modules.proxy.ring_membership import (
    RING_HEARTBEAT_INTERVAL_SECONDS,
    RING_STALE_GRACE_SECONDS,
    RING_STALE_THRESHOLD_SECONDS,
    RingMembershipService,
    dynamic_bridge_ring_membership_enabled,
)

pytestmark = pytest.mark.unit


@pytest.fixture
async def async_session_factory() -> AsyncIterator[Callable[[], AsyncSession]]:
    """Create in-memory SQLite database for testing."""
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async_session_maker = async_sessionmaker(engine, expire_on_commit=False)

    def get_session() -> AsyncSession:
        return async_session_maker()

    yield get_session

    await engine.dispose()


@pytest.fixture
async def ring_service(async_session_factory: Callable[[], AsyncSession]) -> RingMembershipService:
    """Create RingMembershipService with test session factory."""
    return RingMembershipService(async_session_factory)


@pytest.mark.asyncio
async def test_register_and_list_active(ring_service: RingMembershipService) -> None:
    """Register 3 pods, list_active() returns all 3 in sorted order."""
    await ring_service.register("pod-c")
    await ring_service.register("pod-a")
    await ring_service.register("pod-b")

    active = await ring_service.list_active()
    assert active == ["pod-a", "pod-b", "pod-c"]


@pytest.mark.asyncio
async def test_unregister(ring_service: RingMembershipService) -> None:
    """Register then unregister, list_active() returns empty."""
    await ring_service.register("pod-1")
    assert await ring_service.list_active() == ["pod-1"]

    await ring_service.unregister("pod-1")
    assert await ring_service.list_active() == []


@pytest.mark.asyncio
async def test_stale_heartbeat_excluded(ring_service: RingMembershipService) -> None:
    """Register pod, set last_heartbeat_at to 200s ago, list_active(120) → empty."""
    await ring_service.register("pod-stale")

    # Manually update the heartbeat to be stale
    async with ring_service._session() as session:
        from sqlalchemy import update

        stale_time = utcnow() - timedelta(seconds=200)
        stmt = (
            update(BridgeRingMember)
            .where(BridgeRingMember.instance_id == "pod-stale")
            .values(last_heartbeat_at=stale_time)
        )
        await session.execute(stmt)
        await session.commit()

    # With 120s threshold, stale pod should be excluded
    active = await ring_service.list_active(stale_threshold_seconds=120)
    assert active == []

    # With 300s threshold, stale pod should be included
    active = await ring_service.list_active(stale_threshold_seconds=300)
    assert active == ["pod-stale"]


@pytest.mark.asyncio
async def test_mark_stale_keeps_member_visible_until_grace_window_elapses(
    ring_service: RingMembershipService,
) -> None:
    await ring_service.register("pod-grace")

    await ring_service.mark_stale(
        "pod-grace",
        stale_threshold_seconds=RING_STALE_THRESHOLD_SECONDS,
        grace_seconds=RING_STALE_GRACE_SECONDS,
    )

    assert await ring_service.list_active(stale_threshold_seconds=RING_STALE_THRESHOLD_SECONDS) == ["pod-grace"]
    assert await ring_service.list_active(stale_threshold_seconds=RING_HEARTBEAT_INTERVAL_SECONDS) == []


@pytest.mark.asyncio
async def test_ring_fingerprint_deterministic(ring_service: RingMembershipService) -> None:
    """Same members → same fingerprint."""
    await ring_service.register("pod-1")
    await ring_service.register("pod-2")
    await ring_service.register("pod-3")

    fp1 = await ring_service.ring_fingerprint()
    fp2 = await ring_service.ring_fingerprint()

    assert fp1 == fp2
    assert len(fp1) == 64  # SHA256 hex digest length


@pytest.mark.asyncio
async def test_ring_fingerprint_changes_on_membership_change(ring_service: RingMembershipService) -> None:
    """Different members → different fingerprint."""
    await ring_service.register("pod-1")
    await ring_service.register("pod-2")
    fp1 = await ring_service.ring_fingerprint()

    await ring_service.register("pod-3")
    fp2 = await ring_service.ring_fingerprint()

    assert fp1 != fp2


@pytest.mark.asyncio
async def test_heartbeat_updates_timestamp(ring_service: RingMembershipService) -> None:
    """Heartbeat updates last_heartbeat_at."""
    await ring_service.register("pod-hb")

    # Get initial heartbeat
    async with ring_service._session() as session:
        from sqlalchemy import select

        result = await session.execute(select(BridgeRingMember).where(BridgeRingMember.instance_id == "pod-hb"))
        member1 = result.scalar_one()
        initial_hb = member1.last_heartbeat_at

    # Wait a tiny bit and call heartbeat
    import asyncio

    await asyncio.sleep(0.01)
    await ring_service.heartbeat("pod-hb")

    # Get updated heartbeat
    async with ring_service._session() as session:
        from sqlalchemy import select

        result = await session.execute(select(BridgeRingMember).where(BridgeRingMember.instance_id == "pod-hb"))
        member2 = result.scalar_one()
        updated_hb = member2.last_heartbeat_at

    assert updated_hb > initial_hb


def test_dynamic_bridge_ring_membership_disabled_for_sqlite() -> None:
    settings = SimpleNamespace(
        http_responses_session_bridge_enabled=True,
        database_url="sqlite+aiosqlite:///./store.db",
        http_responses_session_bridge_instance_ring=[],
    )

    assert dynamic_bridge_ring_membership_enabled(settings) is False


def test_dynamic_bridge_ring_membership_disabled_for_static_ring() -> None:
    settings = SimpleNamespace(
        http_responses_session_bridge_enabled=True,
        database_url="postgresql+asyncpg://example/db",
        http_responses_session_bridge_instance_ring=["pod-a", "pod-b"],
    )

    assert dynamic_bridge_ring_membership_enabled(settings) is False


def test_dynamic_bridge_ring_membership_enabled_for_non_sqlite_without_static_ring() -> None:
    settings = SimpleNamespace(
        http_responses_session_bridge_enabled=True,
        database_url="postgresql+asyncpg://example/db",
        http_responses_session_bridge_instance_ring=[],
    )

    assert dynamic_bridge_ring_membership_enabled(settings) is True
