"""Memorial slug generation — uniqueness and validity.

The public memorial URL is `{tenant}.indelis.com/memorial/<slug>`, so a slug
must always be non-empty, fit the column, and be unique within its tenant.
Uniqueness across tenants is deliberately NOT required: the subdomain already
scopes the URL.
"""
from unittest.mock import patch
from uuid import uuid4

import pytest
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.memorials.models.memorial import Memorial
from src.apps.memorials.schemas.requests import CreateMemorialRequest
from src.apps.memorials.services.memorial_service import MemorialService
from src.apps.records.models.record import Record
from src.apps.tenants.models.account import Account
from src.core.exceptions import ConflictError
from src.core.utils.text import slugify_memorial


# ─────────────────────────────────────────────────────────────────────────────
# slugify_memorial — pure
# ─────────────────────────────────────────────────────────────────────────────

@pytest.mark.parametrize(
    "name,expected",
    [
        ("Eileen Fitzgerald-D'Souza", "eileen-fitzgerald-d-souza"),
        ("中村 花子", "zhong-cun-hua-zi"),
        ("Мария Иванова", "mariia-ivanova"),
        ("Ann-Marie O'Brien", "ann-marie-o-brien"),
    ],
)
def test_names_transliterate_instead_of_vanishing(name, expected):
    """The previous ASCII-only regex deleted non-Latin characters outright,
    producing an empty slug and a broken /memorial/ URL."""
    assert slugify_memorial(name) == expected


@pytest.mark.parametrize("name", ["中村 花子", "Мария Иванова", "محمد علي"])
def test_non_latin_names_never_produce_an_empty_slug(name):
    assert slugify_memorial(name) != ""


def test_untransliterable_name_falls_back_to_the_record_name():
    assert slugify_memorial("...", fallback="Jane Doe") == "jane-doe"


def test_falls_back_to_a_random_token_when_nothing_is_usable():
    """An ugly URL beats a broken one. Only reachable when neither the display
    name nor the record name has a single transliterable character."""
    slug = slugify_memorial("...", fallback="???")
    assert slug.startswith("memorial-")
    assert len(slug) > len("memorial-")


def test_a_very_long_name_is_truncated_to_fit_the_column():
    """memorials.slug is VARCHAR(255); the old code never truncated."""
    slug = slugify_memorial("Wilhelmina " * 40)
    assert 0 < len(slug) <= 200
    assert not slug.endswith("-")


# ─────────────────────────────────────────────────────────────────────────────
# create_for_record — integration
# ─────────────────────────────────────────────────────────────────────────────

async def _make_account(db: AsyncSession) -> Account:
    uid = uuid4().hex[:8]
    acc = Account(
        organization_name=f"Slug Cemetery {uid}",
        subdomain=f"slug-{uid}",
        contact_email=f"admin-{uid}@slug.test",
        plan="professional",
        status="active",
    )
    db.add(acc)
    await db.flush()
    return acc


async def _make_record(db: AsyncSession, tenant_id, *, first="Jane", last="Doe") -> Record:
    rec = Record(tenant_id=tenant_id, first_name=first, last_name=last, status="active")
    db.add(rec)
    await db.flush()
    return rec


async def _create(db, record, tenant_id, display_name=None):
    return await MemorialService.create_for_record(
        db=db,
        record_id=record.id,
        tenant_id=tenant_id,
        data=CreateMemorialRequest(display_name=display_name, is_published=False),
        current_user=None,
        request=None,
    )


@pytest.mark.asyncio
async def test_duplicate_names_get_suffixed_slugs(db_session: AsyncSession):
    acc = await _make_account(db_session)
    first = await _make_record(db_session, acc.id)
    second = await _make_record(db_session, acc.id)

    m1 = await _create(db_session, first, acc.id, "John Smith")
    m2 = await _create(db_session, second, acc.id, "John Smith")

    assert m1.slug == "john-smith"
    assert m2.slug == "john-smith-2"


@pytest.mark.asyncio
async def test_two_tenants_may_share_the_same_slug(db_session: AsyncSession):
    """Uniqueness is per-tenant. A global unique index would be wrong — the
    subdomain already scopes the public URL."""
    acc_a = await _make_account(db_session)
    acc_b = await _make_account(db_session)
    rec_a = await _make_record(db_session, acc_a.id)
    rec_b = await _make_record(db_session, acc_b.id)

    m_a = await _create(db_session, rec_a, acc_a.id, "John Smith")
    m_b = await _create(db_session, rec_b, acc_b.id, "John Smith")

    assert m_a.slug == m_b.slug == "john-smith"


@pytest.mark.asyncio
async def test_a_non_latin_name_produces_a_usable_public_url(db_session: AsyncSession):
    acc = await _make_account(db_session)
    rec = await _make_record(db_session, acc.id, first="Hanako", last="Nakamura")

    memorial = await _create(db_session, rec, acc.id, "中村 花子")

    assert memorial.slug == "zhong-cun-hua-zi"


@pytest.mark.asyncio
async def test_losing_the_insert_race_retries_instead_of_failing(db_session: AsyncSession):
    """Simulates the check-then-insert race deterministically: the first INSERT
    raises the slug-constraint violation, as it would if a concurrent request
    had just taken that slug. The memorial must still be created."""
    acc = await _make_account(db_session)
    rec = await _make_record(db_session, acc.id)

    real_flush = AsyncSession.flush
    calls = {"n": 0}

    async def flaky_flush(self, *args, **kwargs):
        calls["n"] += 1
        if calls["n"] == 1:
            raise IntegrityError(
                "INSERT", {},
                Exception('duplicate key value violates unique constraint '
                          '"uq_memorials_tenant_slug"'),
            )
        return await real_flush(self, *args, **kwargs)

    with patch.object(AsyncSession, "flush", flaky_flush):
        memorial = await _create(db_session, rec, acc.id, "John Smith")

    assert memorial.id is not None
    # It took a different slug rather than 500ing.
    assert memorial.slug.startswith("john-smith")
    assert memorial.slug != "john-smith"


@pytest.mark.asyncio
async def test_a_different_constraint_violation_is_not_swallowed(db_session: AsyncSession):
    """The retry must only catch the slug constraint. A second memorial for the
    same record is a genuine conflict and must still be reported as one."""
    acc = await _make_account(db_session)
    rec = await _make_record(db_session, acc.id)

    await _create(db_session, rec, acc.id, "John Smith")

    with pytest.raises(ConflictError):
        await _create(db_session, rec, acc.id, "John Smith Again")


@pytest.mark.asyncio
async def test_a_long_record_name_still_produces_a_slug_that_fits(db_session: AsyncSession):
    """No display name, so the slug comes from the record — whose own columns
    are long enough to overflow the slug column without truncation."""
    acc = await _make_account(db_session)
    rec = await _make_record(db_session, acc.id, first="Wilhelmina" * 9, last="Fitzgerald" * 9)

    memorial = await _create(db_session, rec, acc.id)

    column_limit = Memorial.__table__.c.slug.type.length
    assert 0 < len(memorial.slug) <= column_limit


def test_an_over_long_display_name_is_rejected_by_the_schema():
    """display_name is VARCHAR(255) and previously had no cap, so an over-long
    name reached the database and surfaced as a DataError 500. It is now a
    clean 422 at the edge."""
    from pydantic import ValidationError

    with pytest.raises(ValidationError):
        CreateMemorialRequest(display_name="Wilhelmina " * 40, is_published=False)
