"""Integration tests for xprofile against a running Xapiand server.

Requires a Xapiand server running on localhost (default port).
"""
from __future__ import annotations

import cuuid

import pytest
import pytest_asyncio
from xapiand import NotFoundError

from xprofile import XProfile


@pytest_asyncio.fixture
async def profile():
    """Create a profile and clean it up after the test."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Test User",
        is_active=True,
        main_type="personal",
        contact_email="test@example.com",
        is_contact_email_active=True,
    )
    yield p
    try:
        await p.delete()
    except Exception:
        pass


# ---------------------------------------------------------------------------
# Basic CRUD tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_create_profile():
    """Test creating a profile and verifying returned fields."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Alice",
        is_active=True,
        main_type="personal",
        contact_email="alice@example.com",
    )
    try:
        assert p.name == "Alice"
        assert p.is_active is True
        assert p.main_type == "personal"
        assert p.contact_email == "alice@example.com"
        assert p._id is not None
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_get_profile(profile):
    """Test retrieving a profile by ID."""
    fetched = await XProfile.objects.get(id=profile._id, volatile=True)
    assert fetched._id == profile._id
    assert fetched.name == "Test User"
    assert fetched.is_active is True


@pytest.mark.asyncio
async def test_update_profile(profile):
    """Test updating a profile field and saving."""
    profile.name = "Updated User"
    profile.is_active = False
    await profile.save()

    fetched = await XProfile.objects.get(id=profile._id, volatile=True)
    assert fetched.name == "Updated User"
    assert fetched.is_active is False


@pytest.mark.asyncio
async def test_filter_profiles():
    """Test searching/filtering profiles."""
    unique = cuuid.uuid4().hex[:8]
    name = f"Searchable_{unique}"
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name=name,
        is_active=True,
        main_type="business",
    )
    try:
        results = await XProfile.objects.filter(
            query=f"name:{name}",
            limit=10,
        )
        assert results.total_count >= 1
        names = [r.name for r in results.results]
        assert name in names
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_delete_profile():
    """Test deleting a profile."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="To Delete",
        is_active=False,
        main_type="personal",
    )
    doc_id = p._id
    await p.delete()

    with pytest.raises(NotFoundError):
        await XProfile.objects.get(id=doc_id, volatile=True)


@pytest.mark.asyncio
async def test_default_values():
    """Test that schema defaults are applied correctly."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Defaults Test",
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.is_deleted is False
        assert fetched.is_active is False
        assert fetched.is_guest is False
        assert fetched.is_proxy is False
    finally:
        await p.delete()


# ---------------------------------------------------------------------------
# Schema field coverage tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.parametrize("main_type", [
    "personal", "business", "reseller", "referral",
    "supplier", "mashup", "affinity", "dssupplier",
])
async def test_profile_types(main_type: str):
    """Test creating a profile for each main_type choice."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name=f"Type_{main_type}",
        main_type=main_type,
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.main_type == main_type
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_null_fields():
    """Test that nullable fields accept None and are absent on retrieval."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Null Fields",
        slug=None,
        mashup=None,
        store_id=None,
        referral=None,
        campaign=None,
        click_id=None,
        contact_email=None,
        contact_phone=None,
        contact_country_code=None,
        username_email=None,
        username_phone=None,
        guest_prefix=None,
        public_guest_key=None,
        root_affinity_store_owner=None,
        heartbeat=None,
        created_at=None,
        updated_at=None,
        deleted_at=None,
        subscriptions=None,
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        # Xapiand omits null fields from the response, so they are absent
        assert getattr(fetched, 'slug', None) is None
        assert getattr(fetched, 'mashup', None) is None
        assert getattr(fetched, 'store_id', None) is None
        assert getattr(fetched, 'referral', None) is None
        assert getattr(fetched, 'campaign', None) is None
        assert getattr(fetched, 'click_id', None) is None
        assert getattr(fetched, 'contact_email', None) is None
        assert getattr(fetched, 'contact_phone', None) is None
        assert getattr(fetched, 'contact_country_code', None) is None
        assert getattr(fetched, 'username_email', None) is None
        assert getattr(fetched, 'username_phone', None) is None
        assert getattr(fetched, 'guest_prefix', None) is None
        assert getattr(fetched, 'public_guest_key', None) is None
        assert getattr(fetched, 'root_affinity_store_owner', None) is None
        assert getattr(fetched, 'heartbeat', None) is None
        assert getattr(fetched, 'created_at', None) is None
        assert getattr(fetched, 'updated_at', None) is None
        assert getattr(fetched, 'deleted_at', None) is None
        assert getattr(fetched, 'subscriptions', None) is None
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_uuid_fields():
    """Test that UUID fields round-trip correctly."""
    mashup_id = cuuid.uuid4().encode()
    store_id = cuuid.uuid4().encode()
    referral_id = cuuid.uuid4().encode()
    root_id = cuuid.uuid4().encode()

    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="UUID Fields",
        mashup=mashup_id,
        store_id=store_id,
        referral=referral_id,
        root_affinity_store_owner=root_id,
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.mashup == mashup_id
        assert fetched.store_id == store_id
        assert fetched.referral == referral_id
        assert fetched.root_affinity_store_owner == root_id
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_boolean_fields():
    """Test toggling all boolean fields on and off."""
    # All True
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Booleans True",
        is_active=True,
        is_guest=True,
        is_proxy=True,
        is_deleted=True,
        is_contact_email_active=True,
        is_contact_phone_active=True,
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.is_active is True
        assert fetched.is_guest is True
        assert fetched.is_proxy is True
        assert fetched.is_deleted is True
        assert fetched.is_contact_email_active is True
        assert fetched.is_contact_phone_active is True

        # Toggle all to False
        p.is_active = False
        p.is_guest = False
        p.is_proxy = False
        p.is_deleted = False
        p.is_contact_email_active = False
        p.is_contact_phone_active = False
        await p.save()

        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.is_active is False
        assert fetched.is_guest is False
        assert fetched.is_proxy is False
        assert fetched.is_deleted is False
        assert fetched.is_contact_email_active is False
        assert fetched.is_contact_phone_active is False
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_contact_fields():
    """Test all contact-related fields round-trip correctly."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Contact Fields",
        username_email="user@example.com",
        username_phone="+15551234567",
        contact_email="contact@example.com",
        contact_phone="+15559876543",
        contact_country_code="US",
        is_contact_email_active=True,
        is_contact_phone_active=True,
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.username_email == "user@example.com"
        assert fetched.username_phone == "+15551234567"
        assert fetched.contact_email == "contact@example.com"
        assert fetched.contact_phone == "+15559876543"
        assert fetched.contact_country_code == "US"
        assert fetched.is_contact_email_active is True
        assert fetched.is_contact_phone_active is True
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_date_fields():
    """Test date fields with ISO date strings."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Date Fields",
        created_at="2024-01-15T10:30:00Z",
        updated_at="2024-06-20T14:45:00Z",
        deleted_at="2025-01-01T00:00:00Z",
        heartbeat="2024-12-31T23:59:59Z",
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.created_at is not None
        assert fetched.updated_at is not None
        assert fetched.deleted_at is not None
        assert fetched.heartbeat is not None
        # Verify the date values contain the expected date portions
        assert "2024-01-15" in str(fetched.created_at)
        assert "2024-06-20" in str(fetched.updated_at)
        assert "2025-01-01" in str(fetched.deleted_at)
        assert "2024-12-31" in str(fetched.heartbeat)
    finally:
        await p.delete()


@pytest.mark.asyncio
@pytest.mark.xfail(reason="Xapiand maps json→object and cannot add new object fields to an existing schema")
async def test_json_fields():
    """Test JSON fields with complex data structures.

    Xapiand maps the 'json' schema type to 'object', which requires
    nested field type definitions.  Adding new object-type fields to an
    already-provisioned index triggers a 412 that cannot be resolved by
    schema retry.
    """
    subscriptions_data = {"plan": "premium", "features": ["analytics", "export"]}
    images_data = {"avatar": {"url": "https://example.com/avatar.png", "width": 200}}

    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="JSON Fields",
        subscriptions=subscriptions_data,
        images=images_data,
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.subscriptions == subscriptions_data
        assert fetched.images == images_data
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_guest_fields():
    """Test guest-related fields."""
    guest_key = cuuid.uuid4().hex
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Guest User",
        is_guest=True,
        guest_prefix="guest_",
        public_guest_key=guest_key,
    )
    try:
        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.is_guest is True
        assert fetched.guest_prefix == "guest_"
        assert fetched.public_guest_key == guest_key
    finally:
        await p.delete()


# ---------------------------------------------------------------------------
# Flow / lifecycle tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_full_lifecycle():
    """Test the full Create -> Read -> Update -> Search -> Delete lifecycle."""
    unique = cuuid.uuid4().hex[:8]
    name = f"Lifecycle_{unique}"
    doc_id = cuuid.uuid4().encode()

    # Create
    p = await XProfile.objects.create(
        id=doc_id,
        name=name,
        is_active=True,
        main_type="personal",
        contact_email=f"{unique}@example.com",
    )
    assert p._id == doc_id

    try:
        # Read
        fetched = await XProfile.objects.get(id=doc_id, volatile=True)
        assert fetched.name == name
        assert fetched.is_active is True

        # Update
        updated_name = f"Updated_{unique}"
        fetched.name = updated_name
        fetched.main_type = "business"
        await fetched.save()

        refetched = await XProfile.objects.get(id=doc_id, volatile=True)
        assert refetched.name == updated_name
        assert refetched.main_type == "business"

        # Search
        results = await XProfile.objects.filter(
            query=f"name:{updated_name}",
            limit=10,
        )
        assert results.total_count >= 1
        found_ids = [r._id for r in results.results]
        assert doc_id in found_ids
    finally:
        # Delete
        await p.delete()

    with pytest.raises(NotFoundError):
        await XProfile.objects.get(id=doc_id, volatile=True)


@pytest.mark.asyncio
async def test_create_with_explicit_id():
    """Test creating a profile with a specific UUID as id."""
    explicit_id = cuuid.uuid4().encode()
    p = await XProfile.objects.create(
        id=explicit_id,
        name="Explicit ID",
        is_active=True,
    )
    try:
        assert p._id == explicit_id
        fetched = await XProfile.objects.get(id=explicit_id, volatile=True)
        assert fetched._id == explicit_id
        assert fetched.name == "Explicit ID"
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_multiple_profiles_filter():
    """Test creating several profiles with different types and filtering by type."""
    unique = cuuid.uuid4().hex[:8]
    profiles = []
    try:
        for ptype in ("personal", "business", "reseller"):
            p = await XProfile.objects.create(
                id=cuuid.uuid4().encode(),
                name=f"Filter_{ptype}_{unique}",
                main_type=ptype,
                is_active=True,
            )
            profiles.append(p)

        results = await XProfile.objects.filter(
            query=f"main_type:business AND name:Filter_business_{unique}",
            limit=10,
        )
        assert results.total_count >= 1
        types = [r.main_type for r in results.results]
        assert "business" in types
        for r in results.results:
            if r.name == f"Filter_business_{unique}":
                assert r.main_type == "business"
    finally:
        for p in profiles:
            try:
                await p.delete()
            except Exception:
                pass


@pytest.mark.asyncio
async def test_update_preserves_unmodified_fields():
    """Test that updating one field preserves other fields."""
    p = await XProfile.objects.create(
        id=cuuid.uuid4().encode(),
        name="Preserve Test",
        main_type="personal",
        is_active=True,
        contact_email="preserve@example.com",
        slug="preserve-test",
    )
    try:
        # Update only the name
        p.name = "Preserve Updated"
        await p.save()

        fetched = await XProfile.objects.get(id=p._id, volatile=True)
        assert fetched.name == "Preserve Updated"
        # Unmodified fields should be preserved
        assert fetched.main_type == "personal"
        assert fetched.is_active is True
        assert fetched.contact_email == "preserve@example.com"
        assert fetched.slug == "preserve-test"
    finally:
        await p.delete()


@pytest.mark.asyncio
async def test_filter_pagination():
    """Test offset/limit pagination on filtered results."""
    unique = cuuid.uuid4().hex[:8]
    profiles = []
    try:
        for i in range(5):
            p = await XProfile.objects.create(
                id=cuuid.uuid4().encode(),
                name=f"Page_{unique}_{i:02d}",
                main_type="personal",
                is_active=True,
            )
            profiles.append(p)

        # Get first page (limit=2, check_at_least=10 to get accurate counts)
        page1 = await XProfile.objects.filter(
            query=f"name:Page_{unique}_*",
            limit=2,
            offset=0,
            check_at_least=10,
        )
        assert len(page1.results) == 2

        # Get second page
        page2 = await XProfile.objects.filter(
            query=f"name:Page_{unique}_*",
            limit=2,
            offset=2,
            check_at_least=10,
        )
        assert len(page2.results) == 2

        # Ensure pages don't overlap
        page1_ids = {r._id for r in page1.results}
        page2_ids = {r._id for r in page2.results}
        assert page1_ids.isdisjoint(page2_ids)

        # Estimated matches should reflect all 5
        assert page1.matches_estimated >= 5
    finally:
        for p in profiles:
            try:
                await p.delete()
            except Exception:
                pass


@pytest.mark.asyncio
async def test_save_idempotent():
    """Test that saving the same profile twice does not create duplicates."""
    doc_id = cuuid.uuid4().encode()
    p = await XProfile.objects.create(
        id=doc_id,
        name="Idempotent",
        is_active=True,
        main_type="personal",
    )
    try:
        assert p._id == doc_id

        # Save again without changes
        await p.save()
        assert p._id == doc_id

        # Save with a change
        p.name = "Idempotent Updated"
        await p.save()
        assert p._id == doc_id

        # Verify only one document with this id exists
        fetched = await XProfile.objects.get(id=doc_id, volatile=True)
        assert fetched.name == "Idempotent Updated"
    finally:
        await p.delete()


# ---------------------------------------------------------------------------
# Error handling tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_get_nonexistent_raises():
    """Test that getting a non-existent profile raises NotFoundError."""
    fake_id = cuuid.uuid4().encode()
    with pytest.raises(NotFoundError):
        await XProfile.objects.get(id=fake_id, volatile=True)


@pytest.mark.asyncio
async def test_delete_nonexistent_raises():
    """Test that deleting a non-existent profile raises NotFoundError."""
    fake_id = cuuid.uuid4().encode()
    p = XProfile(id=fake_id)
    with pytest.raises(NotFoundError):
        await p.delete()
