"""Tests for Entity Links plugin — link, unlink, reverse links, multi-links, event cleanup."""

from __future__ import annotations

import uuid

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession

from app.core.db import close_engine, reset_engine_for_testing
from app.core.event_bus import get_event_bus
from app.core.service_container import get_container
from app.main import create_app
from app.plugins.builtins.entity_links import EntityLinksPlugin
from app.plugins.registry import reset_registry_for_testing
from app.services.plugin_service import reset_plugin_service_for_testing
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users


@pytest_asyncio.fixture
async def plugin_app(engine: AsyncEngine, redis_client):
    """FastAPI app with entity_links plugin registered."""
    reset_engine_for_testing(engine)
    app = create_app()

    registry = reset_registry_for_testing()
    registry.initialize(engine, app)

    container = get_container()
    await container.initialize()

    registry.register_plugin(EntityLinksPlugin())
    reset_plugin_service_for_testing(registry)

    yield app
    await close_engine()


@pytest_asyncio.fixture
async def plugin_client(plugin_app) -> AsyncClient:
    transport = ASGITransport(app=plugin_app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        yield c


@pytest_asyncio.fixture
async def authed_client(plugin_client: AsyncClient, db_session: AsyncSession) -> AsyncClient:
    """Authenticated admin client with seeded data."""
    seed = await seed_tenant_and_users(db_session)
    await login_client(plugin_client, "admin@tenanta.com")
    resp = await plugin_client.post("/api/v1/plugins/entity_links/install", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    resp = await plugin_client.post("/api/v1/plugins/entity_links/activate", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    return plugin_client, seed


@pytest.mark.asyncio
async def test_link_file_to_company(authed_client: AsyncClient):
    """AC2: POST /api/v1/dms/files/{id}/link → 200, file linked to entity."""
    client, seed = authed_client
    file_id = str(uuid.uuid4())
    company_id = str(seed["company_a"].id)

    resp = await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": company_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["file_id"] == file_id
    assert data["entity_type"] == "contact"
    assert data["entity_id"] == company_id
    assert data["already_linked"] is False


@pytest.mark.asyncio
async def test_link_file_to_contact(authed_client: AsyncClient):
    """POST /api/v1/dms/files/{id}/link → 200, file linked to contact."""
    client, seed = authed_client
    file_id = str(uuid.uuid4())
    # Use a random UUID for contact (no contact seeded, but link is N:M metadata)
    contact_id = str(uuid.uuid4())

    resp = await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": contact_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["entity_type"] == "contact"
    assert data["entity_id"] == contact_id


@pytest.mark.asyncio
async def test_unlink_file_from_entity(authed_client: AsyncClient):
    """AC3: DELETE /api/v1/dms/files/{id}/link → 204, link removed."""
    client, seed = authed_client
    file_id = str(uuid.uuid4())
    company_id = str(seed["company_a"].id)

    # Link first
    resp = await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": company_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200

    # Unlink
    resp = await client.request(
        "DELETE",
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": company_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 204

    # Verify links list is empty
    resp = await client.get(f"/api/v1/dms/files/{file_id}/links", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert resp.json() == []


@pytest.mark.asyncio
async def test_list_file_links(authed_client: AsyncClient):
    """GET /api/v1/dms/files/{id}/links → 200, list all linked entities for file."""
    client, seed = authed_client
    file_id = str(uuid.uuid4())
    company_id = str(seed["company_a"].id)
    contact_id = str(uuid.uuid4())

    # Link to company
    await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": company_id},
        headers=ORIGIN_HEADER,
    )
    # Link to contact
    await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": contact_id},
        headers=ORIGIN_HEADER,
    )

    resp = await client.get(f"/api/v1/dms/files/{file_id}/links", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 2


@pytest.mark.asyncio
async def test_multi_links_one_file_many_entities(authed_client: AsyncClient):
    """Multi-links: one file → many entities."""
    client, seed = authed_client
    file_id = str(uuid.uuid4())

    # Link to 3 different companies
    for _ in range(3):
        entity_id = str(uuid.uuid4())
        resp = await client.post(
            f"/api/v1/dms/files/{file_id}/link",
            json={"entity_type": "contact", "entity_id": entity_id},
            headers=ORIGIN_HEADER,
        )
        assert resp.status_code == 200

    resp = await client.get(f"/api/v1/dms/files/{file_id}/links", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert len(resp.json()) == 3


@pytest.mark.asyncio
async def test_reverse_link_company_files(authed_client: AsyncClient):
    """GET /api/v1/companies/{id}/files → 200, list linked files for company."""
    client, seed = authed_client
    company_id = str(seed["company_a"].id)

    # Link 2 files to the company
    for _ in range(2):
        file_id = str(uuid.uuid4())
        await client.post(
            f"/api/v1/dms/files/{file_id}/link",
            json={"entity_type": "contact", "entity_id": company_id},
            headers=ORIGIN_HEADER,
        )

    resp = await client.get(f"/api/v1/companies/{company_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 2
    assert all(link["entity_type"] == "company" for link in data)


@pytest.mark.asyncio
async def test_reverse_link_contact_files(authed_client: AsyncClient):
    """GET /api/v1/contacts/{id}/files → 200, list linked files for contact."""
    client, seed = authed_client
    contact_id = str(uuid.uuid4())

    # Link 1 file to the contact
    file_id = str(uuid.uuid4())
    await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": contact_id},
        headers=ORIGIN_HEADER,
    )

    resp = await client.get(f"/api/v1/contacts/{contact_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 1
    assert data[0]["entity_type"] == "contact"


@pytest.mark.asyncio
async def test_event_cleanup_on_company_deleted(authed_client: AsyncClient):
    """AC13: DMS plugin listens to company.deleted event → linked files cleanup."""
    client, seed = authed_client
    company_id = seed["company_a"].id
    tenant_id = seed["tenant_a"].id

    # Link a file to the company
    file_id = str(uuid.uuid4())
    resp = await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": str(company_id)},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200

    # Verify link exists
    resp = await client.get(f"/api/v1/companies/{company_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert len(resp.json()) == 1

    # Publish company.deleted event
    event_bus = get_event_bus()
    await event_bus.publish(
        "company.deleted",
        {
            "entity_id": str(company_id),
            "company_id": str(company_id),
            "tenant_id": str(tenant_id),
        },
    )

    # Allow async event handler to complete
    import asyncio

    await asyncio.sleep(0.1)

    # Verify link is cleaned up
    resp = await client.get(f"/api/v1/companies/{company_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert resp.json() == []


@pytest.mark.asyncio
async def test_event_cleanup_on_contact_deleted(authed_client: AsyncClient):
    """Event cleanup on contact.deleted → linked files removed."""
    client, seed = authed_client
    contact_id = uuid.uuid4()
    tenant_id = seed["tenant_a"].id

    # Link a file to the contact
    file_id = str(uuid.uuid4())
    resp = await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": str(contact_id)},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200

    # Verify link exists
    resp = await client.get(f"/api/v1/contacts/{contact_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert len(resp.json()) == 1

    # Publish contact.deleted event
    event_bus = get_event_bus()
    await event_bus.publish(
        "contact.deleted",
        {
            "entity_id": str(contact_id),
            "contact_id": str(contact_id),
            "tenant_id": str(tenant_id),
        },
    )

    # Allow async event handler to complete
    import asyncio

    await asyncio.sleep(0.1)

    # Verify link is cleaned up
    resp = await client.get(f"/api/v1/contacts/{contact_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert resp.json() == []


@pytest.mark.asyncio
async def test_link_invalid_file_id(authed_client: AsyncClient):
    """POST /api/v1/dms/files/{invalid}/link → 400."""
    client, seed = authed_client
    resp = await client.post(
        "/api/v1/dms/files/bad-uuid/link",
        json={"entity_type": "contact", "entity_id": str(uuid.uuid4())},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_link_invalid_entity_id(authed_client: AsyncClient):
    """POST /api/v1/dms/files/{id}/link with invalid entity_id → 400."""
    client, seed = authed_client
    resp = await client.post(
        f"/api/v1/dms/files/{uuid.uuid4()}/link",
        json={"entity_type": "contact", "entity_id": "bad-uuid"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_link_invalid_entity_type(authed_client: AsyncClient):
    """POST /api/v1/dms/files/{id}/link with invalid entity_type → 400."""
    client, seed = authed_client
    resp = await client.post(
        f"/api/v1/dms/files/{uuid.uuid4()}/link",
        json={"entity_type": "invalid", "entity_id": str(uuid.uuid4())},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_link_already_linked(authed_client: AsyncClient):
    """POST /api/v1/dms/files/{id}/link twice → already_linked=True."""
    client, seed = authed_client
    file_id = str(uuid.uuid4())
    company_id = str(seed["company_a"].id)
    resp = await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": company_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    assert resp.json()["already_linked"] is False
    resp = await client.post(
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": company_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    assert resp.json()["already_linked"] is True


@pytest.mark.asyncio
async def test_unlink_not_found(authed_client: AsyncClient):
    """DELETE /api/v1/dms/files/{id}/link with nonexistent link → 404."""
    client, seed = authed_client
    file_id = str(uuid.uuid4())
    company_id = str(seed["company_a"].id)
    resp = await client.request(
        "DELETE",
        f"/api/v1/dms/files/{file_id}/link",
        json={"entity_type": "contact", "entity_id": company_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_unlink_invalid_file_id(authed_client: AsyncClient):
    """DELETE /api/v1/dms/files/{invalid}/link → 400."""
    client, seed = authed_client
    resp = await client.request(
        "DELETE",
        "/api/v1/dms/files/bad-uuid/link",
        json={"entity_type": "contact", "entity_id": str(uuid.uuid4())},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_list_file_links_invalid_id(authed_client: AsyncClient):
    """GET /api/v1/dms/files/{invalid}/links → 400."""
    client, seed = authed_client
    resp = await client.get("/api/v1/dms/files/bad-uuid/links", headers=ORIGIN_HEADER)
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_list_company_files_invalid_id(authed_client: AsyncClient):
    """GET /api/v1/companies/{invalid}/files → 400."""
    client, seed = authed_client
    resp = await client.get("/api/v1/companies/bad-uuid/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_list_contact_files_invalid_id(authed_client: AsyncClient):
    """GET /api/v1/contacts/{invalid}/files → 400."""
    client, seed = authed_client
    resp = await client.get("/api/v1/contacts/bad-uuid/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_list_company_files_empty(authed_client: AsyncClient):
    """GET /api/v1/companies/{id}/files with no links → 200 + empty list."""
    client, seed = authed_client
    company_id = str(seed["company_a"].id)
    resp = await client.get(f"/api/v1/companies/{company_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert resp.json() == []


@pytest.mark.asyncio
async def test_list_contact_files_empty(authed_client: AsyncClient):
    """GET /api/v1/contacts/{id}/files with no links → 200 + empty list."""
    client, seed = authed_client
    contact_id = str(uuid.uuid4())
    resp = await client.get(f"/api/v1/contacts/{contact_id}/files", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert resp.json() == []
