"""Phase 10 — Workspaces, navigation, unified search, module contributions."""

from __future__ import annotations

import json

import pytest

from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.apps.memberships.models import Membership
from simorgh.apps.modules.contributions import (
    NavContribution,
    register_contributions,
)
from simorgh.apps.modules.contributions import (
    reset_for_tests as reset_contribs,
)
from simorgh.apps.modules.registry import (
    FeatureSpec,
    ModuleManifest,
    register_module,
)
from simorgh.apps.modules.registry import (
    reset_registry_for_tests as reset_modules,
)
from simorgh.apps.workspaces.models import NavigationItem, Workspace, WorkspaceMembership
from simorgh.apps.workspaces.services import (
    add_member,
    create_workspace,
    install_navigation_from_contributions,
    resolve_navigation,
    workspaces_for_user,
)
from simorgh.core import actions as core_actions
from simorgh.core import search as core_search
from simorgh.core import semantic as core_semantic
from simorgh.core.context import RequestContext, current_workspace, use_request_context


# ---------------------------------------------------------------------------
# Autouse fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def _reset_platform_registries():
    """Keep the cross-cutting registries isolated per test."""
    core_search.reset_for_tests()
    core_semantic.reset_for_tests()
    core_actions.reset_for_tests()
    reset_contribs()
    reset_modules()
    yield
    core_search.reset_for_tests()
    core_semantic.reset_for_tests()
    core_actions.reset_for_tests()
    reset_contribs()
    reset_modules()


@pytest.fixture
def alice_admin_membership(alice, tenant_acme, acme_tree, role_admin) -> Membership:
    """Alice with full perms in tenant_acme."""
    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["eu"],
        role=role_admin,
    )
    m.users.add(alice)
    return m


# ---------------------------------------------------------------------------
# Models / services
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_create_workspace_is_tenant_scoped(tenant_acme, acme_tree):
    ws = create_workspace(tenant_acme, slug="main", name="Main")
    assert ws.tenant_id == tenant_acme.pk
    assert ws.organization_node_id is not None
    assert ws.kind == "internal"


@pytest.mark.django_db
def test_create_workspace_unique_per_tenant(tenant_acme, tenant_globex, acme_tree):
    create_workspace(tenant_acme, slug="main", name="Main")
    # Same slug under different tenant should bootstrap fine — but tenant_globex
    # has no org nodes, so anchor lookup fails. Use a different fixture path.
    from simorgh.apps.organizations.services import create_node

    create_node(tenant_id=tenant_globex.pk, name="HQ")
    other = create_workspace(tenant_globex, slug="main", name="Main2")
    assert other.tenant_id == tenant_globex.pk

    # Duplicate under same tenant is rejected.
    with pytest.raises(Exception):  # noqa: B017
        create_workspace(tenant_acme, slug="main", name="Dup")


@pytest.mark.django_db
def test_add_and_list_workspace_members(tenant_acme, acme_tree, alice, role_admin):
    ws = create_workspace(tenant_acme, slug="main", name="Main")
    add_member(ws, alice, roles=[role_admin])
    assert WorkspaceMembership.objects.filter(workspace=ws, user=alice).exists()
    assert workspaces_for_user(alice, tenant_acme) == [ws]


# ---------------------------------------------------------------------------
# Navigation resolution
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_resolve_navigation_filters_by_permission(
    tenant_acme, acme_tree, alice, alice_admin_membership, perms,
):
    ws = create_workspace(tenant_acme, slug="main", name="Main")
    add_member(ws, alice)
    NavigationItem.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["eu"],
        workspace=ws,
        key="visible",
        label_key="nav.visible",
        route="/visible",
        permission="tenants.tenant.view",
        order=10,
    )
    NavigationItem.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["eu"],
        workspace=ws,
        key="hidden",
        label_key="nav.hidden",
        route="/hidden",
        permission="unknown.never.granted",
        order=20,
    )

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(alice_admin_membership,),
        permissions=frozenset({"tenants.tenant.view"}),
    )
    tree = resolve_navigation(ws, ctx)
    keys = {item["label_key"] for item in tree}
    assert keys == {"nav.visible"}


# ---------------------------------------------------------------------------
# Module contributions → navigation install
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_install_navigation_from_module_contributions(tenant_acme, acme_tree):
    register_module(
        ModuleManifest(
            name="demo",
            version=1,
            label_key="demo",
            features=(FeatureSpec(code="basic", label_key="demo.basic"),),
        ),
    )
    register_contributions(
        "demo",
        navigation=(
            NavContribution(key="demo.root", label_key="demo.root", route="/demo", order=10),
            NavContribution(
                key="demo.child",
                label_key="demo.child",
                route="/demo/child",
                parent_key="demo.root",
                order=20,
            ),
        ),
    )

    ws = create_workspace(tenant_acme, slug="main", name="Main")
    written = install_navigation_from_contributions(ws, "demo")
    assert written == 2

    items = list(NavigationItem.objects.filter(workspace=ws).order_by("order"))
    assert [i.key for i in items] == ["demo.root", "demo.child"]
    assert items[1].parent_id == items[0].pk

    # Idempotent — same contributions can be re-installed without growing rows.
    install_navigation_from_contributions(ws, "demo")
    assert NavigationItem.objects.filter(workspace=ws).count() == 2


# ---------------------------------------------------------------------------
# Workspace middleware / X-Workspace header
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_x_workspace_header_populates_context(
    api_client, alice, alice_admin_membership, tenant_acme, acme_tree,
):
    sync_registry_to_db()
    ws = create_workspace(tenant_acme, slug="main", name="Main")
    add_member(ws, alice)

    api_client.force_login(alice)
    resp = api_client.get(
        f"/api/v1/workspaces/{ws.slug}/bootstrap/",
        HTTP_X_TENANT=tenant_acme.slug,
        HTTP_X_WORKSPACE=ws.slug,
    )
    assert resp.status_code == 200, resp.content
    body = resp.json()
    assert body["workspace"]["slug"] == "main"
    assert body["actor"]["is_authenticated"]


# ---------------------------------------------------------------------------
# Workspace HTTP API
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_workspace_create_requires_permission(
    api_client, alice, alice_membership, tenant_acme,
):
    sync_registry_to_db()
    api_client.force_login(alice)
    resp = api_client.post(
        "/api/v1/workspaces/",
        data=json.dumps({"slug": "main", "name": "Main"}),
        content_type="application/json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 403, resp.content


@pytest.mark.django_db
def test_workspace_create_and_list(
    api_client, alice, alice_admin_membership, tenant_acme,
):
    sync_registry_to_db()
    api_client.force_login(alice)
    resp = api_client.post(
        "/api/v1/workspaces/",
        data=json.dumps({"slug": "main", "name": "Main"}),
        content_type="application/json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 201, resp.content
    # Add self as member, then list.
    ws = Workspace.objects.get(slug="main")
    add_member(ws, alice)

    resp = api_client.get("/api/v1/workspaces/", HTTP_X_TENANT=tenant_acme.slug)
    assert resp.status_code == 200
    slugs = [w["slug"] for w in resp.json()["results"]]
    assert "main" in slugs


# ---------------------------------------------------------------------------
# Unified search
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_unified_search_registry_and_filtering(
    tenant_acme, acme_tree, alice, alice_admin_membership,
):
    def adapter(ctx, query, limit):
        return [
            core_search.SearchHit(
                entity="demo.thing",
                id="1",
                title=f"hit for {query}",
                score=1.0,
            ),
        ]

    core_search.register_searchable(
        core_search.SearchableSpec(
            entity="demo.thing",
            label_key="demo.thing",
            search=adapter,
        ),
    )

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(alice_admin_membership,),
        permissions=frozenset({"tenants.tenant.view"}),
    )
    hits = core_search.run_search(ctx, "foo")
    assert len(hits) == 1
    assert hits[0].title == "hit for foo"

    # Permission gate is enforced.
    core_search.reset_for_tests()
    core_search.register_searchable(
        core_search.SearchableSpec(
            entity="demo.thing",
            label_key="demo.thing",
            search=adapter,
            permission="modules.module.view",
        ),
    )
    hits = core_search.run_search(ctx, "foo")
    assert hits == []


@pytest.mark.django_db
def test_unified_search_endpoint(
    api_client, alice, alice_admin_membership, tenant_acme,
):
    sync_registry_to_db()

    def adapter(ctx, query, limit):
        return [core_search.SearchHit(entity="demo.thing", id="42", title=query, score=2.0)]

    core_search.register_searchable(
        core_search.SearchableSpec(entity="demo.thing", label_key="x", search=adapter),
    )

    api_client.force_login(alice)
    resp = api_client.get(
        "/api/v1/search/?q=hello",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200, resp.content
    body = resp.json()
    assert body["count"] == 1
    assert body["results"][0]["title"] == "hello"


# ---------------------------------------------------------------------------
# Core re-export shims
# ---------------------------------------------------------------------------
def test_core_semantic_reexports():
    assert hasattr(core_semantic, "register_entity")
    assert hasattr(core_semantic, "EntitySpec")
    assert hasattr(core_semantic, "FieldSpec")


def test_core_actions_reexports():
    assert hasattr(core_actions, "register_action")
    assert hasattr(core_actions, "ActionSpec")
    assert hasattr(core_actions, "validate_payload")


# ---------------------------------------------------------------------------
# Context engine
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_current_workspace_helper(tenant_acme, acme_tree):
    ws = create_workspace(tenant_acme, slug="main", name="Main")
    ctx = RequestContext(actor=None, tenant=tenant_acme, workspace=ws)
    with use_request_context(ctx):
        assert current_workspace() == ws
    assert current_workspace() is None
