"""Tests for Phase 11.A foundation primitives and 11.B platform_core models."""

from __future__ import annotations

from io import BytesIO

import pytest

from simorgh.apps.platform_core import services as pc
from simorgh.apps.platform_core.models import (
    Attachment,
    CustomFieldType,
)
from simorgh.apps.storage.services import store_file
from simorgh.apps.workspaces.models import Workspace
from simorgh.apps.workspaces.services import create_workspace
from simorgh.core.context import RequestContext, use_request_context
from simorgh.core.exceptions import (
    ConcurrentUpdate,
    ContractError,
    PlatformError,
    ScopeViolation,
)
from simorgh.core.locks import with_row_lock

# ---------------------------------------------------------------------------
# 11.A — Foundation primitives
# ---------------------------------------------------------------------------


def test_exceptions_hierarchy():
    assert issubclass(ConcurrentUpdate, PlatformError)
    assert issubclass(ScopeViolation, PlatformError)
    assert issubclass(ContractError, PlatformError)


@pytest.mark.django_db
def test_with_row_lock_returns_queryset_on_sqlite(tenant_acme):
    # On SQLite select_for_update is a no-op but the queryset must still
    # evaluate correctly.
    qs = with_row_lock(Workspace.objects.filter(tenant=tenant_acme))
    assert list(qs) == []


@pytest.mark.django_db
def test_versioned_model_optimistic_lock(tenant_acme, acme_tree):
    """VersionedModel is exercised once PlatformEntityModel composes it (Phase 11.J)."""
    # Smoke-import only; behaviour is tested in test_phase11_module_kit when a
    # concrete model inherits VersionedModel.
    from simorgh.core.models import VersionedModel

    assert VersionedModel._meta.abstract is True


@pytest.mark.django_db
def test_record_service_event_resolves_resource(tenant_acme, acme_tree, alice):
    from simorgh.apps.audit.models import AuditLog
    from simorgh.core.audit import record_service_event

    ws = create_workspace(tenant_acme, slug="audit-target", name="Audit")
    record_service_event("workspaces.workspace_pinged", resource=ws, after={"ok": True})

    log = AuditLog.objects.filter(action="workspaces.workspace_pinged").latest("created_at")
    assert log.resource_type == "platform_workspaces.workspace"
    assert log.resource_id == str(ws.pk)
    assert log.after == {"ok": True}


# ---------------------------------------------------------------------------
# 11.B — platform_core: Attachments
# ---------------------------------------------------------------------------


@pytest.fixture
def host_workspace(tenant_acme, acme_tree):
    """A workspace that other tests can attach things to (has tenant + org_node)."""
    return create_workspace(tenant_acme, slug="host", name="Host")


@pytest.fixture
def sample_file(tenant_acme, acme_tree, alice):
    ctx = RequestContext(actor=alice, tenant=tenant_acme)
    with use_request_context(ctx):
        return store_file(
            filename="a.txt",
            content=BytesIO(b"hello"),
            content_type="text/plain",
            organization_node_id=acme_tree["root"].pk,
            tenant_id=tenant_acme.pk,
        )


@pytest.mark.django_db
def test_attach_file_creates_attachment_and_activity(host_workspace, sample_file, alice):
    att = pc.attach_file(host_workspace, sample_file, uploaded_by=alice, description="hi")
    assert att.tenant_id == host_workspace.tenant_id
    assert att.organization_node_id == host_workspace.organization_node_id
    assert att.file_id == sample_file.pk
    assert att.uploaded_by_id == alice.pk

    listed = pc.list_attachments(host_workspace)
    assert [a.pk for a in listed] == [att.pk]

    # Activity feed records "attached"
    activities = pc.activity_for_entity(host_workspace)
    assert any(a.verb == "attached" for a in activities)


@pytest.mark.django_db
def test_attach_file_rejects_cross_tenant(tenant_acme, tenant_globex, host_workspace, alice):
    other_tree_root = host_workspace.organization_node  # belongs to acme
    foreign_file = store_file(
        filename="x.txt",
        content=BytesIO(b"x"),
        content_type="text/plain",
        organization_node_id=other_tree_root.pk,
        tenant_id=tenant_globex.pk,  # ← wrong tenant
        uploaded_by_id=alice.pk,
    )
    with pytest.raises(pc.PlatformCoreError):
        pc.attach_file(host_workspace, foreign_file)


@pytest.mark.django_db
def test_remove_attachment_soft_deletes(host_workspace, sample_file, alice):
    att = pc.attach_file(host_workspace, sample_file, uploaded_by=alice)
    pc.remove_attachment(att, actor=alice)

    # default manager hides
    assert pc.list_attachments(host_workspace) == []
    # but with_deleted() still shows it
    assert Attachment.objects.with_deleted().filter(pk=att.pk).exists()
    att.refresh_from_db()
    assert att.is_deleted is True
    assert att.deleted_by_id == alice.pk


# ---------------------------------------------------------------------------
# 11.B — platform_core: Comments
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_post_comment_threading_and_events(host_workspace, alice):
    parent = pc.post_comment(host_workspace, body="parent", author=alice)
    reply = pc.post_comment(host_workspace, body="reply", author=alice, parent=parent)
    assert reply.parent_id == parent.pk
    listed = pc.list_comments(host_workspace)
    assert {c.pk for c in listed} == {parent.pk, reply.pk}


@pytest.mark.django_db
def test_post_comment_empty_body_rejected(host_workspace, alice):
    with pytest.raises(pc.PlatformCoreError):
        pc.post_comment(host_workspace, body="   ", author=alice)


@pytest.mark.django_db
def test_edit_comment_sets_edited_at(host_workspace, alice):
    c = pc.post_comment(host_workspace, body="v1", author=alice)
    assert c.edited_at is None
    pc.edit_comment(c, body="v2", actor=alice)
    c.refresh_from_db()
    assert c.body == "v2"
    assert c.edited_at is not None


@pytest.mark.django_db
def test_delete_comment_cascades_to_replies(host_workspace, alice):
    parent = pc.post_comment(host_workspace, body="p", author=alice)
    reply = pc.post_comment(host_workspace, body="r", author=alice, parent=parent)
    pc.delete_comment(parent, actor=alice)

    parent.refresh_from_db()
    reply.refresh_from_db()
    assert parent.is_deleted is True
    assert reply.is_deleted is True


# ---------------------------------------------------------------------------
# 11.B — platform_core: Activity
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_record_activity_directly(host_workspace, alice):
    a = pc.record_activity(verb="custom", entity=host_workspace, actor=alice, extra={"k": 1})
    assert a.verb == "custom"
    assert a.actor_id == alice.pk
    assert a.extra == {"k": 1}


@pytest.mark.django_db
def test_activity_for_actor_returns_user_actions(host_workspace, alice):
    pc.record_activity(verb="x", entity=host_workspace, actor=alice)
    pc.record_activity(verb="y", entity=host_workspace, actor=alice)
    items = pc.activity_for_actor(alice)
    assert len(items) >= 2
    assert all(a.actor_id == alice.pk for a in items)


# ---------------------------------------------------------------------------
# 11.B — platform_core: Custom fields
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_define_and_set_custom_field(host_workspace):
    cfd = pc.define_custom_field(
        tenant_id=host_workspace.tenant_id,
        organization_node_id=host_workspace.organization_node_id,
        entity_type="platform_workspaces.workspace",
        key="priority",
        label_key="custom.priority",
        field_type=CustomFieldType.STRING,
    )
    assert cfd.key == "priority"
    pc.set_custom_field_value(host_workspace, definition=cfd, value="high")

    values = pc.get_custom_field_values(host_workspace)
    assert values == {"priority": "high"}

    # update_or_create flow — setting again updates
    pc.set_custom_field_value(host_workspace, definition="priority", value="low")
    assert pc.get_custom_field_values(host_workspace) == {"priority": "low"}


@pytest.mark.django_db
def test_set_custom_field_unknown_key_raises(host_workspace):
    with pytest.raises(pc.PlatformCoreError):
        pc.set_custom_field_value(host_workspace, definition="nope", value=1)


@pytest.mark.django_db
def test_custom_field_definition_unique_per_tenant(host_workspace):
    pc.define_custom_field(
        tenant_id=host_workspace.tenant_id,
        organization_node_id=host_workspace.organization_node_id,
        entity_type="platform_workspaces.workspace",
        key="dup",
        label_key="l",
        field_type=CustomFieldType.STRING,
    )
    from django.db.utils import IntegrityError

    with pytest.raises(IntegrityError):
        pc.define_custom_field(
            tenant_id=host_workspace.tenant_id,
            organization_node_id=host_workspace.organization_node_id,
            entity_type="platform_workspaces.workspace",
            key="dup",
            label_key="l",
            field_type=CustomFieldType.STRING,
        )
