"""Tests for Tags plugin — CRUD, assignment, bulk assign, cascade delete, counts."""

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.service_container import get_container
from app.main import create_app
from app.plugins.builtins.tags import TagsPlugin
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 tags 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(TagsPlugin())
    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."""
    await seed_tenant_and_users(db_session)
    await login_client(plugin_client, "admin@tenanta.com")
    # Install + activate the tags plugin
    resp = await plugin_client.post("/api/v1/plugins/tags/install", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    resp = await plugin_client.post("/api/v1/plugins/tags/activate", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    return plugin_client


@pytest.mark.asyncio
async def test_list_tags_empty(authed_client: AsyncClient):
    """AC6: GET /api/v1/tags → 200 + tags with counts (empty)."""
    resp = await authed_client.get("/api/v1/tags", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert resp.json() == []


@pytest.mark.asyncio
async def test_create_tag(authed_client: AsyncClient):
    """AC7: POST /api/v1/tags → 201, tag created."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "Important", "color": "#FF0000"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 201
    data = resp.json()
    assert data["name"] == "Important"
    assert data["color"] == "#FF0000"
    assert data["entity_count"] == 0


@pytest.mark.asyncio
async def test_list_tags_with_counts(authed_client: AsyncClient):
    """AC6: GET /api/v1/tags → 200 + tags with counts."""
    # Create a tag
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "VIP", "color": "#00FF00"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 201
    resp.json()["id"]

    # List tags — should show entity_count=0
    resp = await authed_client.get("/api/v1/tags", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 1
    assert data[0]["name"] == "VIP"
    assert data[0]["entity_count"] == 0


@pytest.mark.asyncio
async def test_update_tag(authed_client: AsyncClient):
    """AC8: PATCH /api/v1/tags/{id} → 200."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "Old Name", "color": "#000000"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]

    resp = await authed_client.patch(
        f"/api/v1/tags/{tag_id}",
        json={"name": "New Name", "color": "#FFFFFF"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["name"] == "New Name"
    assert data["color"] == "#FFFFFF"


@pytest.mark.asyncio
async def test_delete_tag_cascade(authed_client: AsyncClient):
    """AC9: DELETE /api/v1/tags/{id} → 204, cascade delete assignments."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "ToDelete", "color": "#123456"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]

    # Assign tag to an entity
    entity_id = str(uuid.uuid4())
    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "contact", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200

    # Delete tag
    resp = await authed_client.delete(f"/api/v1/tags/{tag_id}", headers=ORIGIN_HEADER)
    assert resp.status_code == 204

    # Verify tag is gone
    resp = await authed_client.get("/api/v1/tags", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert len(resp.json()) == 0


@pytest.mark.asyncio
async def test_assign_tag(authed_client: AsyncClient):
    """AC10: POST /api/v1/tags/assign → 200, tag assigned to entity."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "AssignMe", "color": "#FF00FF"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    entity_id = str(uuid.uuid4())

    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "contact", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["tag_id"] == tag_id
    assert data["entity_type"] == "contact"
    assert data["entity_id"] == entity_id
    assert data["already_assigned"] is False

    # Verify count increased
    resp = await authed_client.get("/api/v1/tags", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    assert resp.json()[0]["entity_count"] == 1


@pytest.mark.asyncio
async def test_unassign_tag(authed_client: AsyncClient):
    """AC11: DELETE /api/v1/tags/assign → 204, tag removed."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "UnassignMe", "color": "#AAAAAA"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    entity_id = str(uuid.uuid4())

    # Assign first
    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "contact", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200

    # Unassign
    resp = await authed_client.request(
        "DELETE",
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "contact", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 204


@pytest.mark.asyncio
async def test_bulk_assign_tags(authed_client: AsyncClient):
    """AC12: POST /api/v1/tags/bulk-assign → 200, multiple tags assigned."""
    tag_ids = []
    for i in range(3):
        resp = await authed_client.post(
            "/api/v1/tags",
            json={"name": f"BulkTag{i}", "color": "#0000FF"},
            headers=ORIGIN_HEADER,
        )
        tag_ids.append(resp.json()["id"])

    entity_id = str(uuid.uuid4())
    resp = await authed_client.post(
        "/api/v1/tags/bulk-assign",
        json={"tag_ids": tag_ids, "entity_type": "file", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["total"] == 3
    assert len(data["assigned"]) == 3
    assert len(data["already_assigned"]) == 0


@pytest.mark.asyncio
async def test_list_tag_entities(authed_client: AsyncClient):
    """GET /api/v1/tags/{id}/entities → 200, list entities with this tag."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "EntitiesTag", "color": "#FF8800"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]

    entity_ids = [str(uuid.uuid4()) for _ in range(2)]
    for eid in entity_ids:
        await authed_client.post(
            "/api/v1/tags/assign",
            json={"tag_id": tag_id, "entity_type": "contact", "entity_id": eid},
            headers=ORIGIN_HEADER,
        )

    resp = await authed_client.get(f"/api/v1/tags/{tag_id}/entities", headers=ORIGIN_HEADER)
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 2


@pytest.mark.asyncio
async def test_create_tag_duplicate(authed_client: AsyncClient):
    """POST /api/v1/tags with existing name → 409."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "DupeTag", "color": "#111111"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 201
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "DupeTag", "color": "#222222"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 409


@pytest.mark.asyncio
async def test_update_tag_not_found(authed_client: AsyncClient):
    """PATCH /api/v1/tags/{nonexistent} → 404."""
    resp = await authed_client.patch(
        f"/api/v1/tags/{uuid.uuid4()}",
        json={"name": "NewName"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_update_tag_invalid_id(authed_client: AsyncClient):
    """PATCH /api/v1/tags/{invalid_uuid} → 400."""
    resp = await authed_client.patch(
        "/api/v1/tags/not-a-uuid",
        json={"name": "NewName"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_update_tag_duplicate_name(authed_client: AsyncClient):
    """PATCH /api/v1/tags/{id} with existing name → 409."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "TagA", "color": "#AAAAAA"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 201
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "TagB", "color": "#BBBBBB"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 201
    tag_b_id = resp.json()["id"]
    resp = await authed_client.patch(
        f"/api/v1/tags/{tag_b_id}",
        json={"name": "TagA"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 409


@pytest.mark.asyncio
async def test_update_tag_color_only(authed_client: AsyncClient):
    """PATCH /api/v1/tags/{id} with only color → 200."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "ColorTag", "color": "#CCCCCC"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    resp = await authed_client.patch(
        f"/api/v1/tags/{tag_id}",
        json={"color": "#DDDDDD"},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    assert resp.json()["color"] == "#DDDDDD"


@pytest.mark.asyncio
async def test_delete_tag_not_found(authed_client: AsyncClient):
    """DELETE /api/v1/tags/{nonexistent} → 404."""
    resp = await authed_client.delete(f"/api/v1/tags/{uuid.uuid4()}", headers=ORIGIN_HEADER)
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_delete_tag_invalid_id(authed_client: AsyncClient):
    """DELETE /api/v1/tags/{invalid} → 400."""
    resp = await authed_client.delete("/api/v1/tags/bad-uuid", headers=ORIGIN_HEADER)
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_assign_tag_invalid_entity_type(authed_client: AsyncClient):
    """POST /api/v1/tags/assign with invalid entity_type → 400."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "ETTag", "color": "#EEEEEE"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "invalid", "entity_id": str(uuid.uuid4())},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_assign_tag_not_found(authed_client: AsyncClient):
    """POST /api/v1/tags/assign with nonexistent tag → 404."""
    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={
            "tag_id": str(uuid.uuid4()),
            "entity_type": "contact",
            "entity_id": str(uuid.uuid4()),
        },
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_assign_tag_already_assigned(authed_client: AsyncClient):
    """POST /api/v1/tags/assign twice → already_assigned=True."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "DupAssign", "color": "#FFFFFF"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    entity_id = str(uuid.uuid4())
    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "contact", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    assert resp.json()["already_assigned"] is False
    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "contact", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    assert resp.json()["already_assigned"] is True


@pytest.mark.asyncio
async def test_assign_tag_invalid_ids(authed_client: AsyncClient):
    """POST /api/v1/tags/assign with invalid tag_id → 400."""
    resp = await authed_client.post(
        "/api/v1/tags/assign",
        json={"tag_id": "bad", "entity_type": "contact", "entity_id": str(uuid.uuid4())},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_unassign_tag_not_found(authed_client: AsyncClient):
    """DELETE /api/v1/tags/assign with nonexistent assignment → 404."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "Unassign404", "color": "#ABABAB"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    resp = await authed_client.request(
        "DELETE",
        "/api/v1/tags/assign",
        json={"tag_id": tag_id, "entity_type": "contact", "entity_id": str(uuid.uuid4())},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_bulk_assign_invalid_entity_type(authed_client: AsyncClient):
    """POST /api/v1/tags/bulk-assign with invalid entity_type → 400."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "BulkBad", "color": "#000001"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    resp = await authed_client.post(
        "/api/v1/tags/bulk-assign",
        json={"tag_ids": [tag_id], "entity_type": "invalid", "entity_id": str(uuid.uuid4())},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_bulk_assign_tag_not_found(authed_client: AsyncClient):
    """POST /api/v1/tags/bulk-assign with nonexistent tag → 404."""
    resp = await authed_client.post(
        "/api/v1/tags/bulk-assign",
        json={
            "tag_ids": [str(uuid.uuid4())],
            "entity_type": "file",
            "entity_id": str(uuid.uuid4()),
        },
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_bulk_assign_already_assigned(authed_client: AsyncClient):
    """POST /api/v1/tags/bulk-assign twice → already_assigned populated."""
    resp = await authed_client.post(
        "/api/v1/tags",
        json={"name": "BulkDup1", "color": "#001100"},
        headers=ORIGIN_HEADER,
    )
    tag_id = resp.json()["id"]
    entity_id = str(uuid.uuid4())
    resp = await authed_client.post(
        "/api/v1/tags/bulk-assign",
        json={"tag_ids": [tag_id], "entity_type": "file", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    resp = await authed_client.post(
        "/api/v1/tags/bulk-assign",
        json={"tag_ids": [tag_id], "entity_type": "file", "entity_id": entity_id},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert len(data["already_assigned"]) == 1
    assert len(data["assigned"]) == 0


@pytest.mark.asyncio
async def test_list_tag_entities_not_found(authed_client: AsyncClient):
    """GET /api/v1/tags/{nonexistent}/entities → 404."""
    resp = await authed_client.get(f"/api/v1/tags/{uuid.uuid4()}/entities", headers=ORIGIN_HEADER)
    assert resp.status_code == 404


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